diff --git "a/.github/ISSUE_TEMPLATE/\352\270\260\353\212\245\352\265\254\355\230\204.md" "b/.github/ISSUE_TEMPLATE/\352\270\260\353\212\245\352\265\254\355\230\204.md" index f7caeed6c..4cd774efc 100644 --- "a/.github/ISSUE_TEMPLATE/\352\270\260\353\212\245\352\265\254\355\230\204.md" +++ "b/.github/ISSUE_TEMPLATE/\352\270\260\353\212\245\352\265\254\355\230\204.md" @@ -2,7 +2,7 @@ name: 기능구현 about: 새로운 기능 이슈 등록 title: "[FE/BE] feat:" -labels: 기능구현 +labels: '' assignees: '' --- @@ -15,4 +15,12 @@ assignees: ''
+## 체크리스트 + +- [ ] `assignee` 설정 (선택) +- [ ] `labels` 설정 +- [ ] `milestone` 설정 + +
+ ## 주의사항 diff --git "a/.github/ISSUE_TEMPLATE/\353\246\254\355\214\251\355\204\260\353\247\201.md" "b/.github/ISSUE_TEMPLATE/\353\246\254\355\214\251\355\204\260\353\247\201.md" index 4160320fc..30d8920df 100644 --- "a/.github/ISSUE_TEMPLATE/\353\246\254\355\214\251\355\204\260\353\247\201.md" +++ "b/.github/ISSUE_TEMPLATE/\353\246\254\355\214\251\355\204\260\353\247\201.md" @@ -11,4 +11,12 @@ assignees: ''
+## 체크리스트 + +- [ ] `assignee` 설정 (선택) +- [ ] `labels` 설정 +- [ ] `milestone` 설정 + +
+ ## 주의사항 diff --git "a/.github/ISSUE_TEMPLATE/\353\262\204\352\267\270-\353\246\254\355\217\254\355\212\270.md" "b/.github/ISSUE_TEMPLATE/\353\262\204\352\267\270-\353\246\254\355\217\254\355\212\270.md" new file mode 100644 index 000000000..f4574b3f3 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\353\262\204\352\267\270-\353\246\254\355\217\254\355\212\270.md" @@ -0,0 +1,17 @@ +--- +name: 버그 리포트 +about: '스탬프크러쉬 서비스를 사용하면서 생긴 버그 리포트용 ' +title: '' +labels: '' +assignees: '' + +--- + +## 버그 기능 + - 페이지 캡쳐나, 기능을 적어주세요. + + ## 버그 상황 + - 버그 상황에 대해서 간단히 설명해 주세요. + + ## 기대 동작 + - 기대했던 동작에 대해서 설명해 주세요. diff --git "a/.github/ISSUE_TEMPLATE/\353\262\204\352\267\270\353\246\254\355\217\254\355\212\270.md" "b/.github/ISSUE_TEMPLATE/\353\262\204\352\267\270\353\246\254\355\217\254\355\212\270.md" new file mode 100644 index 000000000..fa4411833 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\353\262\204\352\267\270\353\246\254\355\217\254\355\212\270.md" @@ -0,0 +1,8 @@ +## 버그 기능 +- 페이지 캡쳐나, 기능을 적어주세요. + +## 버그 상황 +- 버그 상황에 대해서 간단히 설명해 주세요. + +## 기대 동작 +- 기대했던 동작에 대해서 설명해 주세요. diff --git "a/.github/ISSUE_TEMPLATE/\353\262\204\352\267\270\354\210\230\354\240\225.md" "b/.github/ISSUE_TEMPLATE/\353\262\204\352\267\270\354\210\230\354\240\225.md" index 34ad8b601..4f2a677f2 100644 --- "a/.github/ISSUE_TEMPLATE/\353\262\204\352\267\270\354\210\230\354\240\225.md" +++ "b/.github/ISSUE_TEMPLATE/\353\262\204\352\267\270\354\210\230\354\240\225.md" @@ -11,4 +11,12 @@ assignees: ''
+## 체크리스트 + +- [ ] `assignee` 설정 (선택) +- [ ] `labels` 설정 +- [ ] `milestone` 설정 + +
+ ## 주의사항 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5e97f3ab6..b2900bced 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,13 @@ ## 주요 변경사항 ## 리뷰어에게... + +## 관련 이슈 + +closes + +## 체크리스트 + +- [ ] `reviewers` 설정 +- [ ] `label` 설정 +- [ ] `milestone` 설정 diff --git a/.github/workflows/backend_ci.yml b/.github/workflows/backend_ci.yml new file mode 100644 index 000000000..5a5591a3d --- /dev/null +++ b/.github/workflows/backend_ci.yml @@ -0,0 +1,59 @@ +name: 스탬프크러쉬 백엔드 CI 테스트 자동화 + +on: + pull_request: + branches: + - main + - develop + paths: 'backend/**' + +defaults: + run: + working-directory: backend + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - name: 리포지토리를 가져옵니다 + uses: actions/checkout@v3 +# with: +# token: ${{ secrets.SUBMODULE_TOKEN }} +# submodules: recursive + + - name: JDK 17을 설치합니다 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: Gradle 명령 실행을 위한 권한을 부여합니다 + run: chmod +x gradlew + + - name: Gradle build를 수행합니다 + run: ./gradlew build + + - name: 테스트 결과를 PR에 코멘트로 등록합니다 + uses: EnricoMi/publish-unit-test-result-action@v1 + if: always() + with: + files: '**/build/test-results/test/TEST-*.xml' + + - name: 테스트 실패 시, 실패한 코드 라인에 Check 코멘트를 등록합니다 + uses: mikepenz/action-junit-report@v3 + if: always() + with: + report_paths: '**/build/test-results/test/TEST-*.xml' + token: ${{ github.token }} + + - name: build 실패 시 Slack으로 알립니다 + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + author_name: 백엔드 빌드 실패 알림 + fields: repo, message, commit, author, action, eventName, ref, workflow, job, took + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + if: failure() diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..08571c34f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "backend/src/main/resources/security"] + path = backend/src/main/resources/security + url = https://github.com/woowacourse-teams/2023-stamp-crush-submodule.git diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..13566b81b --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 000000000..82166659c --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,44 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +http-requests-log.http + +/src/main/resources/static/docs + +logs/ +was-logs/ diff --git a/backend/build.gradle b/backend/build.gradle new file mode 100644 index 000000000..4a1df631f --- /dev/null +++ b/backend/build.gradle @@ -0,0 +1,91 @@ +buildscript { + ext { + restdocsApiSpecVersion = '0.18.2' + } +} + +plugins { + id 'java' + id 'org.springframework.boot' version '3.1.1' + id 'io.spring.dependency-management' version '1.1.0' + + id 'com.epages.restdocs-api-spec' version "${restdocsApiSpecVersion}" + id 'org.hidetake.swagger.generator' version '2.18.2' +} + +group = 'com.stampcrush' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4' + + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.mysql:mysql-connector-j' + + annotationProcessor 'org.projectlombok:lombok' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.rest-assured:rest-assured:5.3.1' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + testImplementation 'org.projectlombok:lombok' + testImplementation "com.epages:restdocs-api-spec-mockmvc:${restdocsApiSpecVersion}" + + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' +} + +tasks.named('test') { + useJUnitPlatform() +} + +openapi3 { + setServer("http://localhost:8080") + title = "스탬프크러쉬 API Docs" + description = "스탬프크러쉬 API 명세서" + version = "0.0.1" + format = "json" + + outputDirectory = 'build/resources/main/static/docs' +} + +task createOutputDirectory { + doFirst { + file(openapi3.outputDirectory).mkdirs() + } +} + +tasks.withType(GenerateSwaggerUI) { + dependsOn 'openapi3' + + delete file('src/main/resources/static/docs/') + copy { + from "build/resources/main/static/docs" + into "src/main/resources/static/docs/" + + } +} + +bootJar { + dependsOn 'createOutputDirectory', ':openapi3' +} diff --git a/backend/gradle/wrapper/gradle-wrapper.jar b/backend/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..c1962a79e Binary files /dev/null and b/backend/gradle/wrapper/gradle-wrapper.jar differ diff --git a/backend/gradle/wrapper/gradle-wrapper.properties b/backend/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..37aef8d3f --- /dev/null +++ b/backend/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/backend/gradlew b/backend/gradlew new file mode 100755 index 000000000..aeb74cbb4 --- /dev/null +++ b/backend/gradlew @@ -0,0 +1,245 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/backend/gradlew.bat b/backend/gradlew.bat new file mode 100644 index 000000000..93e3f59f1 --- /dev/null +++ b/backend/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/backend/settings.gradle b/backend/settings.gradle new file mode 100644 index 000000000..0f5036dcc --- /dev/null +++ b/backend/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'backend' diff --git a/backend/src/main/java/com/stampcrush/backend/BackendApplication.java b/backend/src/main/java/com/stampcrush/backend/BackendApplication.java new file mode 100644 index 000000000..272bc9f17 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/BackendApplication.java @@ -0,0 +1,12 @@ +package com.stampcrush.backend; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class BackendApplication { + + public static void main(String[] args) { + SpringApplication.run(BackendApplication.class, args); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/ExceptionResponse.java b/backend/src/main/java/com/stampcrush/backend/api/ExceptionResponse.java new file mode 100644 index 000000000..c2838657d --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/ExceptionResponse.java @@ -0,0 +1,11 @@ +package com.stampcrush.backend.api; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class ExceptionResponse { + + private final String exceptionMessage; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/GlobalExceptionHandler.java b/backend/src/main/java/com/stampcrush/backend/api/GlobalExceptionHandler.java new file mode 100644 index 000000000..73017c209 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/GlobalExceptionHandler.java @@ -0,0 +1,57 @@ +package com.stampcrush.backend.api; + +import com.stampcrush.backend.exception.*; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import static org.springframework.http.HttpStatus.*; + +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler(StampCrushException.class) + public ResponseEntity handleStampCrushException(StampCrushException exception) { + return ResponseEntity + .status(INTERNAL_SERVER_ERROR.value()) + .body(new ExceptionResponse(exception.getMessage())); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException(NotFoundException exception) { + return ResponseEntity + .status(NOT_FOUND.value()) + .body(new ExceptionResponse(exception.getMessage())); + } + + @ExceptionHandler(BadRequestException.class) + public ResponseEntity handleBadRequestException(BadRequestException exception) { + return ResponseEntity + .status(BAD_REQUEST.value()) + .body(new ExceptionResponse(exception.getMessage())); + } + + @ExceptionHandler(ForbiddenException.class) + public ResponseEntity handleForbiddenException(ForbiddenException exception) { + return ResponseEntity + .status(FORBIDDEN.value()) + .body(new ExceptionResponse(exception.getMessage())); + } + + @ExceptionHandler(UnAuthorizationException.class) + public ResponseEntity handleUnAuthorizationException(UnAuthorizationException exception) { + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED.value()) + .body(new ExceptionResponse(exception.getMessage())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception exception) { + return ResponseEntity + .status(INTERNAL_SERVER_ERROR.value()) + .body(new ExceptionResponse(exception.getMessage())); +// .body(new ExceptionResponse("알 수 없는 에러가 발생했습니다. 잠시 후 다시 시도해 주세요.")); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/LogController.java b/backend/src/main/java/com/stampcrush/backend/api/LogController.java new file mode 100644 index 000000000..b82bb4f1b --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/LogController.java @@ -0,0 +1,21 @@ +package com.stampcrush.backend.api; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +public class LogController { + + @GetMapping("/api/test") + public ResponseEntity logTest() { + log.debug("debug log"); + log.info("info log"); + log.warn("warn log"); + log.error("error log"); + + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeCommandApiController.java b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeCommandApiController.java new file mode 100644 index 000000000..332f6d517 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeCommandApiController.java @@ -0,0 +1,49 @@ +package com.stampcrush.backend.api.manager.cafe; + +import com.stampcrush.backend.api.manager.cafe.request.CafeCreateRequest; +import com.stampcrush.backend.api.manager.cafe.request.CafeUpdateRequest; +import com.stampcrush.backend.application.manager.cafe.ManagerCafeCommandService; +import com.stampcrush.backend.application.manager.cafe.dto.CafeCreateDto; +import com.stampcrush.backend.application.manager.cafe.dto.CafeUpdateDto; +import com.stampcrush.backend.config.resolver.OwnerAuth; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/admin/cafes") +public class ManagerCafeCommandApiController { + + private final ManagerCafeCommandService managerCafeCommandService; + + @PostMapping + ResponseEntity createCafe( + OwnerAuth owner, + @RequestBody @Valid CafeCreateRequest cafeCreateRequest + ) { + CafeCreateDto cafeCreateDto = new CafeCreateDto( + owner.getId(), + cafeCreateRequest.getName(), + cafeCreateRequest.getRoadAddress(), + cafeCreateRequest.getDetailAddress(), + cafeCreateRequest.getBusinessRegistrationNumber()); + Long cafeId = managerCafeCommandService.createCafe(cafeCreateDto); + return ResponseEntity.created(URI.create("/cafes/" + cafeId)).build(); + } + + @PatchMapping("/{cafeId}") + ResponseEntity updateCafe( + OwnerAuth owner, + @PathVariable("cafeId") Long cafeId, + @RequestBody @Valid CafeUpdateRequest request + ) { + CafeUpdateDto cafeUpdateDto = request.toServiceDto(); + + managerCafeCommandService.updateCafeInfo(cafeUpdateDto, cafeId); + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeCouponSettingCommandApiController.java b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeCouponSettingCommandApiController.java new file mode 100644 index 000000000..4a7ea58d7 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeCouponSettingCommandApiController.java @@ -0,0 +1,29 @@ +package com.stampcrush.backend.api.manager.cafe; + +import com.stampcrush.backend.api.manager.cafe.request.CafeCouponSettingUpdateRequest; +import com.stampcrush.backend.application.manager.cafe.ManagerCafeCouponSettingCommandService; +import com.stampcrush.backend.application.manager.cafe.dto.CafeCouponSettingDto; +import com.stampcrush.backend.config.resolver.OwnerAuth; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/admin/coupon-setting") +public class ManagerCafeCouponSettingCommandApiController { + + private final ManagerCafeCouponSettingCommandService managerCafeCouponSettingCommandService; + + @PostMapping + public ResponseEntity updateCafeCouponSetting( + OwnerAuth owner, + @RequestParam("cafe-id") Long cafeId, + @RequestBody @Valid CafeCouponSettingUpdateRequest request + ) { + CafeCouponSettingDto cafeCouponSettingDto = request.toCouponSettingDto(); + managerCafeCouponSettingCommandService.updateCafeCouponSetting(cafeId, cafeCouponSettingDto); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeCouponSettingFindApiController.java b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeCouponSettingFindApiController.java new file mode 100644 index 000000000..d549dec0c --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeCouponSettingFindApiController.java @@ -0,0 +1,64 @@ +package com.stampcrush.backend.api.manager.cafe; + +import com.stampcrush.backend.api.manager.cafe.response.CafeCouponCoordinateFindResponse; +import com.stampcrush.backend.api.manager.cafe.response.CafeCouponSettingFindResponse; +import com.stampcrush.backend.application.manager.cafe.ManagerCafeCouponSettingFindService; +import com.stampcrush.backend.application.manager.cafe.dto.CafeCouponSettingFindResultDto; +import com.stampcrush.backend.config.resolver.OwnerAuth; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/admin/coupon-setting") +public class ManagerCafeCouponSettingFindApiController { + + private final ManagerCafeCouponSettingFindService managerCafeCouponSettingFindService; + + @GetMapping("/{couponId}") + public ResponseEntity findCouponSetting( + OwnerAuth owner, + @RequestParam("cafe-id") Long cafeId, + @PathVariable("couponId") Long couponId + ) { + CafeCouponSettingFindResultDto cafeCouponSetting = managerCafeCouponSettingFindService.findCouponSetting(cafeId, couponId); + CafeCouponSettingFindResponse response = new CafeCouponSettingFindResponse( + cafeCouponSetting.getFrontImageUrl(), + cafeCouponSetting.getBackImageUrl(), + cafeCouponSetting.getStampImageUrl(), + cafeCouponSetting.getCoordinates().stream() + .map(stamp -> new CafeCouponCoordinateFindResponse( + stamp.getOrder(), + stamp.getXCoordinate(), + stamp.getYCoordinate())).toList()); + return ResponseEntity.status(HttpStatus.OK).body(response); + } + + @GetMapping + public ResponseEntity findCafeCouponSetting( + OwnerAuth owner, + @RequestParam("cafe-id") Long cafeId, + @PathVariable("couponId") Long couponId + ) { + CafeCouponSettingFindResultDto cafeCouponSetting = managerCafeCouponSettingFindService.findCouponSetting(cafeId, couponId); + CafeCouponSettingFindResponse response = new CafeCouponSettingFindResponse( + cafeCouponSetting.getFrontImageUrl(), + cafeCouponSetting.getBackImageUrl(), + cafeCouponSetting.getStampImageUrl(), + cafeCouponSetting.getCoordinates().stream() + .map(stamp -> new CafeCouponCoordinateFindResponse( + stamp.getOrder(), + stamp.getXCoordinate(), + stamp.getYCoordinate())).toList()); + return ResponseEntity.status(HttpStatus.OK).body(response); + } + + +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeFindApiController.java b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeFindApiController.java new file mode 100644 index 000000000..39bff06ce --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeFindApiController.java @@ -0,0 +1,32 @@ +package com.stampcrush.backend.api.manager.cafe; + +import com.stampcrush.backend.api.manager.cafe.response.CafeFindResponse; +import com.stampcrush.backend.api.manager.cafe.response.CafesFindResponse; +import com.stampcrush.backend.application.manager.cafe.ManagerCafeFindService; +import com.stampcrush.backend.application.manager.cafe.dto.CafeFindResultDto; +import com.stampcrush.backend.config.resolver.OwnerAuth; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/admin/cafes") +public class ManagerCafeFindApiController { + + private final ManagerCafeFindService managerCafeFindService; + + @GetMapping + ResponseEntity findAllCafes(OwnerAuth owner) { + List cafeFindResultDtos = managerCafeFindService.findCafesByOwner(owner.getId()); + List cafeFindResponses = cafeFindResultDtos.stream() + .map(CafeFindResponse::from) + .toList(); + CafesFindResponse response = new CafesFindResponse(cafeFindResponses); + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/request/CafeCouponSettingUpdateRequest.java b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/request/CafeCouponSettingUpdateRequest.java new file mode 100644 index 000000000..72e783ded --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/request/CafeCouponSettingUpdateRequest.java @@ -0,0 +1,87 @@ +package com.stampcrush.backend.api.manager.cafe.request; + +import com.stampcrush.backend.application.manager.cafe.dto.CafeCouponSettingDto; +import jakarta.validation.constraints.*; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class CafeCouponSettingUpdateRequest { + + @NotNull + @NotBlank + private final String frontImageUrl; + + @NotNull + @NotBlank + private final String backImageUrl; + + @NotNull + @NotBlank + private final String stampImageUrl; + + @NotEmpty + private final List coordinates; + + @NotNull + @NotBlank + private final String reward; + + @NotNull + @Positive + private final Integer expirePeriod; + + @RequiredArgsConstructor + public static class CouponStampCoordinateRequest { + + @NotNull + @Positive + private final Integer order; + + @NotNull + @PositiveOrZero + private final Integer xCoordinate; + + @NotNull + @PositiveOrZero + private final Integer yCoordinate; + + public CafeCouponSettingDto.CafeCouponDesignDto.CafeStampCoordinateDto toCafeStampCoordinateDto() { + return new CafeCouponSettingDto.CafeCouponDesignDto.CafeStampCoordinateDto(order, xCoordinate, yCoordinate); + } + + public Integer getOrder() { + return order; + } + + public Integer getxCoordinate() { + return xCoordinate; + } + + public Integer getyCoordinate() { + return yCoordinate; + } + } + + public CafeCouponSettingDto toCouponSettingDto() { + CafeCouponSettingDto.CafeCouponDesignDto cafeCouponDesignDto = new CafeCouponSettingDto.CafeCouponDesignDto( + frontImageUrl, + backImageUrl, + stampImageUrl, + coordinates.stream() + .map(CouponStampCoordinateRequest::toCafeStampCoordinateDto) + .toList() + ); + + CafeCouponSettingDto.CafePolicyDto cafePolicyDto = new CafeCouponSettingDto.CafePolicyDto( + coordinates.size(), + reward, + expirePeriod + ); + + return new CafeCouponSettingDto(cafeCouponDesignDto, cafePolicyDto); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/request/CafeCreateRequest.java b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/request/CafeCreateRequest.java new file mode 100644 index 000000000..1e3f86d3b --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/request/CafeCreateRequest.java @@ -0,0 +1,29 @@ +package com.stampcrush.backend.api.manager.cafe.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class CafeCreateRequest { + + @NotNull + @NotBlank + private String name; + + @NotNull + @NotBlank + private String roadAddress; + + @NotNull + @NotBlank + private String detailAddress; + + @NotNull + @NotBlank + private String businessRegistrationNumber; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/request/CafeUpdateRequest.java b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/request/CafeUpdateRequest.java new file mode 100644 index 000000000..7b70df4a4 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/request/CafeUpdateRequest.java @@ -0,0 +1,36 @@ +package com.stampcrush.backend.api.manager.cafe.request; + +import com.stampcrush.backend.application.manager.cafe.dto.CafeUpdateDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalTime; + +import static lombok.AccessLevel.PROTECTED; + +@NoArgsConstructor(access = PROTECTED) +@AllArgsConstructor +@Getter +public class CafeUpdateRequest { + + private String introduction; + + private LocalTime openTime; + + private LocalTime closeTime; + + private String telephoneNumber; + + private String cafeImageUrl; + + public CafeUpdateDto toServiceDto() { + return new CafeUpdateDto( + this.introduction, + this.openTime, + this.closeTime, + this.telephoneNumber, + this.cafeImageUrl + ); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/response/CafeCouponCoordinateFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/response/CafeCouponCoordinateFindResponse.java new file mode 100644 index 000000000..b2631db22 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/response/CafeCouponCoordinateFindResponse.java @@ -0,0 +1,23 @@ +package com.stampcrush.backend.api.manager.cafe.response; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class CafeCouponCoordinateFindResponse { + + private final Integer order; + private final Integer xCoordinate; + private final Integer yCoordinate; + + public Integer getOrder() { + return order; + } + + public Integer getxCoordinate() { + return xCoordinate; + } + + public Integer getyCoordinate() { + return yCoordinate; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/response/CafeCouponSettingFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/response/CafeCouponSettingFindResponse.java new file mode 100644 index 000000000..26b8a38df --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/response/CafeCouponSettingFindResponse.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.api.manager.cafe.response; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class CafeCouponSettingFindResponse { + + private final String frontImageUrl; + private final String backImageUrl; + private final String stampImageUrl; + private final List coordinates; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/response/CafeFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/response/CafeFindResponse.java new file mode 100644 index 000000000..902e9dede --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/response/CafeFindResponse.java @@ -0,0 +1,44 @@ +package com.stampcrush.backend.api.manager.cafe.response; + +import com.stampcrush.backend.application.manager.cafe.dto.CafeFindResultDto; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalTime; + +@Getter +@RequiredArgsConstructor +public class CafeFindResponse { + + private final Long id; + private final String name; + + @DateTimeFormat(pattern = "HH:mm") + private final LocalTime openTime; + + @DateTimeFormat(pattern = "HH:mm") + private final LocalTime closeTime; + + private final String telephoneNumber; + private final String cafeImageUrl; + private final String roadAddress; + private final String detailAddress; + private final String businessRegistrationNumber; + private final String introduction; + + public static CafeFindResponse from(CafeFindResultDto cafeFindResultDto) { + return new CafeFindResponse( + cafeFindResultDto.getId(), + cafeFindResultDto.getName(), + cafeFindResultDto.getOpenTime(), + cafeFindResultDto.getCloseTime(), + cafeFindResultDto.getTelephoneNumber(), + cafeFindResultDto.getCafeImageUrl(), + cafeFindResultDto.getRoadAddress(), + cafeFindResultDto.getDetailAddress(), + cafeFindResultDto.getBusinessRegistrationNumber(), + cafeFindResultDto.getIntroduction() + ); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/response/CafesFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/response/CafesFindResponse.java new file mode 100644 index 000000000..e03d183d7 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/cafe/response/CafesFindResponse.java @@ -0,0 +1,13 @@ +package com.stampcrush.backend.api.manager.cafe.response; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class CafesFindResponse { + + private final List cafes; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/ManagerCouponCommandApiController.java b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/ManagerCouponCommandApiController.java new file mode 100644 index 000000000..d1c770a4e --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/ManagerCouponCommandApiController.java @@ -0,0 +1,43 @@ +package com.stampcrush.backend.api.manager.coupon; + +import com.stampcrush.backend.api.manager.coupon.request.CouponCreateRequest; +import com.stampcrush.backend.api.manager.coupon.request.StampCreateRequest; +import com.stampcrush.backend.api.manager.coupon.response.CouponCreateResponse; +import com.stampcrush.backend.application.manager.coupon.ManagerCouponCommandService; +import com.stampcrush.backend.application.manager.coupon.dto.StampCreateDto; +import com.stampcrush.backend.config.resolver.OwnerAuth; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/admin") +public class ManagerCouponCommandApiController { + + private final ManagerCouponCommandService managerCouponCommandService; + + @PostMapping("/customers/{customerId}/coupons") + public ResponseEntity createCoupon( + OwnerAuth owner, + @RequestBody @Valid CouponCreateRequest request, + @PathVariable("customerId") Long customerId + ) { + Long couponId = managerCouponCommandService.createCoupon(request.getCafeId(), customerId); + return ResponseEntity.status(HttpStatus.CREATED).body(new CouponCreateResponse(couponId)); + } + + @PostMapping("/customers/{customerId}/coupons/{couponId}/stamps") + public ResponseEntity createStamp( + OwnerAuth owner, + @PathVariable("customerId") Long customerId, + @PathVariable("couponId") Long couponId, + @RequestBody @Valid StampCreateRequest request + ) { + StampCreateDto stampCreateDto = new StampCreateDto(owner.getId(), customerId, couponId, request.getEarningStampCount()); + managerCouponCommandService.createStamp(stampCreateDto); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/ManagerCouponFindApiController.java b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/ManagerCouponFindApiController.java new file mode 100644 index 000000000..f0e24a9d9 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/ManagerCouponFindApiController.java @@ -0,0 +1,52 @@ +package com.stampcrush.backend.api.manager.coupon; + +import com.stampcrush.backend.api.manager.coupon.response.CafeCustomerFindResponse; +import com.stampcrush.backend.api.manager.coupon.response.CafeCustomersFindResponse; +import com.stampcrush.backend.api.manager.coupon.response.CustomerAccumulatingCouponFindResponse; +import com.stampcrush.backend.api.manager.coupon.response.CustomerAccumulatingCouponsFindResponse; +import com.stampcrush.backend.application.manager.coupon.ManagerCouponFindService; +import com.stampcrush.backend.application.manager.coupon.dto.CafeCustomerFindResultDto; +import com.stampcrush.backend.application.manager.coupon.dto.CustomerAccumulatingCouponFindResultDto; +import com.stampcrush.backend.config.resolver.OwnerAuth; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/admin") +public class ManagerCouponFindApiController { + + private final ManagerCouponFindService managerCouponFindService; + + @GetMapping("/cafes/{cafeId}/customers") + public ResponseEntity findCustomersByCafe( + OwnerAuth owner, + @PathVariable("cafeId") Long cafeId + ) { + List coupons = managerCouponFindService.findCouponsByCafe(cafeId); + List cafeCustomerFindResponses = coupons.stream() + .map(CafeCustomerFindResponse::from) + .toList(); + + return ResponseEntity.ok(new CafeCustomersFindResponse(cafeCustomerFindResponses)); + } + + @GetMapping("/customers/{customerId}/coupons") + public ResponseEntity findCustomerUsingCouponByCafe( + OwnerAuth owner, + @PathVariable("customerId") Long customerId, + @RequestParam("cafe-id") Long cafeId, + @RequestParam("active") boolean active + ) { + List accumulatingCoupon = managerCouponFindService.findAccumulatingCoupon(cafeId, customerId); + + List accumulatingResponses = accumulatingCoupon.stream() + .map(CustomerAccumulatingCouponFindResponse::from) + .toList(); + + return ResponseEntity.ok(new CustomerAccumulatingCouponsFindResponse(accumulatingResponses)); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/request/CouponCreateRequest.java b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/request/CouponCreateRequest.java new file mode 100644 index 000000000..2ed972479 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/request/CouponCreateRequest.java @@ -0,0 +1,17 @@ +package com.stampcrush.backend.api.manager.coupon.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class CouponCreateRequest { + + @NotNull + @Positive + private Long cafeId; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/request/StampCreateRequest.java b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/request/StampCreateRequest.java new file mode 100644 index 000000000..360283bc0 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/request/StampCreateRequest.java @@ -0,0 +1,17 @@ +package com.stampcrush.backend.api.manager.coupon.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class StampCreateRequest { + + @NotNull + @PositiveOrZero + private Integer earningStampCount; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/response/CafeCustomerFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/response/CafeCustomerFindResponse.java new file mode 100644 index 000000000..7e18a7914 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/response/CafeCustomerFindResponse.java @@ -0,0 +1,36 @@ +package com.stampcrush.backend.api.manager.coupon.response; + +import com.stampcrush.backend.application.manager.coupon.dto.CafeCustomerFindResultDto; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.format.DateTimeFormatter; + +@EqualsAndHashCode +@RequiredArgsConstructor +@Getter +public class CafeCustomerFindResponse { + + private final Long id; + private final String nickname; + private final int stampCount; + private final int rewardCount; + private final int visitCount; + private final int maxStampCount; + private final String firstVisitDate; + private final Boolean isRegistered; + + public static CafeCustomerFindResponse from(CafeCustomerFindResultDto serviceDto) { + return new CafeCustomerFindResponse( + serviceDto.getId(), + serviceDto.getNickname(), + serviceDto.getStampCount(), + serviceDto.getRewardCount(), + serviceDto.getVisitCount(), + serviceDto.getMaxStampCount(), + serviceDto.getFirstVisitDate().format(DateTimeFormatter.ofPattern("yyyy:MM:dd")), + serviceDto.isRegistered() + ); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/response/CafeCustomersFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/response/CafeCustomersFindResponse.java new file mode 100644 index 000000000..9c7a75d54 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/response/CafeCustomersFindResponse.java @@ -0,0 +1,15 @@ +package com.stampcrush.backend.api.manager.coupon.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class CafeCustomersFindResponse { + + private List customers; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/response/CouponCreateResponse.java b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/response/CouponCreateResponse.java new file mode 100644 index 000000000..48adc0163 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/response/CouponCreateResponse.java @@ -0,0 +1,13 @@ +package com.stampcrush.backend.api.manager.coupon.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class CouponCreateResponse { + + private Long couponId; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/response/CustomerAccumulatingCouponFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/response/CustomerAccumulatingCouponFindResponse.java new file mode 100644 index 000000000..57fda1b73 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/response/CustomerAccumulatingCouponFindResponse.java @@ -0,0 +1,36 @@ +package com.stampcrush.backend.api.manager.coupon.response; + +import com.stampcrush.backend.application.manager.coupon.dto.CustomerAccumulatingCouponFindResultDto; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.format.DateTimeFormatter; + +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class CustomerAccumulatingCouponFindResponse { + + private Long id; + private Long customerId; + private String nickname; + private int stampCount; + private String expireDate; + private Boolean isPrevious; + private int maxStampCount; + + public static CustomerAccumulatingCouponFindResponse from(CustomerAccumulatingCouponFindResultDto serviceDto) { + return new CustomerAccumulatingCouponFindResponse( + serviceDto.getId(), + serviceDto.getCustomerId(), + serviceDto.getNickname(), + serviceDto.getStampCount(), + serviceDto.getExpireDate().format(DateTimeFormatter.ofPattern("yyyy:MM:dd")), + serviceDto.isPrevious(), + serviceDto.getMaxStampCount() + ); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/response/CustomerAccumulatingCouponsFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/response/CustomerAccumulatingCouponsFindResponse.java new file mode 100644 index 000000000..3c016ef3b --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/coupon/response/CustomerAccumulatingCouponsFindResponse.java @@ -0,0 +1,15 @@ +package com.stampcrush.backend.api.manager.coupon.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class CustomerAccumulatingCouponsFindResponse { + + private List coupons; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/customer/ManagerCustomerCommandApiController.java b/backend/src/main/java/com/stampcrush/backend/api/manager/customer/ManagerCustomerCommandApiController.java new file mode 100644 index 000000000..21df90f8d --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/customer/ManagerCustomerCommandApiController.java @@ -0,0 +1,32 @@ +package com.stampcrush.backend.api.manager.customer; + +import com.stampcrush.backend.api.manager.customer.request.TemporaryCustomerCreateRequest; +import com.stampcrush.backend.application.manager.customer.ManagerCustomerCommandService; +import com.stampcrush.backend.config.resolver.OwnerAuth; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.net.URI; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/admin") +public class ManagerCustomerCommandApiController { + + private final ManagerCustomerCommandService managerCustomerCommandService; + + @PostMapping("/temporary-customers") + public ResponseEntity createTemporaryCustomer( + OwnerAuth owner, + @RequestBody @Valid TemporaryCustomerCreateRequest request + ) { + Long customerId = managerCustomerCommandService.createTemporaryCustomer(request.getPhoneNumber()); + + return ResponseEntity.created(URI.create("/customers/" + customerId)).build(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/customer/ManagerCustomerFindApiController.java b/backend/src/main/java/com/stampcrush/backend/api/manager/customer/ManagerCustomerFindApiController.java new file mode 100644 index 000000000..e468412b0 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/customer/ManagerCustomerFindApiController.java @@ -0,0 +1,38 @@ +package com.stampcrush.backend.api.manager.customer; + +import com.stampcrush.backend.api.manager.customer.response.CustomerFindResponse; +import com.stampcrush.backend.api.manager.customer.response.CustomersFindResponse; +import com.stampcrush.backend.application.manager.customer.ManagerCustomerFindService; +import com.stampcrush.backend.application.manager.customer.dto.CustomerFindDto; +import com.stampcrush.backend.config.resolver.OwnerAuth; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static java.util.stream.Collectors.toList; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/admin") +public class ManagerCustomerFindApiController { + + private final ManagerCustomerFindService managerCustomerFindService; + + @GetMapping("/customers") + public ResponseEntity findCustomer( + OwnerAuth owner, + @RequestParam("phone-number") String phoneNumber + ) { + List customers = managerCustomerFindService.findCustomer(phoneNumber); + + List customerFindResponses = customers.stream() + .map(CustomerFindResponse::from).collect(toList()); + + return ResponseEntity.ok().body(new CustomersFindResponse(customerFindResponses)); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/customer/request/TemporaryCustomerCreateRequest.java b/backend/src/main/java/com/stampcrush/backend/api/manager/customer/request/TemporaryCustomerCreateRequest.java new file mode 100644 index 000000000..42a185682 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/customer/request/TemporaryCustomerCreateRequest.java @@ -0,0 +1,17 @@ +package com.stampcrush.backend.api.manager.customer.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class TemporaryCustomerCreateRequest { + + @NotNull + @NotBlank + private String phoneNumber; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/customer/response/CustomerFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/manager/customer/response/CustomerFindResponse.java new file mode 100644 index 000000000..c33e94a37 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/customer/response/CustomerFindResponse.java @@ -0,0 +1,20 @@ +package com.stampcrush.backend.api.manager.customer.response; + +import com.stampcrush.backend.application.manager.customer.dto.CustomerFindDto; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@EqualsAndHashCode +@AllArgsConstructor +@Getter +public class CustomerFindResponse { + + private Long id; + private String nickname; + private String phoneNumber; + + public static CustomerFindResponse from(CustomerFindDto customerFindDto) { + return new CustomerFindResponse(customerFindDto.getId(), customerFindDto.getNickname(), customerFindDto.getPhoneNumber()); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/customer/response/CustomersFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/manager/customer/response/CustomersFindResponse.java new file mode 100644 index 000000000..59b8f1c8d --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/customer/response/CustomersFindResponse.java @@ -0,0 +1,21 @@ +package com.stampcrush.backend.api.manager.customer.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +import static lombok.AccessLevel.PROTECTED; + +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = PROTECTED) +public class CustomersFindResponse { + + private List customer; + + public static CustomersFindResponse from(List customerFindResponses) { + return new CustomersFindResponse(customerFindResponses); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/image/ManagerImageCommandApiController.java b/backend/src/main/java/com/stampcrush/backend/api/manager/image/ManagerImageCommandApiController.java new file mode 100644 index 000000000..1ae2a4ef4 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/image/ManagerImageCommandApiController.java @@ -0,0 +1,25 @@ +package com.stampcrush.backend.api.manager.image; + +import com.stampcrush.backend.api.manager.image.response.ImageUrlResponse; +import com.stampcrush.backend.application.manager.image.ManagerImageCommandService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/admin") +public class ManagerImageCommandApiController { + + private final ManagerImageCommandService managerImageCommandService; + + @PostMapping("/images") + public ResponseEntity uploadImage(@RequestPart MultipartFile image) { + String url = managerImageCommandService.uploadImageAndReturnUrl(image); + return ResponseEntity.ok(new ImageUrlResponse(url)); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/image/response/ImageUrlResponse.java b/backend/src/main/java/com/stampcrush/backend/api/manager/image/response/ImageUrlResponse.java new file mode 100644 index 000000000..33b828769 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/image/response/ImageUrlResponse.java @@ -0,0 +1,14 @@ +package com.stampcrush.backend.api.manager.image.response; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ImageUrlResponse { + + private String imageUrl; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/reward/ManagerRewardCommandApiController.java b/backend/src/main/java/com/stampcrush/backend/api/manager/reward/ManagerRewardCommandApiController.java new file mode 100644 index 000000000..85cb86cac --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/reward/ManagerRewardCommandApiController.java @@ -0,0 +1,30 @@ +package com.stampcrush.backend.api.manager.reward; + +import com.stampcrush.backend.api.manager.reward.request.RewardUsedUpdateRequest; +import com.stampcrush.backend.application.manager.reward.ManagerRewardCommandService; +import com.stampcrush.backend.application.manager.reward.dto.RewardUsedUpdateDto; +import com.stampcrush.backend.config.resolver.OwnerAuth; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/admin/customers/{customerId}/rewards") +public class ManagerRewardCommandApiController { + + private final ManagerRewardCommandService managerRewardCommandService; + + @PatchMapping("/{rewardId}") + public ResponseEntity updateRewardToUsed( + OwnerAuth owner, + @PathVariable("customerId") Long customerId, + @PathVariable("rewardId") Long rewardId, + @RequestBody @Valid RewardUsedUpdateRequest request + ) { + RewardUsedUpdateDto rewardUsedUpdateDto = new RewardUsedUpdateDto(rewardId, customerId, request.getCafeId(), request.getUsed()); + managerRewardCommandService.useReward(rewardUsedUpdateDto); + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/reward/ManagerRewardFindApiController.java b/backend/src/main/java/com/stampcrush/backend/api/manager/reward/ManagerRewardFindApiController.java new file mode 100644 index 000000000..d0c2811d8 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/reward/ManagerRewardFindApiController.java @@ -0,0 +1,38 @@ +package com.stampcrush.backend.api.manager.reward; + +import com.stampcrush.backend.api.manager.reward.response.RewardFindResponse; +import com.stampcrush.backend.api.manager.reward.response.RewardsFindResponse; +import com.stampcrush.backend.application.manager.reward.ManagerRewardFindService; +import com.stampcrush.backend.application.manager.reward.dto.RewardFindDto; +import com.stampcrush.backend.application.manager.reward.dto.RewardFindResultDto; +import com.stampcrush.backend.config.resolver.OwnerAuth; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/admin/customers/{customerId}/rewards") +public class ManagerRewardFindApiController { + + private final ManagerRewardFindService managerRewardFindService; + + @GetMapping + public ResponseEntity findRewards( + OwnerAuth owner, + @PathVariable("customerId") Long customerId, + @RequestParam("cafe-id") Long cafeId, + @RequestParam("used") Boolean used + ) { + RewardFindDto rewardFindDto = new RewardFindDto(customerId, cafeId, used); + List rewardFindResultDtos = managerRewardFindService.findRewards(rewardFindDto); + + List rewardFindResponses = rewardFindResultDtos.stream() + .map(RewardFindResponse::new) + .toList(); + RewardsFindResponse response = new RewardsFindResponse(rewardFindResponses); + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/reward/request/RewardFindRequest.java b/backend/src/main/java/com/stampcrush/backend/api/manager/reward/request/RewardFindRequest.java new file mode 100644 index 000000000..ac0520376 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/reward/request/RewardFindRequest.java @@ -0,0 +1,14 @@ +package com.stampcrush.backend.api.manager.reward.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class RewardFindRequest { + + private Long cafeId; + private Boolean used; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/reward/request/RewardUsedUpdateRequest.java b/backend/src/main/java/com/stampcrush/backend/api/manager/reward/request/RewardUsedUpdateRequest.java new file mode 100644 index 000000000..0a3e75568 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/reward/request/RewardUsedUpdateRequest.java @@ -0,0 +1,21 @@ +package com.stampcrush.backend.api.manager.reward.request; + +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class RewardUsedUpdateRequest { + + @NotNull + @Positive + private Long cafeId; + + @AssertTrue + private Boolean used; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/reward/response/RewardFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/manager/reward/response/RewardFindResponse.java new file mode 100644 index 000000000..eac52263b --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/reward/response/RewardFindResponse.java @@ -0,0 +1,18 @@ +package com.stampcrush.backend.api.manager.reward.response; + +import com.stampcrush.backend.application.manager.reward.dto.RewardFindResultDto; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class RewardFindResponse { + + private final Long id; + private final String name; + + public RewardFindResponse(RewardFindResultDto rewardFindResultDto) { + this.id = rewardFindResultDto.getId(); + this.name = rewardFindResultDto.getName(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/reward/response/RewardsFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/manager/reward/response/RewardsFindResponse.java new file mode 100644 index 000000000..294f852cc --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/reward/response/RewardsFindResponse.java @@ -0,0 +1,13 @@ +package com.stampcrush.backend.api.manager.reward.response; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class RewardsFindResponse { + + private final List rewards; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/sample/ManagerSampleCouponFindApiController.java b/backend/src/main/java/com/stampcrush/backend/api/manager/sample/ManagerSampleCouponFindApiController.java new file mode 100644 index 000000000..215fe34f2 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/sample/ManagerSampleCouponFindApiController.java @@ -0,0 +1,30 @@ +package com.stampcrush.backend.api.manager.sample; + +import com.stampcrush.backend.api.manager.sample.response.SampleCouponFindResponse; +import com.stampcrush.backend.application.manager.sample.ManagerSampleCouponFindService; +import com.stampcrush.backend.application.manager.sample.dto.SampleCouponsFindResultDto; +import com.stampcrush.backend.config.resolver.OwnerAuth; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/admin/coupon-samples") +public class ManagerSampleCouponFindApiController { + + private final ManagerSampleCouponFindService managerSampleCouponFindService; + + @GetMapping + public ResponseEntity findSampleCoupons( + OwnerAuth owner, + @RequestParam("max-stamp-count") Integer maxStampCount + ) { + SampleCouponsFindResultDto sampleCoupons = managerSampleCouponFindService.findSampleCouponsByMaxStampCount(maxStampCount); + return ResponseEntity.ok() + .body(SampleCouponFindResponse.from(sampleCoupons)); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/manager/sample/response/SampleCouponFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/manager/sample/response/SampleCouponFindResponse.java new file mode 100644 index 000000000..a700d01b3 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/manager/sample/response/SampleCouponFindResponse.java @@ -0,0 +1,101 @@ +package com.stampcrush.backend.api.manager.sample.response; + +import com.stampcrush.backend.application.manager.sample.dto.SampleCouponsFindResultDto; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class SampleCouponFindResponse { + + private final List sampleFrontImages; + private final List sampleBackImages; + private final List sampleStampImages; + + public static SampleCouponFindResponse from(SampleCouponsFindResultDto sampleCoupons) { + List sampleFrontImages = sampleCoupons.getSampleFrontImages() + .stream() + .map(SampleFrontImageFindResponse::from) + .toList(); + + List backImages = sampleCoupons.getSampleBackImages(); + List coordinates = sampleCoupons.getSampleStampCoordinates(); + + List sampleBackImages = backImages + .stream() + .map(it -> new SampleBackImageFindResponse( + it.getId(), + it.getImageUrl(), + coordinates.stream() + .filter(coordinate -> coordinate.isCorrespondingCoordinateSampleBackImage(it.getId())) + .map(coordinate -> new SampleBackImageFindResponse.SampleStampCoordinateFindResponse( + coordinate.getOrder(), + coordinate.getXCoordinate(), + coordinate.getYCoordinate() + )) + .toList() + )) + .toList(); + + List sampleStampImages = sampleCoupons.getSampleStampImages() + .stream() + .map(sampleStampImage -> new SampleStampImageFindResponse( + sampleStampImage.getId(), + sampleStampImage.getImageUrl() + )) + .toList(); + + return new SampleCouponFindResponse(sampleFrontImages, sampleBackImages, sampleStampImages); + } + + @Getter + @RequiredArgsConstructor + public static class SampleFrontImageFindResponse { + + private final Long id; + private final String imageUrl; + + public static SampleFrontImageFindResponse from(SampleCouponsFindResultDto.SampleFrontImageDto sampleFrontImage) { + return new SampleFrontImageFindResponse(sampleFrontImage.getId(), sampleFrontImage.getImageUrl()); + } + } + + @Getter + @RequiredArgsConstructor + public static class SampleBackImageFindResponse { + + private final Long id; + private final String imageUrl; + private final List stampCoordinates; + + @RequiredArgsConstructor + public static class SampleStampCoordinateFindResponse { + + private final Integer order; + private final Integer xCoordinate; + private final Integer yCoordinate; + + public Integer getOrder() { + return order; + } + + public Integer getxCoordinate() { + return xCoordinate; + } + + public Integer getyCoordinate() { + return yCoordinate; + } + } + } + + @Getter + @RequiredArgsConstructor + public static class SampleStampImageFindResponse { + + private final Long id; + private final String imageUrl; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/cafe/VisitorCafeFindApiController.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/cafe/VisitorCafeFindApiController.java new file mode 100644 index 000000000..e881a72d0 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/cafe/VisitorCafeFindApiController.java @@ -0,0 +1,28 @@ +package com.stampcrush.backend.api.visitor.cafe; + +import com.stampcrush.backend.api.visitor.cafe.response.CafeInfoFindByCustomerResponse; +import com.stampcrush.backend.api.visitor.cafe.response.CafeInfoFindResponse; +import com.stampcrush.backend.application.visitor.cafe.VisitorCafeFindService; +import com.stampcrush.backend.application.visitor.cafe.dto.CafeInfoFindByCustomerResultDto; +import com.stampcrush.backend.config.resolver.CustomerAuth; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/cafes") +public class VisitorCafeFindApiController { + + private final VisitorCafeFindService visitorCafeFindService; + + @GetMapping("/{cafeId}") + ResponseEntity findCafe(CustomerAuth customer, @PathVariable Long cafeId) { + CafeInfoFindByCustomerResultDto cafeInfoFindByCustomerResultDto = visitorCafeFindService.findCafeById(cafeId); + CafeInfoFindResponse response = CafeInfoFindResponse.from(cafeInfoFindByCustomerResultDto); + return ResponseEntity.ok().body(new CafeInfoFindByCustomerResponse(response)); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/cafe/response/CafeInfoFindByCustomerResponse.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/cafe/response/CafeInfoFindByCustomerResponse.java new file mode 100644 index 000000000..04cc3469f --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/cafe/response/CafeInfoFindByCustomerResponse.java @@ -0,0 +1,15 @@ +package com.stampcrush.backend.api.visitor.cafe.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static lombok.AccessLevel.PROTECTED; + +@AllArgsConstructor +@NoArgsConstructor(access = PROTECTED) +@Getter +public class CafeInfoFindByCustomerResponse { + + private CafeInfoFindResponse cafe; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/cafe/response/CafeInfoFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/cafe/response/CafeInfoFindResponse.java new file mode 100644 index 000000000..278a5bd71 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/cafe/response/CafeInfoFindResponse.java @@ -0,0 +1,41 @@ +package com.stampcrush.backend.api.visitor.cafe.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.stampcrush.backend.application.visitor.cafe.dto.CafeInfoFindByCustomerResultDto; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalTime; + +@Getter +@RequiredArgsConstructor +public class CafeInfoFindResponse { + + private final Long id; + private final String name; + private final String introduction; + + @JsonFormat(pattern = "HH:mm") + private final LocalTime openTime; + + @JsonFormat(pattern = "HH:mm") + private final LocalTime closeTime; + + private final String telephoneNumber; + private final String cafeImageUrl; + private final String roadAddress; + private final String detailAddress; + + public static CafeInfoFindResponse from(CafeInfoFindByCustomerResultDto cafeInfoFindByCustomerResultDto) { + return new CafeInfoFindResponse( + cafeInfoFindByCustomerResultDto.getId(), + cafeInfoFindByCustomerResultDto.getName(), + cafeInfoFindByCustomerResultDto.getIntroduction(), + cafeInfoFindByCustomerResultDto.getOpenTime(), + cafeInfoFindByCustomerResultDto.getCloseTime(), + cafeInfoFindByCustomerResultDto.getTelephoneNumber(), + cafeInfoFindByCustomerResultDto.getCafeImageUrl(), + cafeInfoFindByCustomerResultDto.getRoadAddress(), + cafeInfoFindByCustomerResultDto.getDetailAddress()); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/coupon/VisitorCouponCommandApiController.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/coupon/VisitorCouponCommandApiController.java new file mode 100644 index 000000000..3cc1ce955 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/coupon/VisitorCouponCommandApiController.java @@ -0,0 +1,24 @@ +package com.stampcrush.backend.api.visitor.coupon; + +import com.stampcrush.backend.application.visitor.coupon.VisitorCouponCommandService; +import com.stampcrush.backend.config.resolver.CustomerAuth; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/coupons") +public class VisitorCouponCommandApiController { + + private final VisitorCouponCommandService visitorCouponCommandService; + + @DeleteMapping("/{couponId}") + public ResponseEntity deleteCoupon(CustomerAuth customer, @PathVariable Long couponId) { + visitorCouponCommandService.deleteCoupon(customer.getId(), couponId); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/coupon/VisitorCouponFindApiController.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/coupon/VisitorCouponFindApiController.java new file mode 100644 index 000000000..e05937272 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/coupon/VisitorCouponFindApiController.java @@ -0,0 +1,28 @@ +package com.stampcrush.backend.api.visitor.coupon; + +import com.stampcrush.backend.api.visitor.coupon.response.CustomerCouponsFindResponse; +import com.stampcrush.backend.application.visitor.coupon.VisitorCouponFindService; +import com.stampcrush.backend.application.visitor.coupon.dto.CustomerCouponFindResultDto; +import com.stampcrush.backend.config.resolver.CustomerAuth; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api") +public class VisitorCouponFindApiController { + + private final VisitorCouponFindService visitorCouponFindService; + + @GetMapping("/coupons") + public ResponseEntity findOneCouponForOneCafe(CustomerAuth customer) { + List coupons = visitorCouponFindService.findOneCouponForOneCafe(customer.getId()); + return ResponseEntity.ok() + .body(CustomerCouponsFindResponse.from(coupons)); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/coupon/response/CustomerCouponFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/coupon/response/CustomerCouponFindResponse.java new file mode 100644 index 000000000..bd452425c --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/coupon/response/CustomerCouponFindResponse.java @@ -0,0 +1,99 @@ +package com.stampcrush.backend.api.visitor.coupon.response; + +import com.stampcrush.backend.application.visitor.coupon.dto.CustomerCouponFindResultDto; +import com.stampcrush.backend.entity.coupon.CouponStatus; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; +import java.util.stream.Stream; + +@Getter +@RequiredArgsConstructor +public class CustomerCouponFindResponse { + + private final CafeInfoResponse cafeInfo; + private final List couponInfos; + + public static CustomerCouponFindResponse from(CustomerCouponFindResultDto dto) { + CafeInfoResponse cafeInfo = CafeInfoResponse.from(dto.getCafeInfoDto()); + List couponInfos = Stream.of(dto.getCouponInfoDto()) + .map(CustomerCouponInfoResponse::from) + .toList(); + return new CustomerCouponFindResponse(cafeInfo, couponInfos); + } + + @Getter + @RequiredArgsConstructor + private static class CafeInfoResponse { + + private final Long id; + private final String name; + private final Boolean isFavorites; + + static CafeInfoResponse from(CustomerCouponFindResultDto.CafeInfoDto cafeInfoDto) { + return new CafeInfoResponse( + cafeInfoDto.getId(), + cafeInfoDto.getName(), + cafeInfoDto.getIsFavorites() + ); + } + } + + @Getter + @RequiredArgsConstructor + private static class CustomerCouponInfoResponse { + + private final Long id; + private final CouponStatus status; + private final Integer stampCount; + private final Integer maxStampCount; + private final String rewardName; + private final String frontImageUrl; + private final String backImageUrl; + private final String stampImageUrl; + private final List coordinates; + + static CustomerCouponInfoResponse from(CustomerCouponFindResultDto.CouponInfoDto couponInfoDto) { + return new CustomerCouponInfoResponse( + couponInfoDto.getId(), + couponInfoDto.getStatus(), + couponInfoDto.getStampCount(), + couponInfoDto.getMaxStampCount(), + couponInfoDto.getRewardName(), + couponInfoDto.getFrontImageUrl(), + couponInfoDto.getBackImageUrl(), + couponInfoDto.getStampImageUrl(), + couponInfoDto.getCoordinates().stream().map(CoordinatesResponse::from).toList() + ); + } + + @RequiredArgsConstructor + private static class CoordinatesResponse { + + private final Integer order; + private final Integer xCoordinate; + private final Integer yCoordinate; + + static CoordinatesResponse from(CustomerCouponFindResultDto.CouponInfoDto.CouponCoordinatesDto couponCoordinatesDto) { + return new CoordinatesResponse( + couponCoordinatesDto.getOrder(), + couponCoordinatesDto.getXCoordinate(), + couponCoordinatesDto.getYCoordinate() + ); + } + + public Integer getOrder() { + return order; + } + + public Integer getxCoordinate() { + return xCoordinate; + } + + public Integer getyCoordinate() { + return yCoordinate; + } + } + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/coupon/response/CustomerCouponsFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/coupon/response/CustomerCouponsFindResponse.java new file mode 100644 index 000000000..049988a62 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/coupon/response/CustomerCouponsFindResponse.java @@ -0,0 +1,21 @@ +package com.stampcrush.backend.api.visitor.coupon.response; + +import com.stampcrush.backend.application.visitor.coupon.dto.CustomerCouponFindResultDto; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class CustomerCouponsFindResponse { + + private final List coupons; + + public static CustomerCouponsFindResponse from(List customerCouponFindResultDtos) { + List coupons = customerCouponFindResultDtos.stream() + .map(CustomerCouponFindResponse::from) + .toList(); + return new CustomerCouponsFindResponse(coupons); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/favorites/VisitorFavoritesCommandApiController.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/favorites/VisitorFavoritesCommandApiController.java new file mode 100644 index 000000000..d5b04ba16 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/favorites/VisitorFavoritesCommandApiController.java @@ -0,0 +1,28 @@ +package com.stampcrush.backend.api.visitor.favorites; + +import com.stampcrush.backend.api.visitor.favorites.request.FavoritesUpdateRequest; +import com.stampcrush.backend.application.visitor.favorites.VisitorFavoritesCommandService; +import com.stampcrush.backend.config.resolver.CustomerAuth; +import com.stampcrush.backend.entity.user.Customer; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api") +public class VisitorFavoritesCommandApiController { + + public final VisitorFavoritesCommandService visitorFavoritesCommandService; + + @PostMapping("/cafes/{cafeId}/favorites") + public ResponseEntity updateFavorites( + CustomerAuth customer, + @PathVariable Long cafeId, + @Valid @RequestBody FavoritesUpdateRequest favoritesUpdateRequest + ) { + visitorFavoritesCommandService.changeFavorites(customer.getId(), cafeId, favoritesUpdateRequest.getIsFavorites()); + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/favorites/request/FavoritesUpdateRequest.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/favorites/request/FavoritesUpdateRequest.java new file mode 100644 index 000000000..ebd29a42f --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/favorites/request/FavoritesUpdateRequest.java @@ -0,0 +1,15 @@ +package com.stampcrush.backend.api.visitor.favorites.request; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class FavoritesUpdateRequest { + + @NotNull + private Boolean isFavorites; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/profile/VisitorProfilesCommandApiController.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/profile/VisitorProfilesCommandApiController.java new file mode 100644 index 000000000..4fa34aa1d --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/profile/VisitorProfilesCommandApiController.java @@ -0,0 +1,30 @@ +package com.stampcrush.backend.api.visitor.profile; + +import com.stampcrush.backend.api.visitor.profile.request.VisitorProfilesPhoneNumberUpdateRequest; +import com.stampcrush.backend.application.visitor.profile.VisitorProfilesCommandService; +import com.stampcrush.backend.config.resolver.CustomerAuth; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/profiles") +public class VisitorProfilesCommandApiController { + + private final VisitorProfilesCommandService visitorProfilesCommandService; + + @PostMapping("/phone-number") + public ResponseEntity savePhoneNumber( + CustomerAuth customer, + @Valid @RequestBody VisitorProfilesPhoneNumberUpdateRequest request + ) { + System.out.println("request.getPhoneNumber() = " + request.getPhoneNumber()); + visitorProfilesCommandService.registerPhoneNumber(customer.getId(), request.getPhoneNumber()); + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/profile/VisitorProfilesFindApiController.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/profile/VisitorProfilesFindApiController.java new file mode 100644 index 000000000..4e5c5b8ba --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/profile/VisitorProfilesFindApiController.java @@ -0,0 +1,26 @@ +package com.stampcrush.backend.api.visitor.profile; + +import com.stampcrush.backend.api.visitor.profile.response.VisitorProfilesFindResponse; +import com.stampcrush.backend.application.visitor.profile.VisitorProfilesFindService; +import com.stampcrush.backend.application.visitor.profile.dto.VisitorProfileFindResultDto; +import com.stampcrush.backend.config.resolver.CustomerAuth; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/profiles") +public class VisitorProfilesFindApiController { + + private final VisitorProfilesFindService visitorProfilesFindService; + + @GetMapping + public ResponseEntity findProfiles(CustomerAuth customer) { + VisitorProfileFindResultDto dto = visitorProfilesFindService.findVisitorProfile(customer.getId()); + + return ResponseEntity.ok().body(VisitorProfilesFindResponse.from(dto)); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/profile/request/VisitorProfilesPhoneNumberUpdateRequest.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/profile/request/VisitorProfilesPhoneNumberUpdateRequest.java new file mode 100644 index 000000000..01632cf98 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/profile/request/VisitorProfilesPhoneNumberUpdateRequest.java @@ -0,0 +1,17 @@ +package com.stampcrush.backend.api.visitor.profile.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class VisitorProfilesPhoneNumberUpdateRequest { + + @NotNull + @NotBlank + private String phoneNumber; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/profile/response/VisitorProfilesFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/profile/response/VisitorProfilesFindResponse.java new file mode 100644 index 000000000..fd4c9c45c --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/profile/response/VisitorProfilesFindResponse.java @@ -0,0 +1,37 @@ +package com.stampcrush.backend.api.visitor.profile.response; + +import com.stampcrush.backend.application.visitor.profile.dto.VisitorProfileFindResultDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class VisitorProfilesFindResponse { + + private VisitorProfileFindResponse profile; + + public static VisitorProfilesFindResponse from(VisitorProfileFindResultDto dto) { + return new VisitorProfilesFindResponse(VisitorProfileFindResponse.from(dto)); + } + + @Getter + @RequiredArgsConstructor + public static class VisitorProfileFindResponse { + + private final Long id; + private final String nickname; + private final String phoneNumber; + private final String email; + + public static VisitorProfilesFindResponse.VisitorProfileFindResponse from(VisitorProfileFindResultDto dto) { + return new VisitorProfilesFindResponse.VisitorProfileFindResponse( + dto.getId(), + dto.getNickname(), + dto.getPhoneNumber(), + dto.getEmail()); + } + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/reward/VisitorRewardsFindController.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/reward/VisitorRewardsFindController.java new file mode 100644 index 000000000..b04d6bc40 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/reward/VisitorRewardsFindController.java @@ -0,0 +1,31 @@ +package com.stampcrush.backend.api.visitor.reward; + +import com.stampcrush.backend.api.visitor.reward.response.VisitorRewardsFindResponse; +import com.stampcrush.backend.application.visitor.reward.VisitorRewardsFindService; +import com.stampcrush.backend.application.visitor.reward.dto.VisitorRewardsFindResultDto; +import com.stampcrush.backend.config.resolver.CustomerAuth; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/rewards") +public class VisitorRewardsFindController { + + private final VisitorRewardsFindService visitorRewardsFindService; + + @GetMapping + public ResponseEntity findRewards( + CustomerAuth customer, + @RequestParam("used") Boolean used + ) { + List result = visitorRewardsFindService.findRewards(customer.getId(), used); + return ResponseEntity.ok().body(VisitorRewardsFindResponse.from(result)); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/reward/response/VisitorRewardsFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/reward/response/VisitorRewardsFindResponse.java new file mode 100644 index 000000000..d28ef9e2e --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/reward/response/VisitorRewardsFindResponse.java @@ -0,0 +1,45 @@ +package com.stampcrush.backend.api.visitor.reward.response; + +import com.stampcrush.backend.application.visitor.reward.dto.VisitorRewardsFindResultDto; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class VisitorRewardsFindResponse { + + private final List rewards; + + public static VisitorRewardsFindResponse from(List dtos) { + return new VisitorRewardsFindResponse( + dtos.stream() + .map(VisitorRewardFindResponse::from) + .toList() + ); + } + + @Getter + @RequiredArgsConstructor + public static class VisitorRewardFindResponse { + + private final Long id; + private final String rewardName; + private final String cafeName; + private final String createdAt; + private final String usedAt; + + public static VisitorRewardFindResponse from(VisitorRewardsFindResultDto dto) { + return new VisitorRewardFindResponse( + dto.getId(), + dto.getRewardName(), + dto.getCafeName(), + LocalDate.from(dto.getCreatedAt()).format(DateTimeFormatter.ofPattern("yyyy:MM:dd")), + LocalDate.from(dto.getUpdatedAt()).format(DateTimeFormatter.ofPattern("yyyy:MM:dd")) + ); + } + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/visithistory/VisitorVisitHistoryFindApiController.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/visithistory/VisitorVisitHistoryFindApiController.java new file mode 100644 index 000000000..a46cfed3f --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/visithistory/VisitorVisitHistoryFindApiController.java @@ -0,0 +1,33 @@ +package com.stampcrush.backend.api.visitor.visithistory; + + +import com.stampcrush.backend.api.visitor.visithistory.response.CustomerStampHistoriesFindResponse; +import com.stampcrush.backend.api.visitor.visithistory.response.CustomerStampHistoryFindResponse; +import com.stampcrush.backend.application.visitor.visithistory.VisitorVisitHistoryFindService; +import com.stampcrush.backend.application.visitor.visithistory.dto.CustomerStampHistoryFindResultDto; +import com.stampcrush.backend.config.resolver.CustomerAuth; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/api") +@RestController +public class VisitorVisitHistoryFindApiController { + + private final VisitorVisitHistoryFindService visitorVisitHistoryFindService; + + @GetMapping("/stamp-histories") + public ResponseEntity findStampHistoriesByCustomer(CustomerAuth customer) { + List stampHistories = visitorVisitHistoryFindService.findStampHistoriesByCustomer(customer.getId()); + List stampHistoryFindResponses = stampHistories.stream() + .map(CustomerStampHistoryFindResponse::from) + .toList(); + + return ResponseEntity.ok(new CustomerStampHistoriesFindResponse(stampHistoryFindResponses)); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/visithistory/response/CustomerStampHistoriesFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/visithistory/response/CustomerStampHistoriesFindResponse.java new file mode 100644 index 000000000..6942aa3d9 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/visithistory/response/CustomerStampHistoriesFindResponse.java @@ -0,0 +1,17 @@ +package com.stampcrush.backend.api.visitor.visithistory.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +import static lombok.AccessLevel.PROTECTED; + +@AllArgsConstructor +@NoArgsConstructor(access = PROTECTED) +@Getter +public class CustomerStampHistoriesFindResponse { + + private List stampHistories; +} diff --git a/backend/src/main/java/com/stampcrush/backend/api/visitor/visithistory/response/CustomerStampHistoryFindResponse.java b/backend/src/main/java/com/stampcrush/backend/api/visitor/visithistory/response/CustomerStampHistoryFindResponse.java new file mode 100644 index 000000000..d0c0e8b36 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/api/visitor/visithistory/response/CustomerStampHistoryFindResponse.java @@ -0,0 +1,30 @@ +package com.stampcrush.backend.api.visitor.visithistory.response; + +import com.stampcrush.backend.application.visitor.visithistory.dto.CustomerStampHistoryFindResultDto; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.format.DateTimeFormatter; + +import static lombok.AccessLevel.PROTECTED; + +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = PROTECTED) +public class CustomerStampHistoryFindResponse { + + private Long id; + private String cafeName; + private int stampCount; + private String createdAt; + + public static CustomerStampHistoryFindResponse from(CustomerStampHistoryFindResultDto serviceDto) { + return new CustomerStampHistoryFindResponse( + serviceDto.getId(), + serviceDto.getCafeName(), + serviceDto.getStampCount(), + serviceDto.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy:MM:dd hh:mm:ss")) + ); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCommandService.java b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCommandService.java new file mode 100644 index 000000000..082d83bed --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCommandService.java @@ -0,0 +1,113 @@ +package com.stampcrush.backend.application.manager.cafe; + +import com.stampcrush.backend.application.manager.cafe.dto.CafeCreateDto; +import com.stampcrush.backend.application.manager.cafe.dto.CafeUpdateDto; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.cafe.CafeCouponDesign; +import com.stampcrush.backend.entity.cafe.CafePolicy; +import com.stampcrush.backend.entity.sample.SampleBackImage; +import com.stampcrush.backend.entity.sample.SampleFrontImage; +import com.stampcrush.backend.entity.sample.SampleStampCoordinate; +import com.stampcrush.backend.entity.sample.SampleStampImage; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.exception.CafeNotFoundException; +import com.stampcrush.backend.exception.OwnerNotFoundException; +import com.stampcrush.backend.repository.cafe.CafeCouponDesignRepository; +import com.stampcrush.backend.repository.cafe.CafePolicyRepository; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.cafe.CafeStampCoordinateRepository; +import com.stampcrush.backend.repository.sample.SampleBackImageRepository; +import com.stampcrush.backend.repository.sample.SampleFrontImageRepository; +import com.stampcrush.backend.repository.sample.SampleStampCoordinateRepository; +import com.stampcrush.backend.repository.sample.SampleStampImageRepository; +import com.stampcrush.backend.repository.user.OwnerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.stampcrush.backend.application.manager.cafe.SampleCouponImage.*; + +@RequiredArgsConstructor +@Transactional +@Service +public class ManagerCafeCommandService { + + private final CafeRepository cafeRepository; + private final OwnerRepository ownerRepository; + private final SampleFrontImageRepository sampleFrontImageRepository; + private final SampleBackImageRepository sampleBackImageRepository; + private final SampleStampImageRepository sampleStampImageRepository; + private final SampleStampCoordinateRepository sampleStampCoordinateRepository; + private final CafePolicyRepository cafePolicyRepository; + private final CafeCouponDesignRepository cafeCouponDesignRepository; + private final CafeStampCoordinateRepository cafeStampCoordinateRepository; + + public Long createCafe(CafeCreateDto cafeCreateDto) { + Owner owner = findExistingOwnerById(cafeCreateDto.getOwnerId()); + Cafe savedCafe = saveCafe(cafeCreateDto, owner); + assignDefaultCafePolicyToCafe(savedCafe); + assignDefaultSampleCafeCouponToCafe(savedCafe); + + return savedCafe.getId(); + } + + private Owner findExistingOwnerById(Long ownerId) { + return ownerRepository.findById(ownerId) + .orElseThrow(() -> new OwnerNotFoundException("회원가입을 먼저 진행해주세요.")); + } + + private Cafe saveCafe(CafeCreateDto cafeCreateDto, Owner owner) { + return cafeRepository.save( + new Cafe( + cafeCreateDto.getName(), + cafeCreateDto.getRoadAddress(), + cafeCreateDto.getDetailAddress(), + cafeCreateDto.getBusinessRegistrationNumber(), + owner + ) + ); + } + + private void assignDefaultCafePolicyToCafe(Cafe savedCafe) { + CafePolicy defaultCafePolicy = CafePolicy.createDefaultCafePolicy(savedCafe); + cafePolicyRepository.save(defaultCafePolicy); + } + + private void assignDefaultSampleCafeCouponToCafe(Cafe savedCafe) { + SampleFrontImage defaultSampleFrontImage = sampleFrontImageRepository.save(SAMPLE_FRONT_IMAGE); + SampleBackImage defaultSampleBackImage = sampleBackImageRepository.save(SAMPLE_BACK_IMAGE); + + sampleStampCoordinateRepository.save(new SampleStampCoordinate(1, 37, 50, defaultSampleBackImage)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(2, 86, 50, defaultSampleBackImage)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(3, 134, 50, defaultSampleBackImage)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(4, 182, 50, defaultSampleBackImage)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(5, 233, 50, defaultSampleBackImage)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(6, 37, 100, defaultSampleBackImage)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(7, 86, 100, defaultSampleBackImage)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(8, 134, 100, defaultSampleBackImage)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(9, 182, 100, defaultSampleBackImage)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(10, 233, 100, defaultSampleBackImage)); + + SampleStampImage defaultSampleStampImage = sampleStampImageRepository.save(SAMPLE_STAMP_IMAGE); + CafeCouponDesign defaultCafeCouponDesign = new CafeCouponDesign( + defaultSampleFrontImage.getImageUrl(), + defaultSampleBackImage.getImageUrl(), + defaultSampleStampImage.getImageUrl(), + false, + savedCafe + ); + cafeCouponDesignRepository.save(defaultCafeCouponDesign); + } + + public void updateCafeInfo(CafeUpdateDto cafeUpdateDto, Long cafeId) { + Cafe cafe = cafeRepository.findById(cafeId) + .orElseThrow(() -> new CafeNotFoundException("존재하지 않는 카페 입니다.")); + cafe.updateCafeAdditionalInformation( + cafeUpdateDto.getIntroduction(), + cafeUpdateDto.getOpenTime(), + cafeUpdateDto.getCloseTime(), + cafeUpdateDto.getTelephoneNumber(), + cafeUpdateDto.getCafeImageUrl() + ); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCouponSettingCommandService.java b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCouponSettingCommandService.java new file mode 100644 index 000000000..e4236b7d3 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCouponSettingCommandService.java @@ -0,0 +1,104 @@ +package com.stampcrush.backend.application.manager.cafe; + +import com.stampcrush.backend.application.manager.cafe.dto.CafeCouponSettingDto; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.cafe.CafeCouponDesign; +import com.stampcrush.backend.entity.cafe.CafePolicy; +import com.stampcrush.backend.entity.cafe.CafeStampCoordinate; +import com.stampcrush.backend.exception.CafeNotFoundException; +import com.stampcrush.backend.repository.cafe.CafeCouponDesignRepository; +import com.stampcrush.backend.repository.cafe.CafePolicyRepository; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.cafe.CafeStampCoordinateRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +@Transactional +@Service +public class ManagerCafeCouponSettingCommandService { + + private final CafeRepository cafeRepository; + private final CafeCouponDesignRepository cafeCouponDesignRepository; + private final CafePolicyRepository cafePolicyRepository; + private final CafeStampCoordinateRepository cafeStampCoordinateRepository; + + public void updateCafeCouponSetting(Long cafeId, CafeCouponSettingDto cafeCouponSettingDto) { + Cafe cafe = findExistingCafe(cafeId); + deletePreviousSetting(cafe); + createNewSetting(cafe, cafeCouponSettingDto); + } + + private Cafe findExistingCafe(Long cafeId) { + Optional findCafe = cafeRepository.findById(cafeId); + + if (findCafe.isEmpty()) { + throw new CafeNotFoundException("존재하지 않는 카페입니다."); + } + + return findCafe.get(); + } + + private void deletePreviousSetting(Cafe cafe) { + deleteCafePolicy(cafe); + deleteCafeCouponDesign(cafe); + } + + private void deleteCafePolicy(Cafe cafe) { + Optional findCafePolicy = cafePolicyRepository.findByCafe(cafe); + if (findCafePolicy.isPresent()) { + CafePolicy currentCafePolicy = findCafePolicy.get(); + cafePolicyRepository.delete(currentCafePolicy); + } + } + + private void deleteCafeCouponDesign(Cafe cafe) { + Optional findCafeCouponDesign = cafeCouponDesignRepository.findByCafe(cafe); + if (findCafeCouponDesign.isPresent()) { + CafeCouponDesign currentCafeCouponDesign = findCafeCouponDesign.get(); + cafeCouponDesignRepository.delete(currentCafeCouponDesign); + } + } + + private void createNewSetting(Cafe cafe, CafeCouponSettingDto cafeCouponSettingDto) { + createNewCafePolicy(cafe, cafeCouponSettingDto); + createNewCafeCouponDesign(cafe, cafeCouponSettingDto); + } + + private void createNewCafePolicy(Cafe cafe, CafeCouponSettingDto cafeCouponSettingDto) { + CafeCouponSettingDto.CafePolicyDto cafePolicyDto = cafeCouponSettingDto.getCafePolicyDto(); + CafePolicy newCafePolicy = new CafePolicy( + cafePolicyDto.getMaxStampCount(), + cafePolicyDto.getReward(), + cafePolicyDto.getExpirePeriod(), + false, + cafe + ); + CafePolicy savedCafePolicy = cafePolicyRepository.save(newCafePolicy); + } + + private void createNewCafeCouponDesign(Cafe cafe, CafeCouponSettingDto cafeCouponSettingDto) { + CafeCouponSettingDto.CafeCouponDesignDto cafeCouponDesignDto = cafeCouponSettingDto.getCafeCouponDesignDto(); + CafeCouponDesign newCafeCouponDesign = new CafeCouponDesign( + cafeCouponDesignDto.getFrontImageUrl(), + cafeCouponDesignDto.getBackImageUrl(), + cafeCouponDesignDto.getStampImageUrl(), + false, + cafe + ); + CafeCouponDesign savedCafeCouponDesign = cafeCouponDesignRepository.save(newCafeCouponDesign); + for (CafeCouponSettingDto.CafeCouponDesignDto.CafeStampCoordinateDto coordinate : cafeCouponDesignDto.getCoordinates()) { + CafeStampCoordinate newCafeStampCoordinate = new CafeStampCoordinate( + coordinate.getOrder(), + coordinate.getXCoordinate(), + coordinate.getYCoordinate(), + savedCafeCouponDesign + ); + CafeStampCoordinate savedCafeStampCoordinate = cafeStampCoordinateRepository.save(newCafeStampCoordinate); + } + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCouponSettingFindService.java b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCouponSettingFindService.java new file mode 100644 index 000000000..c3a155589 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCouponSettingFindService.java @@ -0,0 +1,54 @@ +package com.stampcrush.backend.application.manager.cafe; + +import com.stampcrush.backend.application.manager.cafe.dto.CafeCouponCoordinateFindResultDto; +import com.stampcrush.backend.application.manager.cafe.dto.CafeCouponSettingFindResultDto; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.cafe.CafeCouponDesign; +import com.stampcrush.backend.entity.coupon.Coupon; +import com.stampcrush.backend.entity.coupon.CouponDesign; +import com.stampcrush.backend.exception.CafeCouponSettingNotFoundException; +import com.stampcrush.backend.exception.CafeNotFoundException; +import com.stampcrush.backend.exception.CouponNotFoundException; +import com.stampcrush.backend.repository.cafe.CafeCouponDesignRepository; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.coupon.CouponRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Slf4j +@Service +public class ManagerCafeCouponSettingFindService { + + private final CafeRepository cafeRepository; + private final CouponRepository couponRepository; + private final CafeCouponDesignRepository cafeCouponDesignRepository; + + public CafeCouponSettingFindResultDto findCafeCouponSetting(Long cafeId) { + Cafe cafe = cafeRepository.findById(cafeId) + .orElseThrow(() -> new CafeNotFoundException("카페를 찾지 못했습니다.")); + CafeCouponDesign cafeCouponDesign = cafeCouponDesignRepository.findByCafe(cafe) + .orElseThrow(() -> new CafeCouponSettingNotFoundException("카페 디자인을 찾을 수 없습니다.")); + return new CafeCouponSettingFindResultDto( + cafeCouponDesign.getFrontImageUrl(), + cafeCouponDesign.getBackImageUrl(), + cafeCouponDesign.getStampImageUrl(), + cafeCouponDesign.getCafeStampCoordinates().stream() + .map(stamp -> new CafeCouponCoordinateFindResultDto( + stamp.getStampOrder(), + stamp.getXCoordinate(), + stamp.getYCoordinate())).toList()); + } + + public CafeCouponSettingFindResultDto findCouponSetting(Long cafeId, Long couponId) { + Cafe cafe = cafeRepository.findById(cafeId) + .orElseThrow(() -> new CafeNotFoundException("카페를 찾지 못했습니다.")); + Coupon coupon = couponRepository.findById(couponId) + .orElseThrow(() -> new CouponNotFoundException("couponId: " + couponId + " 쿠폰을 찾지 못했습니다.")); + CouponDesign couponDesign = coupon.getCouponDesign(); + return CafeCouponSettingFindResultDto.from(couponDesign); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeFindService.java b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeFindService.java new file mode 100644 index 000000000..e7af88493 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeFindService.java @@ -0,0 +1,25 @@ +package com.stampcrush.backend.application.manager.cafe; + +import com.stampcrush.backend.application.manager.cafe.dto.CafeFindResultDto; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class ManagerCafeFindService { + + private final CafeRepository cafeRepository; + + public List findCafesByOwner(Long ownerId) { + List cafes = cafeRepository.findAllByOwnerId(ownerId); + return cafes.stream() + .map(CafeFindResultDto::from) + .toList(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/SampleCouponImage.java b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/SampleCouponImage.java new file mode 100644 index 000000000..006523e90 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/SampleCouponImage.java @@ -0,0 +1,12 @@ +package com.stampcrush.backend.application.manager.cafe; + +import com.stampcrush.backend.entity.sample.SampleBackImage; +import com.stampcrush.backend.entity.sample.SampleFrontImage; +import com.stampcrush.backend.entity.sample.SampleStampImage; + +public class SampleCouponImage { + + public static final SampleFrontImage SAMPLE_FRONT_IMAGE = new SampleFrontImage("https://drive.google.com/uc?export=view&id=1J6HcagcK65D6_i0bDQ7llbvdCnCOkJ7h"); + public static final SampleBackImage SAMPLE_BACK_IMAGE = new SampleBackImage("https://drive.google.com/uc?export=view&id=1fhqxcrq31yP8Th8GmdscMBXtVFKuhHK0"); + public static final SampleStampImage SAMPLE_STAMP_IMAGE = new SampleStampImage("https://drive.google.com/uc?export=view&id=1KVBztQdUCpvp8usHUbIbSBYvQManm6eN"); +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeCouponCoordinateFindResultDto.java b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeCouponCoordinateFindResultDto.java new file mode 100644 index 000000000..2ffca0d20 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeCouponCoordinateFindResultDto.java @@ -0,0 +1,13 @@ +package com.stampcrush.backend.application.manager.cafe.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class CafeCouponCoordinateFindResultDto { + + private final Integer order; + private final Integer xCoordinate; + private final Integer yCoordinate; +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeCouponSettingDto.java b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeCouponSettingDto.java new file mode 100644 index 000000000..7444f969b --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeCouponSettingDto.java @@ -0,0 +1,83 @@ +package com.stampcrush.backend.application.manager.cafe.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +import java.util.List; + +@Getter +@ToString +@RequiredArgsConstructor +public class CafeCouponSettingDto { + + private final CafeCouponDesignDto cafeCouponDesignDto; + private final CafePolicyDto cafePolicyDto; + + public static CafeCouponSettingDto of( + String frontImageUrl, + String backImageUrl, + String stampImageUrl, + List> coordinates, + Integer maxStampCount, + String reward, + Integer expirePeriod + ) { + CafeCouponDesignDto cafeCouponDesignDto = CafeCouponDesignDto.of( + frontImageUrl, + backImageUrl, + stampImageUrl, + coordinates + ); + CafePolicyDto cafePolicyDto = CafePolicyDto.of( + maxStampCount, + reward, + expirePeriod + ); + return new CafeCouponSettingDto(cafeCouponDesignDto, cafePolicyDto); + } + + @Getter + @ToString + @RequiredArgsConstructor + public static class CafeCouponDesignDto { + + private final String frontImageUrl; + private final String backImageUrl; + private final String stampImageUrl; + private final List coordinates; + + public static CafeCouponDesignDto of(String frontImageUrl, String backImageUrl, String stampImageUrl, List> coordinates) { + return new CafeCouponDesignDto( + frontImageUrl, + backImageUrl, + stampImageUrl, + coordinates.stream().map(it -> new CafeStampCoordinateDto(it.get(0), it.get(1), it.get(2))).toList() + ); + } + + @Getter + @ToString + @RequiredArgsConstructor + public static class CafeStampCoordinateDto { + + private final Integer order; + private final Integer xCoordinate; + private final Integer yCoordinate; + } + } + + @Getter + @ToString + @RequiredArgsConstructor + public static class CafePolicyDto { + + private final Integer maxStampCount; + private final String reward; + private final Integer expirePeriod; + + public static CafePolicyDto of(Integer maxStampCount, String reward, Integer expirePeriod) { + return new CafePolicyDto(maxStampCount, reward, expirePeriod); + } + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeCouponSettingFindResultDto.java b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeCouponSettingFindResultDto.java new file mode 100644 index 000000000..a42709a3e --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeCouponSettingFindResultDto.java @@ -0,0 +1,30 @@ +package com.stampcrush.backend.application.manager.cafe.dto; + +import com.stampcrush.backend.entity.coupon.CouponDesign; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class CafeCouponSettingFindResultDto { + + private final String frontImageUrl; + private final String backImageUrl; + private final String stampImageUrl; + private final List coordinates; + + public static CafeCouponSettingFindResultDto from(CouponDesign couponDesign) { + return new CafeCouponSettingFindResultDto( + couponDesign.getFrontImageUrl(), + couponDesign.getBackImageUrl(), + couponDesign.getStampImageUrl(), + couponDesign.getCouponStampCoordinates().stream() + .map(stamp -> new CafeCouponCoordinateFindResultDto( + stamp.getStampOrder(), + stamp.getXCoordinate(), + stamp.getYCoordinate())).toList() + ); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeCreateDto.java b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeCreateDto.java new file mode 100644 index 000000000..c8eb3f882 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeCreateDto.java @@ -0,0 +1,19 @@ +package com.stampcrush.backend.application.manager.cafe.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class CafeCreateDto { + + private final Long ownerId; + + private final String name; + + private final String roadAddress; + + private final String detailAddress; + + private final String businessRegistrationNumber; +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeFindResultDto.java b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeFindResultDto.java new file mode 100644 index 000000000..72cc0c810 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeFindResultDto.java @@ -0,0 +1,44 @@ +package com.stampcrush.backend.application.manager.cafe.dto; + +import com.stampcrush.backend.entity.cafe.Cafe; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalTime; + +@Getter +@RequiredArgsConstructor +public class CafeFindResultDto { + + private final Long id; + private final String name; + + @DateTimeFormat(pattern = "HH:mm") + private final LocalTime openTime; + + @DateTimeFormat(pattern = "HH:mm") + private final LocalTime closeTime; + + private final String telephoneNumber; + private final String cafeImageUrl; + private final String roadAddress; + private final String detailAddress; + private final String businessRegistrationNumber; + private final String introduction; + + public static CafeFindResultDto from(Cafe cafe) { + return new CafeFindResultDto( + cafe.getId(), + cafe.getName(), + cafe.getOpenTime(), + cafe.getCloseTime(), + cafe.getTelephoneNumber(), + cafe.getCafeImageUrl(), + cafe.getRoadAddress(), + cafe.getDetailAddress(), + cafe.getBusinessRegistrationNumber(), + cafe.getIntroduction() + ); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeUpdateDto.java b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeUpdateDto.java new file mode 100644 index 000000000..7a38d26f4 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/cafe/dto/CafeUpdateDto.java @@ -0,0 +1,18 @@ +package com.stampcrush.backend.application.manager.cafe.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalTime; + + +@Getter +@AllArgsConstructor +public class CafeUpdateDto { + + private final String introduction; + private final LocalTime openTime; + private final LocalTime closeTime; + private final String telephoneNumber; + private final String cafeImageUrl; +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/CustomerCouponStatistics.java b/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/CustomerCouponStatistics.java new file mode 100644 index 000000000..60daf0937 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/CustomerCouponStatistics.java @@ -0,0 +1,13 @@ +package com.stampcrush.backend.application.manager.coupon; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class CustomerCouponStatistics { + + private final int stampCount; + private final int rewardCount; + private final int maxStampCount; +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/ManagerCouponCommandService.java b/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/ManagerCouponCommandService.java new file mode 100644 index 000000000..0c2c2ebf6 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/ManagerCouponCommandService.java @@ -0,0 +1,182 @@ +package com.stampcrush.backend.application.manager.coupon; + +import com.stampcrush.backend.application.manager.coupon.dto.StampCreateDto; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.cafe.CafeCouponDesign; +import com.stampcrush.backend.entity.cafe.CafePolicy; +import com.stampcrush.backend.entity.coupon.Coupon; +import com.stampcrush.backend.entity.coupon.CouponDesign; +import com.stampcrush.backend.entity.coupon.CouponPolicy; +import com.stampcrush.backend.entity.coupon.CouponStatus; +import com.stampcrush.backend.entity.reward.Reward; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.visithistory.VisitHistory; +import com.stampcrush.backend.exception.CafeNotFoundException; +import com.stampcrush.backend.exception.CustomerNotFoundException; +import com.stampcrush.backend.repository.cafe.CafeCouponDesignRepository; +import com.stampcrush.backend.repository.cafe.CafePolicyRepository; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.coupon.CouponDesignRepository; +import com.stampcrush.backend.repository.coupon.CouponPolicyRepository; +import com.stampcrush.backend.repository.coupon.CouponRepository; +import com.stampcrush.backend.repository.reward.RewardRepository; +import com.stampcrush.backend.repository.user.CustomerRepository; +import com.stampcrush.backend.repository.user.OwnerRepository; +import com.stampcrush.backend.repository.visithistory.VisitHistoryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; + +@RequiredArgsConstructor +@Transactional +@Service +public class ManagerCouponCommandService { + + private final CouponRepository couponRepository; + private final CafeRepository cafeRepository; + private final CustomerRepository customerRepository; + private final CafeCouponDesignRepository cafeCouponDesignRepository; + private final CafePolicyRepository cafePolicyRepository; + private final CouponDesignRepository couponDesignRepository; + private final CouponPolicyRepository couponPolicyRepository; + private final OwnerRepository ownerRepository; + private final RewardRepository rewardRepository; + private final VisitHistoryRepository visitHistoryRepository; + + private record CustomerCoupons(Customer customer, List coupons) { + } + + public Long createCoupon(Long cafeId, Long customerId) { + Customer customer = findCustomerById(customerId); + Cafe cafe = findCafeById(cafeId); + CafePolicy cafePolicy = findCafePolicy(cafe); + CafeCouponDesign cafeCouponDesign = findCafeCouponDesign(cafe); + List existCoupons = couponRepository.findByCafeAndCustomerAndStatus(cafe, customer, CouponStatus.ACCUMULATING); + if (!existCoupons.isEmpty()) { + for (Coupon coupon : existCoupons) { + coupon.expire(); + } + } + + Coupon coupon = issueCoupon(customer, cafe, cafePolicy, cafeCouponDesign); + couponRepository.save(coupon); + return coupon.getId(); + } + + private Customer findCustomerById(Long customerId) { + return customerRepository.findById(customerId) + .orElseThrow(() -> new CustomerNotFoundException("존재하지 않는 회원입니다.")); + } + + private Cafe findCafeById(Long cafeId) { + return cafeRepository.findById(cafeId) + .orElseThrow(() -> new CafeNotFoundException("존재하지 않는 카페입니다.")); + } + + private Coupon issueCoupon(Customer customer, Cafe cafe, CafePolicy cafePolicy, CafeCouponDesign cafeCouponDesign) { + CouponDesign couponDesign = cafeCouponDesign.copy(); + couponDesignRepository.save(couponDesign); + CouponPolicy couponPolicy = cafePolicy.copy(); + couponPolicyRepository.save(couponPolicy); + + LocalDate expiredDate = LocalDate.now().plusMonths(couponPolicy.getExpiredPeriod()); + + return new Coupon(expiredDate, customer, cafe, couponDesign, couponPolicy); + } + + public void createStamp(StampCreateDto stampCreateDto) { + Owner owner = findOwner(stampCreateDto); + Customer customer = findCustomer(stampCreateDto); + Cafe cafe = findCafe(owner); + CafePolicy cafePolicy = findCafePolicy(cafe); + CafeCouponDesign cafeCouponDesign = findCafeCouponDesign(cafe); + Coupon coupon = findCoupon(stampCreateDto, customer, cafe); + int earningStampCount = stampCreateDto.getEarningStampCount(); + + VisitHistory visitHistory = new VisitHistory(cafe, customer, earningStampCount); + visitHistoryRepository.save(visitHistory); + if (coupon.isLessThanMaxStampAfterAccumulateStamp(earningStampCount)) { + coupon.accumulate(earningStampCount); + return; + } + if (coupon.isSameMaxStampAfterAccumulateStamp(earningStampCount)) { + accumulateMaxStampAndMakeReward(customer, cafe, coupon, earningStampCount); + return; + } + int restStamp = earningStampCount; + int restStampCountForReward = coupon.calculateRestStampCountForReward(); + accumulateMaxStampAndMakeReward(customer, cafe, coupon, restStampCountForReward); + + restStamp -= restStampCountForReward; + makeRewardCoupons(customer, cafe, cafePolicy, cafeCouponDesign, coupon, restStamp); + issueAccumulatingCoupon(customer, cafe, cafePolicy, cafeCouponDesign, restStamp); + } + + private CafeCouponDesign findCafeCouponDesign(Cafe cafe) { + return cafeCouponDesignRepository.findByCafe(cafe) + .orElseThrow(IllegalArgumentException::new); + } + + private CafePolicy findCafePolicy(Cafe cafe) { + return cafePolicyRepository.findByCafe(cafe) + .orElseThrow(IllegalArgumentException::new); + } + + private Customer findCustomer(StampCreateDto stampCreateDto) { + return customerRepository.findById(stampCreateDto.getCustomerId()) + .orElseThrow(IllegalArgumentException::new); + } + + private Owner findOwner(StampCreateDto stampCreateDto) { + return ownerRepository.findById(stampCreateDto.getOwnerId()) + .orElseThrow(IllegalArgumentException::new); + } + + private Cafe findCafe(Owner owner) { + List cafes = cafeRepository.findAllByOwner(owner); + if (cafes.isEmpty()) { + throw new CafeNotFoundException("존재하지 않는 카페입니다."); + } + return cafes.stream() + .findAny() + .get(); + } + + private Coupon findCoupon(StampCreateDto stampCreateDto, Customer customer, Cafe cafe) { + Coupon coupon = couponRepository.findById(stampCreateDto.getCouponId()) + .orElseThrow(IllegalArgumentException::new); + if (coupon.isNotAccessible(customer, cafe)) { + throw new IllegalArgumentException(); + } + return coupon; + } + + private void accumulateMaxStampAndMakeReward(Customer customer, Cafe cafe, Coupon coupon, int earningStampCount) { + coupon.accumulate(earningStampCount); + rewardRepository.save(new Reward(coupon.getRewardName(), customer, cafe)); + } + + private void makeRewardCoupons(Customer customer, Cafe cafe, CafePolicy cafePolicy, CafeCouponDesign cafeCouponDesign, Coupon coupon, int restStamp) { + int rewardCouponCount = cafePolicy.calculateRewardCouponCount(restStamp); + for (int i = 0; i < rewardCouponCount; i++) { + Coupon rewardCoupon = issueCoupon(customer, cafe, cafePolicy, cafeCouponDesign); + rewardCoupon.accumulateMaxStamp(); + couponRepository.save(rewardCoupon); + rewardRepository.save(new Reward(coupon.getRewardName(), customer, cafe)); + } + } + + private void issueAccumulatingCoupon(Customer customer, Cafe cafe, CafePolicy cafePolicy, CafeCouponDesign cafeCouponDesign, int earningStampCount) { + int accumulatingStampCount = earningStampCount % cafePolicy.getMaxStampCount(); + if (accumulatingStampCount == 0) { + return; + } + Coupon accumulatingCoupon = issueCoupon(customer, cafe, cafePolicy, cafeCouponDesign); + couponRepository.save(accumulatingCoupon); + accumulatingCoupon.accumulate(accumulatingStampCount); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/ManagerCouponFindService.java b/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/ManagerCouponFindService.java new file mode 100644 index 000000000..70d09ccd3 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/ManagerCouponFindService.java @@ -0,0 +1,113 @@ +package com.stampcrush.backend.application.manager.coupon; + +import com.stampcrush.backend.application.manager.coupon.dto.CafeCustomerFindResultDto; +import com.stampcrush.backend.application.manager.coupon.dto.CustomerAccumulatingCouponFindResultDto; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.coupon.Coupon; +import com.stampcrush.backend.entity.coupon.CouponStatus; +import com.stampcrush.backend.entity.coupon.Coupons; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.visithistory.VisitHistories; +import com.stampcrush.backend.entity.visithistory.VisitHistory; +import com.stampcrush.backend.exception.CafeNotFoundException; +import com.stampcrush.backend.exception.CustomerNotFoundException; +import com.stampcrush.backend.repository.cafe.CafePolicyRepository; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.coupon.CouponRepository; +import com.stampcrush.backend.repository.reward.RewardRepository; +import com.stampcrush.backend.repository.user.CustomerRepository; +import com.stampcrush.backend.repository.visithistory.VisitHistoryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static java.util.stream.Collectors.groupingBy; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class ManagerCouponFindService { + + private final CouponRepository couponRepository; + private final CafeRepository cafeRepository; + private final CustomerRepository customerRepository; + private final CafePolicyRepository cafePolicyRepository; + private final VisitHistoryRepository visitHistoryRepository; + private final RewardRepository rewardRepository; + + public List findCouponsByCafe(Long cafeId) { + Cafe cafe = findExistingCafe(cafeId); + + List customerCoupons = findCouponsGroupedByCustomers(cafe); + + List cafeCustomerFindResultDtos = new ArrayList<>(); + for (CustomerCoupons customerCoupon : customerCoupons) { + Coupons coupons = new Coupons(customerCoupon.coupons); + VisitHistories visitHistories = findVisitHistories(cafe, customerCoupon); + // TODO: CustomerCouponStatistics 없애도 될 것 같다. + CustomerCouponStatistics customerCouponStatistics = coupons.calculateStatistics(); + cafeCustomerFindResultDtos.add( + new CafeCustomerFindResultDto( + customerCoupon.customer.getId(), + customerCoupon.customer.getNickname(), + customerCouponStatistics.getStampCount(), + (int) countUnusedRewards(cafe, customerCoupon.customer), + // TODO: visitCount()는 select count(*)로, first visit date는 min(created_at)으로 가져오는게 더 효율적이지 않을까? + visitHistories.getVisitCount(), + visitHistories.getFirstVisitDate(), + customerCoupon.customer.isRegistered(), + customerCouponStatistics.getMaxStampCount() + ) + ); + } + + return cafeCustomerFindResultDtos; + } + + private Cafe findExistingCafe(Long cafeId) { + return cafeRepository.findById(cafeId) + .orElseThrow(() -> new CafeNotFoundException("존재하지 않는 카페 입니다.")); + } + + private List findCouponsGroupedByCustomers(Cafe cafe) { + List coupons = couponRepository.findByCafe(cafe); + Map> customerCouponMap = coupons.stream() + .collect(groupingBy(Coupon::getCustomer)); + return customerCouponMap.keySet().stream() + .map(iter -> new CustomerCoupons(iter, customerCouponMap.get(iter))) + .toList(); + } + + private VisitHistories findVisitHistories(Cafe cafe, CustomerCoupons customerCoupon) { + List visitHistories = visitHistoryRepository.findByCafeAndCustomer(cafe, customerCoupon.customer); + + return new VisitHistories(visitHistories); + } + + public List findAccumulatingCoupon(Long cafeId, Long customerId) { + Cafe cafe = cafeRepository.findById(cafeId).orElseThrow(() -> new CustomerNotFoundException("존재하지 않는 카페 입니다.")); + Customer customer = customerRepository.findById(customerId).orElseThrow(() -> new CustomerNotFoundException("존재하지 않는 고객 입니다.")); + + List coupons = couponRepository.findByCafeAndCustomerAndStatus(cafe, customer, CouponStatus.ACCUMULATING); + + return coupons.stream() + .map(coupon -> CustomerAccumulatingCouponFindResultDto.of( + coupon, + customer, + coupon.isPrevious() + ) + ) + .toList(); + } + + private long countUnusedRewards(Cafe cafe, Customer customer) { + return rewardRepository.countByCafeAndCustomerAndUsed(cafe, customer, Boolean.FALSE); + } + + private record CustomerCoupons(Customer customer, List coupons) { + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/dto/CafeCustomerFindResultDto.java b/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/dto/CafeCustomerFindResultDto.java new file mode 100644 index 000000000..4a4b3eb15 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/dto/CafeCustomerFindResultDto.java @@ -0,0 +1,38 @@ +package com.stampcrush.backend.application.manager.coupon.dto; + +import com.stampcrush.backend.application.manager.coupon.CustomerCouponStatistics; +import com.stampcrush.backend.entity.user.Customer; +import lombok.*; + +import java.time.LocalDateTime; + +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@ToString +@Getter +public class CafeCustomerFindResultDto { + + private Long id; + private String nickname; + private int stampCount; + private int rewardCount; + private int visitCount; + private LocalDateTime firstVisitDate; + private boolean isRegistered; + private int maxStampCount; + + public static CafeCustomerFindResultDto of(Customer customer, CustomerCouponStatistics customerCouponStatistics, + int visitCount, LocalDateTime firstVisitDate) { + return new CafeCustomerFindResultDto( + customer.getId(), + customer.getNickname(), + customerCouponStatistics.getStampCount(), + customerCouponStatistics.getRewardCount(), + visitCount, + firstVisitDate, + customer.isRegistered(), + customerCouponStatistics.getMaxStampCount() + ); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/dto/CafeCustomersFindResultDto.java b/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/dto/CafeCustomersFindResultDto.java new file mode 100644 index 000000000..80232fa04 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/dto/CafeCustomersFindResultDto.java @@ -0,0 +1,15 @@ +package com.stampcrush.backend.application.manager.coupon.dto; + +import lombok.Getter; + +import java.util.List; + +@Getter +public class CafeCustomersFindResultDto { + + private final List customers; + + public CafeCustomersFindResultDto(List customers) { + this.customers = customers; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/dto/CustomerAccumulatingCouponFindResultDto.java b/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/dto/CustomerAccumulatingCouponFindResultDto.java new file mode 100644 index 000000000..496e5074a --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/dto/CustomerAccumulatingCouponFindResultDto.java @@ -0,0 +1,37 @@ +package com.stampcrush.backend.application.manager.coupon.dto; + +import com.stampcrush.backend.entity.coupon.Coupon; +import com.stampcrush.backend.entity.user.Customer; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +import static lombok.AccessLevel.PROTECTED; + +@AllArgsConstructor +@NoArgsConstructor(access = PROTECTED) +@Getter +public class CustomerAccumulatingCouponFindResultDto { + + private Long id; + private Long customerId; + private String nickname; + private int stampCount; + private LocalDateTime expireDate; + private boolean isPrevious; + private int maxStampCount; + + public static CustomerAccumulatingCouponFindResultDto of(Coupon coupon, Customer customer, boolean isPrevious) { + return new CustomerAccumulatingCouponFindResultDto( + coupon.getId(), + customer.getId(), + customer.getNickname(), + coupon.getStampCount(), + coupon.calculateExpireDate(), + isPrevious, + coupon.getCouponMaxStampCount() + ); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/dto/StampCreateDto.java b/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/dto/StampCreateDto.java new file mode 100644 index 000000000..fd30c511a --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/coupon/dto/StampCreateDto.java @@ -0,0 +1,14 @@ +package com.stampcrush.backend.application.manager.coupon.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class StampCreateDto { + + private Long ownerId; + private Long customerId; + private Long couponId; + private Integer earningStampCount; +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/customer/ManagerCustomerCommandService.java b/backend/src/main/java/com/stampcrush/backend/application/manager/customer/ManagerCustomerCommandService.java new file mode 100644 index 000000000..be444957a --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/customer/ManagerCustomerCommandService.java @@ -0,0 +1,31 @@ +package com.stampcrush.backend.application.manager.customer; + +import com.stampcrush.backend.entity.user.TemporaryCustomer; +import com.stampcrush.backend.exception.CustomerBadRequestException; +import com.stampcrush.backend.repository.user.CustomerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional +@Service +public class ManagerCustomerCommandService { + + private final CustomerRepository customerRepository; + + public Long createTemporaryCustomer(String phoneNumber) { + checkExistCustomer(phoneNumber); + + TemporaryCustomer temporaryCustomer = TemporaryCustomer.from(phoneNumber); + TemporaryCustomer savedTemporaryCustomer = customerRepository.save(temporaryCustomer); + + return savedTemporaryCustomer.getId(); + } + + private void checkExistCustomer(String phoneNumber) { + if (!customerRepository.findByPhoneNumber(phoneNumber).isEmpty()) { + throw new CustomerBadRequestException("이미 존재하는 회원입니다"); + } + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/customer/ManagerCustomerFindService.java b/backend/src/main/java/com/stampcrush/backend/application/manager/customer/ManagerCustomerFindService.java new file mode 100644 index 000000000..3f89b3cb4 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/customer/ManagerCustomerFindService.java @@ -0,0 +1,31 @@ +package com.stampcrush.backend.application.manager.customer; + +import com.stampcrush.backend.application.manager.customer.dto.CustomerFindDto; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.repository.user.CustomerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static java.util.stream.Collectors.toList; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class ManagerCustomerFindService { + + private final CustomerRepository customerRepository; + + @Transactional(readOnly = true) + public List findCustomer(String phoneNumber) { + List customers = customerRepository.findByPhoneNumber(phoneNumber); + + List customerFindDtos = customers.stream() + .map(CustomerFindDto::from) + .collect(toList()); + + return customerFindDtos; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/customer/dto/CustomerFindDto.java b/backend/src/main/java/com/stampcrush/backend/application/manager/customer/dto/CustomerFindDto.java new file mode 100644 index 000000000..82bd3fddc --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/customer/dto/CustomerFindDto.java @@ -0,0 +1,24 @@ +package com.stampcrush.backend.application.manager.customer.dto; + +import com.stampcrush.backend.entity.user.Customer; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static lombok.AccessLevel.PUBLIC; + +@EqualsAndHashCode +@NoArgsConstructor(access = PUBLIC) +@AllArgsConstructor +@Getter +public class CustomerFindDto { + + private Long id; + private String nickname; + private String phoneNumber; + + public static CustomerFindDto from(Customer customer) { + return new CustomerFindDto(customer.getId(), customer.getNickname(), customer.getPhoneNumber()); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/image/ManagerImageCommandService.java b/backend/src/main/java/com/stampcrush/backend/application/manager/image/ManagerImageCommandService.java new file mode 100644 index 000000000..3518a7f80 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/image/ManagerImageCommandService.java @@ -0,0 +1,17 @@ +package com.stampcrush.backend.application.manager.image; + +import com.stampcrush.backend.entity.image.ImageUploader; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@RequiredArgsConstructor +@Service +public class ManagerImageCommandService { + + private final ImageUploader imageUploader; + + public String uploadImageAndReturnUrl(MultipartFile image) { + return imageUploader.upload(image); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/reward/ManagerRewardCommandService.java b/backend/src/main/java/com/stampcrush/backend/application/manager/reward/ManagerRewardCommandService.java new file mode 100644 index 000000000..d50f54d77 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/reward/ManagerRewardCommandService.java @@ -0,0 +1,32 @@ +package com.stampcrush.backend.application.manager.reward; + +import com.stampcrush.backend.application.manager.reward.dto.RewardUsedUpdateDto; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.reward.Reward; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.reward.RewardRepository; +import com.stampcrush.backend.repository.user.CustomerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional +@Service +public class ManagerRewardCommandService { + + private final RewardRepository rewardRepository; + private final CustomerRepository customerRepository; + private final CafeRepository cafeRepository; + + public void useReward(RewardUsedUpdateDto rewardUsedUpdateDto) { + Reward reward = rewardRepository.findById(rewardUsedUpdateDto.getRewardId()) + .orElseThrow(IllegalArgumentException::new); + Customer customer = customerRepository.findById(rewardUsedUpdateDto.getCustomerId()) + .orElseThrow(IllegalArgumentException::new); + Cafe cafe = cafeRepository.findById(rewardUsedUpdateDto.getCafeId()) + .orElseThrow(IllegalArgumentException::new); + reward.useReward(customer, cafe); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/reward/ManagerRewardFindService.java b/backend/src/main/java/com/stampcrush/backend/application/manager/reward/ManagerRewardFindService.java new file mode 100644 index 000000000..62cc66512 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/reward/ManagerRewardFindService.java @@ -0,0 +1,26 @@ +package com.stampcrush.backend.application.manager.reward; + +import com.stampcrush.backend.application.manager.reward.dto.RewardFindDto; +import com.stampcrush.backend.application.manager.reward.dto.RewardFindResultDto; +import com.stampcrush.backend.entity.reward.Reward; +import com.stampcrush.backend.repository.reward.RewardRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class ManagerRewardFindService { + + private final RewardRepository rewardRepository; + + public List findRewards(RewardFindDto rewardFindDto) { + List rewards = rewardRepository.findAllByCustomerIdAndCafeIdAndUsed(rewardFindDto.getCustomerId(), rewardFindDto.getCafeId(), rewardFindDto.isUsed()); + return rewards.stream() + .map(reward -> new RewardFindResultDto(reward.getId(), reward.getName())) + .toList(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/reward/dto/RewardFindDto.java b/backend/src/main/java/com/stampcrush/backend/application/manager/reward/dto/RewardFindDto.java new file mode 100644 index 000000000..f6d494583 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/reward/dto/RewardFindDto.java @@ -0,0 +1,13 @@ +package com.stampcrush.backend.application.manager.reward.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class RewardFindDto { + + private final Long customerId; + private final Long cafeId; + private final boolean used; +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/reward/dto/RewardFindResultDto.java b/backend/src/main/java/com/stampcrush/backend/application/manager/reward/dto/RewardFindResultDto.java new file mode 100644 index 000000000..51427cafb --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/reward/dto/RewardFindResultDto.java @@ -0,0 +1,12 @@ +package com.stampcrush.backend.application.manager.reward.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class RewardFindResultDto { + + private final Long id; + private final String name; +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/reward/dto/RewardUsedUpdateDto.java b/backend/src/main/java/com/stampcrush/backend/application/manager/reward/dto/RewardUsedUpdateDto.java new file mode 100644 index 000000000..b762afa28 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/reward/dto/RewardUsedUpdateDto.java @@ -0,0 +1,14 @@ +package com.stampcrush.backend.application.manager.reward.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class RewardUsedUpdateDto { + + private final Long rewardId; + private final Long customerId; + private final Long cafeId; + private final Boolean used; +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/sample/ManagerSampleCouponFindService.java b/backend/src/main/java/com/stampcrush/backend/application/manager/sample/ManagerSampleCouponFindService.java new file mode 100644 index 000000000..e56f33665 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/sample/ManagerSampleCouponFindService.java @@ -0,0 +1,59 @@ +package com.stampcrush.backend.application.manager.sample; + +import com.stampcrush.backend.application.manager.sample.dto.SampleCouponsFindResultDto; +import com.stampcrush.backend.entity.sample.SampleBackImage; +import com.stampcrush.backend.entity.sample.SampleFrontImage; +import com.stampcrush.backend.entity.sample.SampleStampCoordinate; +import com.stampcrush.backend.entity.sample.SampleStampImage; +import com.stampcrush.backend.repository.sample.SampleBackImageRepository; +import com.stampcrush.backend.repository.sample.SampleFrontImageRepository; +import com.stampcrush.backend.repository.sample.SampleStampCoordinateRepository; +import com.stampcrush.backend.repository.sample.SampleStampImageRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class ManagerSampleCouponFindService { + + private final SampleFrontImageRepository sampleFrontImageRepository; + private final SampleBackImageRepository sampleBackImageRepository; + private final SampleStampCoordinateRepository sampleStampCoordinateRepository; + private final SampleStampImageRepository sampleStampImageRepository; + + public SampleCouponsFindResultDto findSampleCouponsByMaxStampCount(Integer maxStampCount) { + List sampleFrontImages = sampleFrontImageRepository.findAll(); + List sampleStampImages = sampleStampImageRepository.findAll(); + + if (maxStampCount == null) { + List sampleStampCoordinates = sampleStampCoordinateRepository.findAll(); + List sampleBackImages = sampleBackImageRepository.findAll(); + return SampleCouponsFindResultDto.of(sampleFrontImages, sampleBackImages, sampleStampCoordinates, sampleStampImages); + } + + List sampleBackImages = sampleBackImageRepository.findAll() + .stream() + .filter(isSameMaxStampCount(maxStampCount)) + .toList(); + + List coordinates = new ArrayList<>(); + for (SampleBackImage sampleBackImage : sampleBackImages) { + coordinates.addAll(sampleStampCoordinateRepository.findSampleStampCoordinateBySampleBackImage(sampleBackImage)); + } + + return SampleCouponsFindResultDto.of(sampleFrontImages, sampleBackImages, coordinates, sampleStampImages); + } + + private Predicate isSameMaxStampCount(int maxStampCount) { + return sampleBackImage -> { + List coordinates = sampleStampCoordinateRepository.findSampleStampCoordinateBySampleBackImage(sampleBackImage); + return coordinates.size() == maxStampCount; + }; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/manager/sample/dto/SampleCouponsFindResultDto.java b/backend/src/main/java/com/stampcrush/backend/application/manager/sample/dto/SampleCouponsFindResultDto.java new file mode 100644 index 000000000..9eea58d72 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/manager/sample/dto/SampleCouponsFindResultDto.java @@ -0,0 +1,105 @@ +package com.stampcrush.backend.application.manager.sample.dto; + +import com.stampcrush.backend.entity.sample.SampleBackImage; +import com.stampcrush.backend.entity.sample.SampleFrontImage; +import com.stampcrush.backend.entity.sample.SampleStampCoordinate; +import com.stampcrush.backend.entity.sample.SampleStampImage; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class SampleCouponsFindResultDto { + + private final List sampleFrontImages; + private final List sampleBackImages; + private final List sampleStampCoordinates; + private final List sampleStampImages; + + public static SampleCouponsFindResultDto of( + List sampleFrontImages, + List sampleBackImages, + List sampleStampCoordinates, + List sampleStampImages + ) { + List sampleFrontImageDto = sampleFrontImages.stream().map(SampleFrontImageDto::from).toList(); + List sampleBackImageDto = sampleBackImages.stream().map(SampleBackImageDto::from).toList(); + List sampleStampCoordinateDtos = sampleStampCoordinates.stream() + .map(SampleStampCoordinateDto::from) + .toList(); + List sampleStampImageDto = sampleStampImages.stream().map(SampleStampImageDto::from).toList(); + + return new SampleCouponsFindResultDto(sampleFrontImageDto, sampleBackImageDto, sampleStampCoordinateDtos, sampleStampImageDto); + } + + @Getter + @RequiredArgsConstructor + public static class SampleFrontImageDto { + + private final Long id; + private final String imageUrl; + + public static SampleFrontImageDto from(SampleFrontImage sampleFrontImage) { + return new SampleFrontImageDto( + sampleFrontImage.getId(), + sampleFrontImage.getImageUrl() + ); + } + } + + @Getter + @RequiredArgsConstructor + public static class SampleBackImageDto { + + private final Long id; + private final String imageUrl; + + public static SampleBackImageDto from(SampleBackImage sampleBackImage) { + return new SampleBackImageDto( + sampleBackImage.getId(), + sampleBackImage.getImageUrl() + ); + } + } + + @Getter + @RequiredArgsConstructor + public static class SampleStampCoordinateDto { + + private final Integer order; + private final Integer xCoordinate; + private final Integer yCoordinate; + private final Long sampleBackImageId; + + public static SampleStampCoordinateDto from(SampleStampCoordinate coordinate) { + return new SampleStampCoordinateDto( + coordinate.getStampOrder(), + coordinate.getXCoordinate(), + coordinate.getYCoordinate(), + coordinate.getSampleBackImage().getId() + ); + } + + public boolean isCorrespondingCoordinateSampleBackImage(Long sampleBackImageId) { + return this.getSampleBackImageId().equals(sampleBackImageId); + } + } + + @Getter + @RequiredArgsConstructor + public static class SampleStampImageDto { + + private final Long id; + private final String imageUrl; + + public static SampleStampImageDto from(SampleStampImage sampleStampImage) { + return new SampleStampImageDto( + sampleStampImage.getId(), + sampleStampImage.getImageUrl() + ); + } + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/visitor/cafe/VisitorCafeFindService.java b/backend/src/main/java/com/stampcrush/backend/application/visitor/cafe/VisitorCafeFindService.java new file mode 100644 index 000000000..8e6e594ab --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/visitor/cafe/VisitorCafeFindService.java @@ -0,0 +1,23 @@ +package com.stampcrush.backend.application.visitor.cafe; + +import com.stampcrush.backend.application.visitor.cafe.dto.CafeInfoFindByCustomerResultDto; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.exception.CafeNotFoundException; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class VisitorCafeFindService { + + private final CafeRepository cafeRepository; + + public CafeInfoFindByCustomerResultDto findCafeById(Long cafeId) { + Cafe cafe = cafeRepository.findById(cafeId) + .orElseThrow(() -> new CafeNotFoundException("존재하지 않는 카페입니다")); + return CafeInfoFindByCustomerResultDto.from(cafe); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/visitor/cafe/dto/CafeFindResultDto.java b/backend/src/main/java/com/stampcrush/backend/application/visitor/cafe/dto/CafeFindResultDto.java new file mode 100644 index 000000000..d5a11f449 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/visitor/cafe/dto/CafeFindResultDto.java @@ -0,0 +1,41 @@ +package com.stampcrush.backend.application.visitor.cafe.dto; + +import com.stampcrush.backend.entity.cafe.Cafe; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalTime; + +@Getter +@RequiredArgsConstructor +public class CafeFindResultDto { + + private final Long id; + private final String name; + + @DateTimeFormat(pattern = "HH:mm") + private final LocalTime openTime; + + @DateTimeFormat(pattern = "HH:mm") + private final LocalTime closeTime; + + private final String telephoneNumber; + private final String cafeImageUrl; + private final String roadAddress; + private final String detailAddress; + private final String businessRegistrationNumber; + + public static CafeFindResultDto from(Cafe cafe) { + return new CafeFindResultDto( + cafe.getId(), + cafe.getName(), + cafe.getOpenTime(), + cafe.getCloseTime(), + cafe.getTelephoneNumber(), + cafe.getCafeImageUrl(), + cafe.getRoadAddress(), + cafe.getDetailAddress(), + cafe.getBusinessRegistrationNumber()); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/visitor/cafe/dto/CafeInfoFindByCustomerResultDto.java b/backend/src/main/java/com/stampcrush/backend/application/visitor/cafe/dto/CafeInfoFindByCustomerResultDto.java new file mode 100644 index 000000000..d679b0a86 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/visitor/cafe/dto/CafeInfoFindByCustomerResultDto.java @@ -0,0 +1,41 @@ +package com.stampcrush.backend.application.visitor.cafe.dto; + +import com.stampcrush.backend.entity.cafe.Cafe; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalTime; + +@Getter +@RequiredArgsConstructor +public class CafeInfoFindByCustomerResultDto { + + private final Long id; + private final String name; + private final String introduction; + + @DateTimeFormat(pattern = "HH:mm") + private final LocalTime openTime; + + @DateTimeFormat(pattern = "HH:mm") + private final LocalTime closeTime; + + private final String telephoneNumber; + private final String cafeImageUrl; + private final String roadAddress; + private final String detailAddress; + + public static CafeInfoFindByCustomerResultDto from(Cafe cafe) { + return new CafeInfoFindByCustomerResultDto( + cafe.getId(), + cafe.getName(), + cafe.getIntroduction(), + cafe.getOpenTime(), + cafe.getCloseTime(), + cafe.getTelephoneNumber(), + cafe.getCafeImageUrl(), + cafe.getRoadAddress(), + cafe.getDetailAddress()); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/visitor/coupon/VisitorCouponCommandService.java b/backend/src/main/java/com/stampcrush/backend/application/visitor/coupon/VisitorCouponCommandService.java new file mode 100644 index 000000000..c3b75dccd --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/visitor/coupon/VisitorCouponCommandService.java @@ -0,0 +1,22 @@ +package com.stampcrush.backend.application.visitor.coupon; + +import com.stampcrush.backend.entity.coupon.Coupon; +import com.stampcrush.backend.exception.CouponNotFoundException; +import com.stampcrush.backend.repository.coupon.CouponRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional +@Service +public class VisitorCouponCommandService { + + private final CouponRepository couponRepository; + + public void deleteCoupon(Long customerId, Long couponId) { + Coupon coupon = couponRepository.findByIdAndCustomerId(couponId, customerId) + .orElseThrow(() -> new CouponNotFoundException("쿠폰을 찾을 수 없습니다.")); + couponRepository.delete(coupon); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/visitor/coupon/VisitorCouponFindService.java b/backend/src/main/java/com/stampcrush/backend/application/visitor/coupon/VisitorCouponFindService.java new file mode 100644 index 000000000..c19be2b16 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/visitor/coupon/VisitorCouponFindService.java @@ -0,0 +1,56 @@ +package com.stampcrush.backend.application.visitor.coupon; + +import com.stampcrush.backend.application.visitor.coupon.dto.CustomerCouponFindResultDto; +import com.stampcrush.backend.application.visitor.favorites.VisitorFavoritesFindService; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.coupon.Coupon; +import com.stampcrush.backend.entity.coupon.CouponStampCoordinate; +import com.stampcrush.backend.entity.coupon.CouponStatus; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.exception.CustomerNotFoundException; +import com.stampcrush.backend.repository.coupon.CouponRepository; +import com.stampcrush.backend.repository.coupon.CouponStampCoordinateRepository; +import com.stampcrush.backend.repository.user.CustomerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class VisitorCouponFindService { + + private final VisitorFavoritesFindService visitorFavoritesFindService; + private final CustomerRepository customerRepository; + private final CouponRepository couponRepository; + private final CouponStampCoordinateRepository couponStampCoordinateRepository; + + public List findOneCouponForOneCafe(Long customerId) { + Customer customer = findExistingCustomer(customerId); + List coupons = couponRepository.findFilteredAndSortedCoupons(customer, CouponStatus.ACCUMULATING); + + return coupons.stream() + .map(coupon -> formatToDto(customer, coupon)) + .toList(); + } + + private CustomerCouponFindResultDto formatToDto(Customer customer, Coupon coupon) { + Cafe cafe = coupon.getCafe(); + Boolean isFavorites = visitorFavoritesFindService.findIsFavorites(cafe, customer); + List coordinates = couponStampCoordinateRepository.findByCouponDesign(coupon.getCouponDesign()); + return CustomerCouponFindResultDto.of(cafe, coupon, isFavorites, coordinates); + } + + private Customer findExistingCustomer(Long customerId) { + Optional findCustomer = customerRepository.findById(customerId); + + if (findCustomer.isEmpty()) { + throw new CustomerNotFoundException("해당 id의 고객을 찾을 수 없습니다. 회원가입 해 주세요."); + } + + return findCustomer.get(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/visitor/coupon/dto/CustomerCouponFindResultDto.java b/backend/src/main/java/com/stampcrush/backend/application/visitor/coupon/dto/CustomerCouponFindResultDto.java new file mode 100644 index 000000000..22f08c1e3 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/visitor/coupon/dto/CustomerCouponFindResultDto.java @@ -0,0 +1,87 @@ +package com.stampcrush.backend.application.visitor.coupon.dto; + +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.coupon.Coupon; +import com.stampcrush.backend.entity.coupon.CouponStampCoordinate; +import com.stampcrush.backend.entity.coupon.CouponStatus; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class CustomerCouponFindResultDto { + + private final CafeInfoDto cafeInfoDto; + private final CouponInfoDto couponInfoDto; + + public static CustomerCouponFindResultDto of( + Cafe cafe, + Coupon coupon, + Boolean isFavorites, + List coordinates + ) { + CafeInfoDto cafeInfoDto = CafeInfoDto.of(cafe, isFavorites); + CouponInfoDto couponInfoDto = CouponInfoDto.of(coupon, coordinates); + return new CustomerCouponFindResultDto(cafeInfoDto, couponInfoDto); + } + + @Getter + @RequiredArgsConstructor + public static class CafeInfoDto { + + private final Long id; + private final String name; + private final Boolean isFavorites; + + public static CafeInfoDto of(Cafe cafe, Boolean isFavorites) { + return new CafeInfoDto(cafe.getId(), cafe.getName(), isFavorites); + } + } + + @Getter + @RequiredArgsConstructor + public static class CouponInfoDto { + private final Long id; + private final CouponStatus status; + private final Integer stampCount; + private final Integer maxStampCount; + private final String rewardName; + private final String frontImageUrl; + private final String backImageUrl; + private final String stampImageUrl; + private final List coordinates; + + public static CouponInfoDto of(Coupon coupon, List coordinates) { + return new CouponInfoDto( + coupon.getId(), + coupon.getStatus(), + coupon.getStampCount(), + coupon.getCouponMaxStampCount(), + coupon.getRewardName(), + coupon.getCouponDesign().getFrontImageUrl(), + coupon.getCouponDesign().getBackImageUrl(), + coupon.getCouponDesign().getStampImageUrl(), + coordinates.stream().map(CouponCoordinatesDto::from).toList() + ); + } + + @Getter + @RequiredArgsConstructor + public static class CouponCoordinatesDto { + + private final Integer order; + private final Integer xCoordinate; + private final Integer yCoordinate; + + public static CouponCoordinatesDto from(CouponStampCoordinate coordinate) { + return new CouponCoordinatesDto( + coordinate.getStampOrder(), + coordinate.getXCoordinate(), + coordinate.getYCoordinate() + ); + } + } + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/visitor/favorites/VisitorFavoritesCommandService.java b/backend/src/main/java/com/stampcrush/backend/application/visitor/favorites/VisitorFavoritesCommandService.java new file mode 100644 index 000000000..efe26cbcc --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/visitor/favorites/VisitorFavoritesCommandService.java @@ -0,0 +1,38 @@ +package com.stampcrush.backend.application.visitor.favorites; + +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.favorites.Favorites; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.exception.CafeNotFoundException; +import com.stampcrush.backend.exception.CustomerNotFoundException; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.favorites.FavoritesRepository; +import com.stampcrush.backend.repository.user.CustomerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@RequiredArgsConstructor +@Transactional +@Service +public class VisitorFavoritesCommandService { + + private final FavoritesRepository favoritesRepository; + private final CafeRepository cafeRepository; + private final CustomerRepository customerRepository; + + public void changeFavorites(Long customerId, Long cafeId, Boolean isFavorites) { + Cafe cafe = cafeRepository.findById(cafeId) + .orElseThrow(() -> new CafeNotFoundException("카페를 찾을 수 없습니다.")); + Customer customer = customerRepository.findById(customerId) + .orElseThrow(() -> new CustomerNotFoundException("고객을 찾을 수 없습니다")); + Optional findFavorites = favoritesRepository.findByCafeAndCustomer(cafe, customer); + if (findFavorites.isPresent()) { + findFavorites.get().changeFavorites(isFavorites); + return; + } + favoritesRepository.save(new Favorites(cafe, customer, isFavorites)); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/visitor/favorites/VisitorFavoritesFindService.java b/backend/src/main/java/com/stampcrush/backend/application/visitor/favorites/VisitorFavoritesFindService.java new file mode 100644 index 000000000..53f6d6787 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/visitor/favorites/VisitorFavoritesFindService.java @@ -0,0 +1,29 @@ +package com.stampcrush.backend.application.visitor.favorites; + +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.favorites.Favorites; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.repository.favorites.FavoritesRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class VisitorFavoritesFindService { + + private final FavoritesRepository favoritesRepository; + + public boolean findIsFavorites(Cafe cafe, Customer customer) { + Optional favorites = favoritesRepository.findByCafeAndCustomer(cafe, customer); + + if (favorites.isEmpty()) { + return false; + } + + return favorites.get().getIsFavorites(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/visitor/profile/VisitorProfilesCommandService.java b/backend/src/main/java/com/stampcrush/backend/application/visitor/profile/VisitorProfilesCommandService.java new file mode 100644 index 000000000..9d504ef2c --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/visitor/profile/VisitorProfilesCommandService.java @@ -0,0 +1,39 @@ +package com.stampcrush.backend.application.visitor.profile; + +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.exception.CustomerBadRequestException; +import com.stampcrush.backend.exception.CustomerNotFoundException; +import com.stampcrush.backend.repository.user.CustomerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@RequiredArgsConstructor +@Transactional +@Service +public class VisitorProfilesCommandService { + + private final CustomerRepository customerRepository; + + public void registerPhoneNumber(Long customerId, String phoneNumber) { + try { + Customer customer = findExistingCustomer(customerId); + customer.registerPhoneNumber(phoneNumber); + } catch (DataIntegrityViolationException exception) { + throw new CustomerBadRequestException("이미 등록된 전화번호입니다.", exception); + } + } + + private Customer findExistingCustomer(Long customerId) { + Optional findCustomer = customerRepository.findById(customerId); + + if (findCustomer.isEmpty()) { + throw new CustomerNotFoundException("해당 아이디의 고객을 찾을 수 없습니다."); + } + + return findCustomer.get(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/visitor/profile/VisitorProfilesFindService.java b/backend/src/main/java/com/stampcrush/backend/application/visitor/profile/VisitorProfilesFindService.java new file mode 100644 index 000000000..bf2b2866e --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/visitor/profile/VisitorProfilesFindService.java @@ -0,0 +1,24 @@ +package com.stampcrush.backend.application.visitor.profile; + +import com.stampcrush.backend.application.visitor.profile.dto.VisitorProfileFindResultDto; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.exception.CustomerNotFoundException; +import com.stampcrush.backend.repository.user.RegisterCustomerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class VisitorProfilesFindService { + + private final RegisterCustomerRepository registerCustomerRepository; + + public VisitorProfileFindResultDto findVisitorProfile(Long customerId) { + RegisterCustomer customer = registerCustomerRepository.findById(customerId) + .orElseThrow(() -> new CustomerNotFoundException("고객을 찾을 수 없습니다")); + + return new VisitorProfileFindResultDto(customer.getId(), customer.getNickname(), customer.getPhoneNumber(), customer.getEmail()); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/visitor/profile/dto/VisitorProfileFindResultDto.java b/backend/src/main/java/com/stampcrush/backend/application/visitor/profile/dto/VisitorProfileFindResultDto.java new file mode 100644 index 000000000..71abab387 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/visitor/profile/dto/VisitorProfileFindResultDto.java @@ -0,0 +1,15 @@ +package com.stampcrush.backend.application.visitor.profile.dto; + + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class VisitorProfileFindResultDto { + + private final Long id; + private final String nickname; + private final String phoneNumber; + private final String email; +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/visitor/reward/VisitorRewardsFindService.java b/backend/src/main/java/com/stampcrush/backend/application/visitor/reward/VisitorRewardsFindService.java new file mode 100644 index 000000000..c71c8ea1f --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/visitor/reward/VisitorRewardsFindService.java @@ -0,0 +1,40 @@ +package com.stampcrush.backend.application.visitor.reward; + +import com.stampcrush.backend.application.visitor.reward.dto.VisitorRewardsFindResultDto; +import com.stampcrush.backend.entity.reward.Reward; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.exception.CustomerNotFoundException; +import com.stampcrush.backend.repository.reward.RewardRepository; +import com.stampcrush.backend.repository.user.CustomerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +@Service +@Transactional +public class VisitorRewardsFindService { + + private final RewardRepository rewardRepository; + private final CustomerRepository customerRepository; + + public List findRewards(Long customerId, Boolean used) { + Customer customer = findExistingCustomer(customerId); + List findRewards = rewardRepository.findAllByCustomerAndUsed(customer, used); + return findRewards.stream() + .map(VisitorRewardsFindResultDto::from) + .toList(); + } + + private Customer findExistingCustomer(Long customerId) { + Optional customer = customerRepository.findById(customerId); + if (customer.isEmpty()) { + throw new CustomerNotFoundException("해당 id를 가진 고객을 찾을 수 없습니다."); + } + + return customer.get(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/visitor/reward/dto/VisitorRewardsFindResultDto.java b/backend/src/main/java/com/stampcrush/backend/application/visitor/reward/dto/VisitorRewardsFindResultDto.java new file mode 100644 index 000000000..bbd56d3c1 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/visitor/reward/dto/VisitorRewardsFindResultDto.java @@ -0,0 +1,28 @@ +package com.stampcrush.backend.application.visitor.reward.dto; + +import com.stampcrush.backend.entity.reward.Reward; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@RequiredArgsConstructor +public class VisitorRewardsFindResultDto { + + private final Long id; + private final String rewardName; + private final String cafeName; + private final LocalDateTime createdAt; + private final LocalDateTime updatedAt; + + public static VisitorRewardsFindResultDto from(Reward reward) { + return new VisitorRewardsFindResultDto( + reward.getId(), + reward.getName(), + reward.getCafe().getName(), + reward.getCreatedAt(), + reward.getUpdatedAt() + ); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/visitor/visithistory/VisitorVisitHistoryFindService.java b/backend/src/main/java/com/stampcrush/backend/application/visitor/visithistory/VisitorVisitHistoryFindService.java new file mode 100644 index 000000000..e27bf020f --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/visitor/visithistory/VisitorVisitHistoryFindService.java @@ -0,0 +1,36 @@ +package com.stampcrush.backend.application.visitor.visithistory; + +import com.stampcrush.backend.application.visitor.visithistory.dto.CustomerStampHistoryFindResultDto; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.visithistory.VisitHistory; +import com.stampcrush.backend.exception.CustomerNotFoundException; +import com.stampcrush.backend.repository.user.CustomerRepository; +import com.stampcrush.backend.repository.visithistory.VisitHistoryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class VisitorVisitHistoryFindService { + + private final VisitHistoryRepository visitHistoryRepository; + private final CustomerRepository customerRepository; + + public List findStampHistoriesByCustomer(Long customerId) { + Customer customer = findCustomerById(customerId); + + List visitHistories = visitHistoryRepository.findVisitHistoriesByCustomer(customer); + return visitHistories.stream() + .map(CustomerStampHistoryFindResultDto::from) + .toList(); + } + + private Customer findCustomerById(Long customerId) { + return customerRepository.findById(customerId) + .orElseThrow(() -> new CustomerNotFoundException("존재하지 않는 고객입니다.")); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/application/visitor/visithistory/dto/CustomerStampHistoryFindResultDto.java b/backend/src/main/java/com/stampcrush/backend/application/visitor/visithistory/dto/CustomerStampHistoryFindResultDto.java new file mode 100644 index 000000000..b900b9204 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/application/visitor/visithistory/dto/CustomerStampHistoryFindResultDto.java @@ -0,0 +1,26 @@ +package com.stampcrush.backend.application.visitor.visithistory.dto; + +import com.stampcrush.backend.entity.visithistory.VisitHistory; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; + +@RequiredArgsConstructor +@Getter +public class CustomerStampHistoryFindResultDto { + + private final Long id; + private final String cafeName; + private final int stampCount; + private final LocalDateTime createdAt; + + public static CustomerStampHistoryFindResultDto from(VisitHistory visitHistory) { + return new CustomerStampHistoryFindResultDto( + visitHistory.getId(), + visitHistory.getCafeName(), + visitHistory.getStampCount(), + visitHistory.getCreatedAt() + ); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/OAuthProvider.java b/backend/src/main/java/com/stampcrush/backend/auth/OAuthProvider.java new file mode 100644 index 000000000..273d0eef3 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/OAuthProvider.java @@ -0,0 +1,5 @@ +package com.stampcrush.backend.auth; + +public enum OAuthProvider { + KAKAO, NAVER +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/api/ManagerOAuthController.java b/backend/src/main/java/com/stampcrush/backend/auth/api/ManagerOAuthController.java new file mode 100644 index 000000000..d1ad98f70 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/api/ManagerOAuthController.java @@ -0,0 +1,40 @@ +package com.stampcrush.backend.auth.api; + +import com.stampcrush.backend.auth.api.response.AuthTokensResponse; +import com.stampcrush.backend.auth.application.manager.ManagerOAuthLoginService; +import com.stampcrush.backend.auth.application.manager.ManagerOAuthService; +import com.stampcrush.backend.auth.application.util.KakaoLoginParams; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.net.URI; + +@RequiredArgsConstructor +@RestController +@Profile("!test") +@RequestMapping("/api/admin/login") +public class ManagerOAuthController { + + private final ManagerOAuthService managerOAuthService; + private final ManagerOAuthLoginService managerOAuthLoginService; + + @GetMapping("/kakao") + public ResponseEntity loginKakao() { + String redirectUri = managerOAuthService.findLoginRedirectUri(); + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(redirectUri)) + .build(); + } + + @GetMapping("/kakao/token") + public ResponseEntity authorizeUser(@RequestParam("code") String authorizationCode) { + KakaoLoginParams params = new KakaoLoginParams(authorizationCode); + return ResponseEntity.ok(managerOAuthLoginService.login(params)); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/api/VisitorAuthController.java b/backend/src/main/java/com/stampcrush/backend/auth/api/VisitorAuthController.java new file mode 100644 index 000000000..e04bc9dfe --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/api/VisitorAuthController.java @@ -0,0 +1,32 @@ +package com.stampcrush.backend.auth.api; + +import com.stampcrush.backend.auth.api.response.AuthTokensResponse; +import com.stampcrush.backend.auth.application.VisitorAuthService; +import com.stampcrush.backend.auth.request.OAuthRegisterCustomerCreateRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/login") +public class VisitorAuthController { + + private final VisitorAuthService visitorAuthService; + + @PostMapping("/test/token") + public ResponseEntity authorizeUser( + OAuthRegisterCustomerCreateRequest request + ) { + return ResponseEntity.ok().body( + visitorAuthService.join( + request.getNickname(), + request.getEmail(), + request.getOAuthProvider(), + request.getOAuthId() + ) + ); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/api/VisitorOAuthController.java b/backend/src/main/java/com/stampcrush/backend/auth/api/VisitorOAuthController.java new file mode 100644 index 000000000..eaa77aa62 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/api/VisitorOAuthController.java @@ -0,0 +1,40 @@ +package com.stampcrush.backend.auth.api; + +import com.stampcrush.backend.auth.api.response.AuthTokensResponse; +import com.stampcrush.backend.auth.application.util.KakaoLoginParams; +import com.stampcrush.backend.auth.application.visitor.VisitorOAuthLoginService; +import com.stampcrush.backend.auth.application.visitor.VisitorOAuthService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.net.URI; + +@RequiredArgsConstructor +@RestController +@Profile("!test") +@RequestMapping("/api/login") +public class VisitorOAuthController { + + private final VisitorOAuthLoginService visitorOAuthLoginService; + private final VisitorOAuthService visitorOAuthService; + + @GetMapping("/kakao") + public ResponseEntity loginKakao() { + String redirectUri = visitorOAuthService.findLoginRedirectUri(); + return ResponseEntity.status(HttpStatus.FOUND) + .location(URI.create(redirectUri)) + .build(); + } + + @GetMapping("/kakao/token") + public ResponseEntity authorizeUser(@RequestParam("code") String authorizationCode) { + KakaoLoginParams params = new KakaoLoginParams(authorizationCode); + return ResponseEntity.ok(visitorOAuthLoginService.login(params)); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/api/response/AuthTokensResponse.java b/backend/src/main/java/com/stampcrush/backend/auth/api/response/AuthTokensResponse.java new file mode 100644 index 000000000..0852f5878 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/api/response/AuthTokensResponse.java @@ -0,0 +1,22 @@ +package com.stampcrush.backend.auth.api.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class AuthTokensResponse { + + private String accessToken; + private String refreshToken; + private String grantType; + private Long expiresIn; + + public static AuthTokensResponse of(String accessToken, String refreshToken, String grantType, Long expiresIn) { + return new AuthTokensResponse(accessToken, refreshToken, grantType, expiresIn); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/application/VisitorAuthService.java b/backend/src/main/java/com/stampcrush/backend/auth/application/VisitorAuthService.java new file mode 100644 index 000000000..b9530f689 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/application/VisitorAuthService.java @@ -0,0 +1,29 @@ +package com.stampcrush.backend.auth.application; + +import com.stampcrush.backend.auth.OAuthProvider; +import com.stampcrush.backend.auth.api.response.AuthTokensResponse; +import com.stampcrush.backend.auth.application.util.AuthTokensGenerator; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.repository.user.RegisterCustomerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class VisitorAuthService { + + private final RegisterCustomerRepository registerCustomerRepository; + private final AuthTokensGenerator authTokensGenerator; + + public AuthTokensResponse join(String nickname, String email, OAuthProvider oAuthProvider, Long oAuthId) { + RegisterCustomer customer = RegisterCustomer.builder() + .nickname(nickname) + .email(email) + .oAuthProvider(oAuthProvider) + .oAuthId(oAuthId) + .build(); + RegisterCustomer savedCustomer = registerCustomerRepository.save(customer); + Long customerId = savedCustomer.getId(); + return authTokensGenerator.generate(customerId); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/application/manager/ManagerOAuthLoginService.java b/backend/src/main/java/com/stampcrush/backend/auth/application/manager/ManagerOAuthLoginService.java new file mode 100644 index 000000000..759efa2dd --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/application/manager/ManagerOAuthLoginService.java @@ -0,0 +1,47 @@ +package com.stampcrush.backend.auth.application.manager; + +import com.stampcrush.backend.auth.api.response.AuthTokensResponse; +import com.stampcrush.backend.auth.application.util.AuthTokensGenerator; +import com.stampcrush.backend.auth.application.util.OAuthLoginParams; +import com.stampcrush.backend.auth.client.OAuthInfoResponse; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.repository.user.OwnerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Profile("!test") +public class ManagerOAuthLoginService { + + private final OwnerRepository ownerRepository; + private final AuthTokensGenerator authTokensGenerator; + private final ManagerOAuthService requestOAuthInfoService; + + public AuthTokensResponse login(OAuthLoginParams params) { + OAuthInfoResponse oAuthInfoResponse = requestOAuthInfoService.request(params); + Long memberId = findOrCreateOwner(oAuthInfoResponse); + return authTokensGenerator.generate(memberId); + } + + private Long findOrCreateOwner(OAuthInfoResponse oAuthInfo) { + return ownerRepository.findByOAuthProviderAndOAuthId( + oAuthInfo.getOAuthProvider(), + oAuthInfo.getOAuthId() + ) + .map(Owner::getId) + .orElseGet(() -> createOwner(oAuthInfo)); + } + + private Long createOwner(OAuthInfoResponse oAuthInfo) { + Owner oAuthOwner = Owner.builder() + .nickname(oAuthInfo.getNickname()) + .email(oAuthInfo.getEmail()) + .oAuthId(oAuthInfo.getOAuthId()) + .oAuthProvider(oAuthInfo.getOAuthProvider()) + .build(); + + return ownerRepository.save(oAuthOwner).getId(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/application/manager/ManagerOAuthService.java b/backend/src/main/java/com/stampcrush/backend/auth/application/manager/ManagerOAuthService.java new file mode 100644 index 000000000..fb83178b7 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/application/manager/ManagerOAuthService.java @@ -0,0 +1,60 @@ +package com.stampcrush.backend.auth.application.manager; + +import com.stampcrush.backend.auth.OAuthProvider; +import com.stampcrush.backend.auth.application.util.OAuthLoginParams; +import com.stampcrush.backend.auth.client.ManagerOAuthApiClient; +import com.stampcrush.backend.auth.client.OAuthInfoResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +@Profile("!test") +public class ManagerOAuthService { + + private final String clientId; + private final String clientSecret; + private final String baseUri; + private final String apiUri; + private final String redirectUri; + + private final Map clients; + + public ManagerOAuthService( + @Value("${oauth.kakao.client-id}") String clientId, + @Value("${oauth.kakao.client-secret}") String clientSecret, + @Value("${oauth.kakao.redirect-uri-manager}") String redirectUri, + @Value("${oauth.kakao.url.auth}") String baseUri, + @Value("${oauth.kakao.url.api}") String apiUri, + List clients + ) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.redirectUri = redirectUri; + this.baseUri = baseUri; + this.apiUri = apiUri; + this.clients = clients.stream().collect( + Collectors.toUnmodifiableMap(ManagerOAuthApiClient::oAuthProvider, Function.identity()) + ); + } + + public String findLoginRedirectUri() { + return baseUri + + "/oauth/authorize" + + "?response_type=code" + + "&client_id=" + clientId + + "&redirect_uri=" + redirectUri; + } + + public OAuthInfoResponse request(OAuthLoginParams params) { + ManagerOAuthApiClient client = clients.get(params.oAuthProvider()); + String accessToken = client.requestAccessToken(params); + + return client.requestOauthInfo(accessToken); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/application/util/AuthTokensGenerator.java b/backend/src/main/java/com/stampcrush/backend/auth/application/util/AuthTokensGenerator.java new file mode 100644 index 000000000..e0b231b32 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/application/util/AuthTokensGenerator.java @@ -0,0 +1,36 @@ +package com.stampcrush.backend.auth.application.util; + +import com.stampcrush.backend.auth.api.response.AuthTokensResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component +@RequiredArgsConstructor +public class AuthTokensGenerator { + + private static final String BEARER_TYPE = "Bearer"; + private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30; // 30분 + private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7; // 7일 + + private final JwtTokenProvider jwtTokenProvider; + + public AuthTokensResponse generate(Long memberId) { + long now = (new Date()).getTime(); + Date accessTokenExpiredAt = new Date(now + ACCESS_TOKEN_EXPIRE_TIME); + Date refreshTokenExpiredAt = new Date(now + REFRESH_TOKEN_EXPIRE_TIME); + + String subject = memberId.toString(); + + String accessToken = jwtTokenProvider.generate(subject, accessTokenExpiredAt); + + String refreshToken = jwtTokenProvider.generate(subject, refreshTokenExpiredAt); + + return AuthTokensResponse.of(accessToken, refreshToken, BEARER_TYPE, ACCESS_TOKEN_EXPIRE_TIME / 1000L); + } + + public Long extractMemberId(String accessToken) { + return Long.valueOf(jwtTokenProvider.extractSubject(accessToken)); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/application/util/JwtTokenProvider.java b/backend/src/main/java/com/stampcrush/backend/auth/application/util/JwtTokenProvider.java new file mode 100644 index 000000000..1a5cc87a1 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/application/util/JwtTokenProvider.java @@ -0,0 +1,49 @@ +package com.stampcrush.backend.auth.application.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; + +@Component +public class JwtTokenProvider { + + private final Key key; + + public JwtTokenProvider(@Value("${jwt.secret-key}") String secretKey) { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + public String generate(String subject, Date expiredAt) { + return Jwts.builder() + .setSubject(subject) + .setExpiration(expiredAt) + .signWith(key, SignatureAlgorithm.HS512) + .compact(); + } + + public String extractSubject(String accessToken) { + Claims claims = parseClaims(accessToken); + return claims.getSubject(); + } + + private Claims parseClaims(String accessToken) { + try { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(accessToken) + .getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/application/util/KakaoLoginParams.java b/backend/src/main/java/com/stampcrush/backend/auth/application/util/KakaoLoginParams.java new file mode 100644 index 000000000..a1d25f1a0 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/application/util/KakaoLoginParams.java @@ -0,0 +1,30 @@ +package com.stampcrush.backend.auth.application.util; + +import com.stampcrush.backend.auth.OAuthProvider; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +@Getter +@NoArgsConstructor +public class KakaoLoginParams implements OAuthLoginParams { + + private String authorizationCode; + + public KakaoLoginParams(String authorizationCode) { + this.authorizationCode = authorizationCode; + } + + @Override + public OAuthProvider oAuthProvider() { + return OAuthProvider.KAKAO; + } + + @Override + public MultiValueMap makeBody() { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("code", authorizationCode); + return body; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/application/util/OAuthLoginParams.java b/backend/src/main/java/com/stampcrush/backend/auth/application/util/OAuthLoginParams.java new file mode 100644 index 000000000..66d424757 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/application/util/OAuthLoginParams.java @@ -0,0 +1,11 @@ +package com.stampcrush.backend.auth.application.util; + +import com.stampcrush.backend.auth.OAuthProvider; +import org.springframework.util.MultiValueMap; + +public interface OAuthLoginParams { + + OAuthProvider oAuthProvider(); + + MultiValueMap makeBody(); +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/application/visitor/VisitorOAuthLoginService.java b/backend/src/main/java/com/stampcrush/backend/auth/application/visitor/VisitorOAuthLoginService.java new file mode 100644 index 000000000..699b72940 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/application/visitor/VisitorOAuthLoginService.java @@ -0,0 +1,47 @@ +package com.stampcrush.backend.auth.application.visitor; + +import com.stampcrush.backend.auth.api.response.AuthTokensResponse; +import com.stampcrush.backend.auth.application.util.AuthTokensGenerator; +import com.stampcrush.backend.auth.application.util.OAuthLoginParams; +import com.stampcrush.backend.auth.client.OAuthInfoResponse; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.repository.user.RegisterCustomerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Profile("!test") +public class VisitorOAuthLoginService { + + private final RegisterCustomerRepository registerCustomerRepository; + private final AuthTokensGenerator authTokensGenerator; + private final VisitorOAuthService requestOAuthInfoService; + + public AuthTokensResponse login(OAuthLoginParams params) { + OAuthInfoResponse oAuthInfoResponse = requestOAuthInfoService.request(params); + Long memberId = findOrCreateCustomer(oAuthInfoResponse); + return authTokensGenerator.generate(memberId); + } + + private Long findOrCreateCustomer(OAuthInfoResponse oAuthInfo) { + return registerCustomerRepository.findByOAuthProviderAndOAuthId( + oAuthInfo.getOAuthProvider(), + oAuthInfo.getOAuthId() + ) + .map(RegisterCustomer::getId) + .orElseGet(() -> createCustomer(oAuthInfo)); + } + + private Long createCustomer(OAuthInfoResponse oAuthInfo) { + RegisterCustomer customer = RegisterCustomer.builder() + .nickname(oAuthInfo.getNickname()) + .email(oAuthInfo.getEmail()) + .oAuthId(oAuthInfo.getOAuthId()) + .oAuthProvider(oAuthInfo.getOAuthProvider()) + .build(); + + return registerCustomerRepository.save(customer).getId(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/application/visitor/VisitorOAuthService.java b/backend/src/main/java/com/stampcrush/backend/auth/application/visitor/VisitorOAuthService.java new file mode 100644 index 000000000..19ddc0624 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/application/visitor/VisitorOAuthService.java @@ -0,0 +1,60 @@ +package com.stampcrush.backend.auth.application.visitor; + +import com.stampcrush.backend.auth.OAuthProvider; +import com.stampcrush.backend.auth.application.util.OAuthLoginParams; +import com.stampcrush.backend.auth.client.OAuthInfoResponse; +import com.stampcrush.backend.auth.client.VisitorOAuthApiClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +@Profile("!test") +public class VisitorOAuthService { + + private final String clientId; + private final String clientSecret; + private final String baseUri; + private final String apiUri; + private final String redirectUri; + + private final Map clients; + + public VisitorOAuthService( + @Value("${oauth.kakao.client-id}") String clientId, + @Value("${oauth.kakao.client-secret}") String clientSecret, + @Value("${oauth.kakao.redirect-uri-visitor}") String redirectUri, + @Value("${oauth.kakao.url.auth}") String baseUri, + @Value("${oauth.kakao.url.api}") String apiUri, + List clients + ) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.redirectUri = redirectUri; + this.baseUri = baseUri; + this.apiUri = apiUri; + this.clients = clients.stream().collect( + Collectors.toUnmodifiableMap(VisitorOAuthApiClient::oAuthProvider, Function.identity()) + ); + } + + public String findLoginRedirectUri() { + return baseUri + + "/oauth/authorize" + + "?response_type=code" + + "&client_id=" + clientId + + "&redirect_uri=" + redirectUri; + } + + public OAuthInfoResponse request(OAuthLoginParams params) { + VisitorOAuthApiClient client = clients.get(params.oAuthProvider()); + String accessToken = client.requestAccessToken(params); + + return client.requestOauthInfo(accessToken); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/client/KakaoInfoResponse.java b/backend/src/main/java/com/stampcrush/backend/auth/client/KakaoInfoResponse.java new file mode 100644 index 000000000..17c00ead5 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/client/KakaoInfoResponse.java @@ -0,0 +1,52 @@ +package com.stampcrush.backend.auth.client; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.stampcrush.backend.auth.OAuthProvider; +import lombok.Getter; + +@Getter +@JsonIgnoreProperties(ignoreUnknown = true) +public class KakaoInfoResponse implements OAuthInfoResponse { + + @JsonProperty("id") + private Long id; + + @JsonProperty("kakao_account") + private KakaoAccount kakaoAccount; + + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + static class KakaoAccount { + + private KakaoProfile profile; + private String email; + } + + @Getter + @JsonIgnoreProperties(ignoreUnknown = true) + static class KakaoProfile { + + private String nickname; + } + + @Override + public String getEmail() { + return kakaoAccount.email; + } + + @Override + public String getNickname() { + return kakaoAccount.profile.nickname; + } + + @Override + public OAuthProvider getOAuthProvider() { + return OAuthProvider.KAKAO; + } + + @Override + public Long getOAuthId() { + return id; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/client/KakaoTokens.java b/backend/src/main/java/com/stampcrush/backend/auth/client/KakaoTokens.java new file mode 100644 index 000000000..26d76d0f1 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/client/KakaoTokens.java @@ -0,0 +1,28 @@ +package com.stampcrush.backend.auth.client; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class KakaoTokens { + + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("token_type") + private String tokenType; + + @JsonProperty("refresh_token") + private String refreshToken; + + @JsonProperty("expires_in") + private String expiresIn; + + @JsonProperty("refresh_token_expires_in") + private String refreshTokenExpiresIn; + + @JsonProperty("scope") + private String scope; +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/client/ManagerKakaoApiClient.java b/backend/src/main/java/com/stampcrush/backend/auth/client/ManagerKakaoApiClient.java new file mode 100644 index 000000000..10d9173e8 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/client/ManagerKakaoApiClient.java @@ -0,0 +1,90 @@ +package com.stampcrush.backend.auth.client; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stampcrush.backend.auth.OAuthProvider; +import com.stampcrush.backend.auth.application.util.OAuthLoginParams; +import com.stampcrush.backend.exception.StampCrushException; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +@Profile("!test") +@RequiredArgsConstructor +@Component +public class ManagerKakaoApiClient implements ManagerOAuthApiClient { + + private static final String GRANT_TYPE = "authorization_code"; + + @Value("${oauth.kakao.url.auth}") + private String authUrl; + + @Value("${oauth.kakao.url.api}") + private String apiUrl; + + @Value("${oauth.kakao.client-id}") + private String clientId; + + @Value("${oauth.kakao.redirect-uri-manager}") + private String redirectUri; + + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + + @Override + public OAuthProvider oAuthProvider() { + return OAuthProvider.KAKAO; + } + + @Override + public String requestAccessToken(OAuthLoginParams params) { + String url = authUrl + "/oauth/token"; + + final HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + final MultiValueMap body = params.makeBody(); + body.add("grant_type", GRANT_TYPE); + body.add("client_id", clientId); + body.add("redirect_uri", redirectUri); + + final HttpEntity> request = new HttpEntity<>(body, headers); + + KakaoTokens response = restTemplate.postForObject(url, request, KakaoTokens.class); + + assert response != null; + return response.getAccessToken(); + } + + @Override + public OAuthInfoResponse requestOauthInfo(String accessToken) { + String url = apiUrl + "/v2/user/me"; + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + httpHeaders.setBearerAuth(accessToken); + + MultiValueMap body = new LinkedMultiValueMap<>(); +// body.add("property_keys", "[\"kakao_account.email\", \"kakao_account.profile\"]"); + + HttpEntity request = new HttpEntity<>(null, httpHeaders); +// HttpEntity request = new HttpEntity<>(body, httpHeaders); + + ResponseEntity response = restTemplate.postForEntity(url, request, String.class); + String responseBody = response.getBody(); + + try { + return objectMapper.readValue(responseBody, KakaoInfoResponse.class); + } catch (JsonProcessingException e) { + throw new StampCrushException("KakaoInfoResponse로 직렬화에 실패했습니다"); + } + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/client/ManagerOAuthApiClient.java b/backend/src/main/java/com/stampcrush/backend/auth/client/ManagerOAuthApiClient.java new file mode 100644 index 000000000..26a1c44cd --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/client/ManagerOAuthApiClient.java @@ -0,0 +1,13 @@ +package com.stampcrush.backend.auth.client; + +import com.stampcrush.backend.auth.OAuthProvider; +import com.stampcrush.backend.auth.application.util.OAuthLoginParams; + +public interface ManagerOAuthApiClient { + + OAuthProvider oAuthProvider(); + + String requestAccessToken(OAuthLoginParams params); + + OAuthInfoResponse requestOauthInfo(String accessToken); +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/client/OAuthInfoResponse.java b/backend/src/main/java/com/stampcrush/backend/auth/client/OAuthInfoResponse.java new file mode 100644 index 000000000..04b30ad22 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/client/OAuthInfoResponse.java @@ -0,0 +1,14 @@ +package com.stampcrush.backend.auth.client; + +import com.stampcrush.backend.auth.OAuthProvider; + +public interface OAuthInfoResponse { + + String getNickname(); + + String getEmail(); + + OAuthProvider getOAuthProvider(); + + Long getOAuthId(); +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/client/VisitorKakaoApiClient.java b/backend/src/main/java/com/stampcrush/backend/auth/client/VisitorKakaoApiClient.java new file mode 100644 index 000000000..849168f1e --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/client/VisitorKakaoApiClient.java @@ -0,0 +1,90 @@ +package com.stampcrush.backend.auth.client; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stampcrush.backend.auth.OAuthProvider; +import com.stampcrush.backend.auth.application.util.OAuthLoginParams; +import com.stampcrush.backend.exception.StampCrushException; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +@Profile("!test") +@RequiredArgsConstructor +@Component +public class VisitorKakaoApiClient implements VisitorOAuthApiClient { + + private static final String GRANT_TYPE = "authorization_code"; + + @Value("${oauth.kakao.url.auth}") + private String authUrl; + + @Value("${oauth.kakao.url.api}") + private String apiUrl; + + @Value("${oauth.kakao.client-id}") + private String clientId; + + @Value("${oauth.kakao.redirect-uri-visitor}") + private String redirectUri; + + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + + @Override + public OAuthProvider oAuthProvider() { + return OAuthProvider.KAKAO; + } + + @Override + public String requestAccessToken(OAuthLoginParams params) { + String url = authUrl + "/oauth/token"; + + final HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + final MultiValueMap body = params.makeBody(); + body.add("grant_type", GRANT_TYPE); + body.add("client_id", clientId); + body.add("redirect_uri", redirectUri); + + final HttpEntity> request = new HttpEntity<>(body, headers); + + KakaoTokens response = restTemplate.postForObject(url, request, KakaoTokens.class); + + assert response != null; + return response.getAccessToken(); + } + + @Override + public OAuthInfoResponse requestOauthInfo(String accessToken) { + String url = apiUrl + "/v2/user/me"; + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + httpHeaders.setBearerAuth(accessToken); + + MultiValueMap body = new LinkedMultiValueMap<>(); +// body.add("property_keys", "[\"kakao_account.email\", \"kakao_account.profile\"]"); + + HttpEntity request = new HttpEntity<>(null, httpHeaders); +// HttpEntity request = new HttpEntity<>(body, httpHeaders); + + ResponseEntity response = restTemplate.postForEntity(url, request, String.class); + String responseBody = response.getBody(); + + try { + return objectMapper.readValue(responseBody, KakaoInfoResponse.class); + } catch (JsonProcessingException e) { + throw new StampCrushException("KakaoInfoResponse로 직렬화에 실패했습니다"); + } + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/client/VisitorOAuthApiClient.java b/backend/src/main/java/com/stampcrush/backend/auth/client/VisitorOAuthApiClient.java new file mode 100644 index 000000000..9585ad1e3 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/client/VisitorOAuthApiClient.java @@ -0,0 +1,13 @@ +package com.stampcrush.backend.auth.client; + +import com.stampcrush.backend.auth.OAuthProvider; +import com.stampcrush.backend.auth.application.util.OAuthLoginParams; + +public interface VisitorOAuthApiClient { + + OAuthProvider oAuthProvider(); + + String requestAccessToken(OAuthLoginParams params); + + OAuthInfoResponse requestOauthInfo(String accessToken); +} diff --git a/backend/src/main/java/com/stampcrush/backend/auth/request/OAuthRegisterCustomerCreateRequest.java b/backend/src/main/java/com/stampcrush/backend/auth/request/OAuthRegisterCustomerCreateRequest.java new file mode 100644 index 000000000..dc664ed56 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/auth/request/OAuthRegisterCustomerCreateRequest.java @@ -0,0 +1,15 @@ +package com.stampcrush.backend.auth.request; + +import com.stampcrush.backend.auth.OAuthProvider; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class OAuthRegisterCustomerCreateRequest { + + private final String nickname; + private final String email; + private final OAuthProvider oAuthProvider; + private final Long oAuthId; +} diff --git a/backend/src/main/java/com/stampcrush/backend/config/LogFormat.java b/backend/src/main/java/com/stampcrush/backend/config/LogFormat.java new file mode 100644 index 000000000..26dba9c2c --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/config/LogFormat.java @@ -0,0 +1,12 @@ +package com.stampcrush.backend.config; + +public final class LogFormat { + + public static final String OWNER_UNAUTHORIZATION_LOG_FORMAT = "사장 아이디: {}, 인증에 실패했습니다."; + public static final String CUSTOMER_UNAUTHORIZATION_LOG_FORMAT = "고객 아이디: {}, 인증에 실패했습니다."; + public static final String NOT_FOUND_LOG_FORMAT = "자원: {}, 아이디: {}, 존재하지 않습니다."; + public static final String TEMPORARY_USER_LOG_FORMAT = "임시 유저 아이디: {}, {}"; + + private LogFormat() { + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/config/OAuthClientConfig.java b/backend/src/main/java/com/stampcrush/backend/config/OAuthClientConfig.java new file mode 100644 index 000000000..75c91ca11 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/config/OAuthClientConfig.java @@ -0,0 +1,14 @@ +package com.stampcrush.backend.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class OAuthClientConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/config/PersistenceConfig.java b/backend/src/main/java/com/stampcrush/backend/config/PersistenceConfig.java new file mode 100644 index 000000000..780200be5 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/config/PersistenceConfig.java @@ -0,0 +1,9 @@ +package com.stampcrush.backend.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration(proxyBeanMethods = false) +@EnableJpaAuditing +public class PersistenceConfig { +} diff --git a/backend/src/main/java/com/stampcrush/backend/config/WebMvcConfig.java b/backend/src/main/java/com/stampcrush/backend/config/WebMvcConfig.java new file mode 100644 index 000000000..be3004d13 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/config/WebMvcConfig.java @@ -0,0 +1,44 @@ +package com.stampcrush.backend.config; + +import com.stampcrush.backend.auth.application.util.AuthTokensGenerator; +import com.stampcrush.backend.config.interceptor.BasicAuthInterceptor; +import com.stampcrush.backend.config.resolver.CustomerArgumentResolver; +import com.stampcrush.backend.config.resolver.OwnerArgumentResolver; +import com.stampcrush.backend.repository.user.OwnerRepository; +import com.stampcrush.backend.repository.user.RegisterCustomerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@RequiredArgsConstructor +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + private final BasicAuthInterceptor basicAuthInterceptor; + private final OwnerRepository ownerRepository; + private final RegisterCustomerRepository registerCustomerRepository; + private final AuthTokensGenerator authTokensGenerator; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(basicAuthInterceptor) + .addPathPatterns("/api/**") + .excludePathPatterns( + "/api/swagger-ui/**", + "/api/docs/**", + "/api/admin/login/**", + "/api/login/**", + "/api/admin/images" + ); + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new OwnerArgumentResolver(ownerRepository, authTokensGenerator)); + resolvers.add(new CustomerArgumentResolver(registerCustomerRepository, authTokensGenerator)); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/config/interceptor/BasicAuthInterceptor.java b/backend/src/main/java/com/stampcrush/backend/config/interceptor/BasicAuthInterceptor.java new file mode 100644 index 000000000..7d1663a85 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/config/interceptor/BasicAuthInterceptor.java @@ -0,0 +1,46 @@ +package com.stampcrush.backend.config.interceptor; + +import com.stampcrush.backend.exception.UnAuthorizationException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +public class BasicAuthInterceptor implements HandlerInterceptor { + + private static final String BASIC_AUTHORIZATION_HEADER = "basic"; + private static final String TOKEN_AUTHORIZATION_HEADER = "bearer"; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String requestPath = request.getRequestURI(); + + //TODO: 나중에 삭제해야함. + if (requestPath.startsWith("/admin/login/auth/") || requestPath.startsWith("/api/admin/login/kakao") + || requestPath.startsWith("/api/login/kakao") || requestPath.startsWith("/login/auth/kakao")) { + return true; + } + + String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + + boolean isAuthorizationNull = authorization == null; + if (isAuthorizationNull) { + throw new UnAuthorizationException("인증을 할 수 없습니다"); + } + + if (!isValidAuthorizationHeaderType(authorization)) { + throw new UnAuthorizationException("인증을 할 수 없습니다"); + } + + return true; + } + + private boolean isValidAuthorizationHeaderType(String authorization) { + boolean isBasicAuthorization = authorization.toLowerCase().startsWith(BASIC_AUTHORIZATION_HEADER); + boolean isTokenAuthorization = authorization.toLowerCase().startsWith(TOKEN_AUTHORIZATION_HEADER); + + return isBasicAuthorization || isTokenAuthorization; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/config/resolver/CustomerArgumentResolver.java b/backend/src/main/java/com/stampcrush/backend/config/resolver/CustomerArgumentResolver.java new file mode 100644 index 000000000..e58b22604 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/config/resolver/CustomerArgumentResolver.java @@ -0,0 +1,74 @@ +package com.stampcrush.backend.config.resolver; + +import com.stampcrush.backend.auth.application.util.AuthTokensGenerator; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.exception.CustomerUnAuthorizationException; +import com.stampcrush.backend.repository.user.RegisterCustomerRepository; +import lombok.RequiredArgsConstructor; +import org.apache.tomcat.util.codec.binary.Base64; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import javax.naming.AuthenticationException; + +@RequiredArgsConstructor +public class CustomerArgumentResolver implements HandlerMethodArgumentResolver { + + private static final String BASIC_TYPE = "Basic"; + + private static final String BEARER_TYPE = "Bearer"; + private static final String DELIMITER = ":"; + + private final RegisterCustomerRepository customerRepository; + private final AuthTokensGenerator authTokensGenerator; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(CustomerAuth.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + String authorization = webRequest.getHeader(HttpHeaders.AUTHORIZATION); + + // basic + if (authorization.startsWith(BASIC_TYPE)) { + String[] credentials = getCredentials(authorization); + + String loginId = credentials[0]; + // 비밀번호 암호화는 어디서 해야하는지 .. + String encryptedPassword = credentials[1]; + + RegisterCustomer customer = customerRepository.findByLoginId(loginId).orElseThrow(() -> new CustomerUnAuthorizationException("회원정보가 잘못되었습니다.")); + + customer.checkPassword(encryptedPassword); + + return new CustomerAuth(customer.getId()); + } + + // bearer + if (authorization.startsWith(BEARER_TYPE)) { + String jwtToken = authorization.substring(7); + Long customerId = authTokensGenerator.extractMemberId(jwtToken); + // TODO: findById 매개변수로 왜 Integer 가 들어가는지 모르겠음 + RegisterCustomer customer = customerRepository.findById(Math.toIntExact(customerId)).orElseThrow(() -> new CustomerUnAuthorizationException("회원정보가 잘못되었습니다.")); + + return new CustomerAuth(customer.getId()); + } + + throw new AuthenticationException(); + } + + private String[] getCredentials(String header) { + String authHeaderValue = header.substring(BASIC_TYPE.length()).trim(); + + byte[] decodedBytes = Base64.decodeBase64(authHeaderValue); + String decodedString = new String(decodedBytes); + + return decodedString.split(DELIMITER); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/config/resolver/CustomerAuth.java b/backend/src/main/java/com/stampcrush/backend/config/resolver/CustomerAuth.java new file mode 100644 index 000000000..9f1bcb871 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/config/resolver/CustomerAuth.java @@ -0,0 +1,11 @@ +package com.stampcrush.backend.config.resolver; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class CustomerAuth { + + private final Long id; +} diff --git a/backend/src/main/java/com/stampcrush/backend/config/resolver/OwnerArgumentResolver.java b/backend/src/main/java/com/stampcrush/backend/config/resolver/OwnerArgumentResolver.java new file mode 100644 index 000000000..6fbf62a4a --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/config/resolver/OwnerArgumentResolver.java @@ -0,0 +1,71 @@ +package com.stampcrush.backend.config.resolver; + +import com.stampcrush.backend.auth.application.util.AuthTokensGenerator; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.exception.OwnerUnAuthorizationException; +import com.stampcrush.backend.repository.user.OwnerRepository; +import lombok.RequiredArgsConstructor; +import org.apache.tomcat.util.codec.binary.Base64; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import javax.naming.AuthenticationException; + +@RequiredArgsConstructor +public class OwnerArgumentResolver implements HandlerMethodArgumentResolver { + + private static final String BASIC_TYPE = "Basic"; + private static final String BEARER_TYPE = "Bearer"; + private static final String DELIMITER = ":"; + + private final OwnerRepository ownerRepository; + private final AuthTokensGenerator authTokensGenerator; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(OwnerAuth.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + String authorization = webRequest.getHeader(HttpHeaders.AUTHORIZATION); + + // basic + if (authorization.startsWith(BASIC_TYPE)) { + String[] credentials = getCredentials(authorization); + + String loginId = credentials[0]; + // 비밀번호 암호화는 어디서 해야하는지 .. + String encryptedPassword = credentials[1]; + + Owner owner = ownerRepository.findByLoginId(loginId).orElseThrow(() -> new OwnerUnAuthorizationException("회원정보가 잘못되었습니다.")); + owner.checkPassword(encryptedPassword); + + return new OwnerAuth(owner.getId()); + } + + // bearer + if (authorization.startsWith(BEARER_TYPE)) { + String jwtToken = authorization.substring(7); + Long ownerId = authTokensGenerator.extractMemberId(jwtToken); + Owner owner = ownerRepository.findById(ownerId).orElseThrow(() -> new OwnerUnAuthorizationException("회원정보가 잘못되었습니다.")); + + return new OwnerAuth(owner.getId()); + } + + throw new AuthenticationException(); + } + + private String[] getCredentials(String header) { + String authHeaderValue = header.substring(BASIC_TYPE.length()).trim(); + + byte[] decodedBytes = Base64.decodeBase64(authHeaderValue); + String decodedString = new String(decodedBytes); + + return decodedString.split(DELIMITER); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/config/resolver/OwnerAuth.java b/backend/src/main/java/com/stampcrush/backend/config/resolver/OwnerAuth.java new file mode 100644 index 000000000..5df5ad075 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/config/resolver/OwnerAuth.java @@ -0,0 +1,11 @@ +package com.stampcrush.backend.config.resolver; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class OwnerAuth { + + private final Long id; +} diff --git a/backend/src/main/java/com/stampcrush/backend/config/s3/S3Config.java b/backend/src/main/java/com/stampcrush/backend/config/s3/S3Config.java new file mode 100644 index 000000000..fe2d7b7d1 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/config/s3/S3Config.java @@ -0,0 +1,18 @@ +package com.stampcrush.backend.config.s3; + +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + + @Bean + public AmazonS3 amazonS3() { + return AmazonS3ClientBuilder.standard() + .withRegion(Regions.AP_NORTHEAST_2) + .build(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/baseentity/BaseDate.java b/backend/src/main/java/com/stampcrush/backend/entity/baseentity/BaseDate.java new file mode 100644 index 000000000..6bc399d23 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/baseentity/BaseDate.java @@ -0,0 +1,31 @@ +package com.stampcrush.backend.entity.baseentity; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseDate { + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; + + public BaseDate(LocalDateTime createdAt, LocalDateTime updatedAt) { + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public BaseDate() { + this(null, null); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/cafe/Cafe.java b/backend/src/main/java/com/stampcrush/backend/entity/cafe/Cafe.java new file mode 100644 index 000000000..a5854a96e --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/cafe/Cafe.java @@ -0,0 +1,94 @@ +package com.stampcrush.backend.entity.cafe; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import com.stampcrush.backend.entity.user.Owner; +import jakarta.persistence.*; +import lombok.Getter; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; + +@Getter +@Entity +public class Cafe extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + private String name; + + @DateTimeFormat(pattern = "HH:mm") + private LocalTime openTime; + + @DateTimeFormat(pattern = "HH:mm") + private LocalTime closeTime; + + private String telephoneNumber; + + private String cafeImageUrl; + + @Lob + private String introduction; + + private String roadAddress; + + private String detailAddress; + + private String businessRegistrationNumber; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "owner_id") + private Owner owner; + + @OneToMany(mappedBy = "cafe") + private List policies = new ArrayList<>(); + + public Cafe(Long id, String name, LocalTime openTime, + LocalTime closeTime, String telephoneNumber, String cafeImageUrl, + String introduction, String roadAddress, String detailAddress, + String businessRegistrationNumber, Owner owner) { + this.id = id; + this.name = name; + this.openTime = openTime; + this.closeTime = closeTime; + this.telephoneNumber = telephoneNumber; + this.cafeImageUrl = cafeImageUrl; + this.introduction = introduction; + this.roadAddress = roadAddress; + this.detailAddress = detailAddress; + this.businessRegistrationNumber = businessRegistrationNumber; + this.owner = owner; + } + + public Cafe(String name, String roadAddress, String detailAddress, String businessRegistrationNumber, Owner owner) { + this(null, name, null, null, null, null, null, roadAddress, detailAddress, businessRegistrationNumber, owner); + } + + public Cafe(String name, LocalTime openTime, LocalTime closeTime, + String telephoneNumber, String cafeImageUrl, String introduction, + String roadAddress, String detailAddress, + String businessRegistrationNumber, Owner owner) { + this(null, name, openTime, closeTime, telephoneNumber, cafeImageUrl, introduction, roadAddress, detailAddress, businessRegistrationNumber, owner); + } + + protected Cafe() { + } + + public Cafe(long id, String name, String roadAddress, String detailAddress, String telephoneNumber, Owner owner) { + this(id, name, null, null, telephoneNumber, null, null, null, null, null, owner); + } + + public void updateCafeAdditionalInformation(String introduction, LocalTime openTime, LocalTime closeTime, String telephoneNumber, String cafeImageUrl) { + this.introduction = introduction; + this.openTime = openTime; + this.closeTime = closeTime; + this.telephoneNumber = telephoneNumber; + this.cafeImageUrl = cafeImageUrl; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/cafe/CafeCouponDesign.java b/backend/src/main/java/com/stampcrush/backend/entity/cafe/CafeCouponDesign.java new file mode 100644 index 000000000..16c1c1bdd --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/cafe/CafeCouponDesign.java @@ -0,0 +1,65 @@ +package com.stampcrush.backend.entity.cafe; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import com.stampcrush.backend.entity.coupon.CouponDesign; +import com.stampcrush.backend.entity.coupon.CouponStampCoordinate; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +import java.util.ArrayList; +import java.util.List; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@SQLDelete(sql = "UPDATE cafe_coupon_design SET deleted = true WHERE id = ?") +@Where(clause = "deleted = false") +@Entity +public class CafeCouponDesign extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + private String frontImageUrl; + + private String backImageUrl; + + private String stampImageUrl; + + private Boolean deleted; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "cafe_id") + private Cafe cafe; + + @OneToMany(mappedBy = "cafeCouponDesign") + private List cafeStampCoordinates = new ArrayList<>(); + + public CafeCouponDesign(String frontImageUrl, String backImageUrl, String stampImageUrl, Boolean deleted, Cafe cafe) { + this.frontImageUrl = frontImageUrl; + this.backImageUrl = backImageUrl; + this.stampImageUrl = stampImageUrl; + this.deleted = deleted; + this.cafe = cafe; + } + + public void delete() { + this.deleted = true; + } + + public CouponDesign copy() { + CouponDesign couponDesign = new CouponDesign(frontImageUrl, backImageUrl, stampImageUrl); + for (CafeStampCoordinate cafeStampCoordinate : cafeStampCoordinates) { + CouponStampCoordinate couponStampCoordinate = cafeStampCoordinate.copy(couponDesign); + couponDesign.addCouponStampCoordinate(couponStampCoordinate); + } + return couponDesign; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/cafe/CafePolicy.java b/backend/src/main/java/com/stampcrush/backend/entity/cafe/CafePolicy.java new file mode 100644 index 000000000..7eb2819ff --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/cafe/CafePolicy.java @@ -0,0 +1,61 @@ +package com.stampcrush.backend.entity.cafe; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import com.stampcrush.backend.entity.coupon.CouponPolicy; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@NoArgsConstructor(access = PROTECTED) +@Getter +@SQLDelete(sql = "UPDATE cafe_policy SET deleted = true WHERE id = ?") +@Where(clause = "deleted = false") +@Entity +public class CafePolicy extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + private Integer maxStampCount; + + private String reward; + + private Integer expirePeriod; + + private Boolean deleted; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "cafe_id") + private Cafe cafe; + + public CafePolicy(Integer maxStampCount, String reward, Integer expirePeriod, Boolean deleted, Cafe cafe) { + this.maxStampCount = maxStampCount; + this.reward = reward; + this.expirePeriod = expirePeriod; + this.deleted = deleted; + this.cafe = cafe; + } + + public static CafePolicy createDefaultCafePolicy(Cafe cafe) { + return new CafePolicy(10, "아메리카노 1잔", 6, false, cafe); + } + + public void delete() { + this.deleted = true; + } + + public CouponPolicy copy() { + return new CouponPolicy(maxStampCount, reward, expirePeriod); + } + + public int calculateRewardCouponCount(int earningStampCount) { + return earningStampCount / maxStampCount; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/cafe/CafeStampCoordinate.java b/backend/src/main/java/com/stampcrush/backend/entity/cafe/CafeStampCoordinate.java new file mode 100644 index 000000000..d281f7b6d --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/cafe/CafeStampCoordinate.java @@ -0,0 +1,43 @@ +package com.stampcrush.backend.entity.cafe; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import com.stampcrush.backend.entity.coupon.CouponDesign; +import com.stampcrush.backend.entity.coupon.CouponStampCoordinate; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class CafeStampCoordinate extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + private Integer stampOrder; + + private Integer xCoordinate; + + private Integer yCoordinate; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "cafe_coupon_design_id") + private CafeCouponDesign cafeCouponDesign; + + public CafeStampCoordinate(Integer stampOrder, Integer xCoordinate, Integer yCoordinate, CafeCouponDesign cafeCouponDesign) { + this.stampOrder = stampOrder; + this.xCoordinate = xCoordinate; + this.yCoordinate = yCoordinate; + this.cafeCouponDesign = cafeCouponDesign; + } + + public CouponStampCoordinate copy(CouponDesign couponDesign) { + return new CouponStampCoordinate(stampOrder, xCoordinate, yCoordinate, couponDesign); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/coupon/Coupon.java b/backend/src/main/java/com/stampcrush/backend/entity/coupon/Coupon.java new file mode 100644 index 000000000..342c9214e --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/coupon/Coupon.java @@ -0,0 +1,178 @@ +package com.stampcrush.backend.entity.coupon; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.cafe.CafePolicy; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.exception.CafePolicyNotFoundException; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@SQLDelete(sql = "UPDATE coupon SET deleted = true WHERE id = ?") +@Where(clause = "deleted = false") +@Entity +public class Coupon extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + private LocalDate expiredDate; + + @Enumerated(EnumType.STRING) + private CouponStatus status = CouponStatus.ACCUMULATING; + + private Boolean deleted = Boolean.FALSE; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "customer_id") + private Customer customer; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "cafe_id") + private Cafe cafe; + + @OneToOne(fetch = LAZY) + @JoinColumn(name = "coupon_design_id") + private CouponDesign couponDesign; + + @OneToOne(fetch = LAZY) + @JoinColumn(name = "coupon_policy_id") + private CouponPolicy couponPolicy; + + @OneToMany(mappedBy = "coupon", cascade = CascadeType.ALL, fetch = LAZY) + private List stamps = new ArrayList<>(); + + public Coupon(LocalDateTime createdAt, LocalDateTime updatedAt, + LocalDate expiredDate, Customer customer, + Cafe cafe, CouponDesign couponDesign, CouponPolicy couponPolicy) { + super(createdAt, updatedAt); + this.expiredDate = expiredDate; + this.customer = customer; + this.cafe = cafe; + this.couponDesign = couponDesign; + this.couponPolicy = couponPolicy; + } + + public Coupon(LocalDate expiredDate, Customer customer, Cafe cafe, CouponDesign couponDesign, CouponPolicy couponPolicy) { + this(null, null, expiredDate, customer, cafe, couponDesign, couponPolicy); + } + + public void reward() { + this.status = CouponStatus.REWARDED; + } + + public void expire() { + this.status = CouponStatus.EXPIRED; + } + + public boolean isAccumulating() { + return this.status == CouponStatus.ACCUMULATING; + } + + public boolean isRewarded() { + return this.status == CouponStatus.REWARDED; + } + + public int getStampCount() { + return stamps.size(); + } + + public int calculateVisitCount() { + return stamps.stream() + .map(BaseDate::getCreatedAt) + .map(date -> LocalDateTime.of(date.getYear(), date.getMonth(), date.getDayOfMonth(), date.getHour(), date.getMinute())) + .collect(Collectors.toSet()) + .size(); + } + + public LocalDateTime compareCreatedAtAndReturnEarlier(LocalDateTime visitTime) { + if (this.getCreatedAt().isBefore(visitTime)) { + return this.getCreatedAt(); + } + return visitTime; + } + + public LocalDateTime calculateExpireDate() { + return this.getCreatedAt().plusMonths(this.couponPolicy.getExpiredPeriod()); + } + + public boolean isNotAccessible(Customer customer, Cafe cafe) { + return !this.customer.equals(customer) || !this.cafe.equals(cafe); + } + + public void accumulate(int earningStampCount) { + for (int i = 0; i < earningStampCount; i++) { + Stamp stamp = new Stamp(); + stamp.registerCoupon(this); + } + if (couponPolicy.isSameMaxStampCount(stamps.size())) { + status = CouponStatus.REWARDED; + } + } + + public void accumulateMaxStamp() { + for (int i = 0; i < couponPolicy.getMaxStampCount(); i++) { + Stamp stamp = new Stamp(); + stamp.registerCoupon(this); + } + status = CouponStatus.REWARDED; + } + + public int getCouponMaxStampCount() { + return couponPolicy.getMaxStampCount(); + } + + public int calculateMaxStampCountWhenAccumulating() { + if (status == CouponStatus.ACCUMULATING) { + return couponPolicy.getMaxStampCount(); + } + return 0; + } + + public boolean isLessThanMaxStampAfterAccumulateStamp(int earningStampCount) { + return this.stamps.size() + earningStampCount < couponPolicy.getMaxStampCount(); + } + + public boolean isSameMaxStampAfterAccumulateStamp(int earningStampCount) { + return this.stamps.size() + earningStampCount == couponPolicy.getMaxStampCount(); + } + + public int calculateRestStampCountForReward() { + return couponPolicy.getMaxStampCount() - this.stamps.size(); + } + + public String getRewardName() { + return couponPolicy.getRewardName(); + } + + public boolean isPrevious() { + CafePolicy currentCafePolicy = findCurrentCafePolicy(); + return couponPolicy.isPrevious(currentCafePolicy); + } + + private CafePolicy findCurrentCafePolicy() { + List policies = cafe.getPolicies(); + + if (policies.isEmpty()) { + throw new CafePolicyNotFoundException("해당하는 카페의 정책이 존재하지 않습니다."); + } + + return policies.get(0); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/coupon/CouponDesign.java b/backend/src/main/java/com/stampcrush/backend/entity/coupon/CouponDesign.java new file mode 100644 index 000000000..141e16168 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/coupon/CouponDesign.java @@ -0,0 +1,45 @@ +package com.stampcrush.backend.entity.coupon; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +import static jakarta.persistence.CascadeType.ALL; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class CouponDesign extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + private String frontImageUrl; + + private String backImageUrl; + + private String stampImageUrl; + + @OneToMany(mappedBy = "couponDesign", cascade = ALL) + private List couponStampCoordinates = new ArrayList<>(); + + public CouponDesign(String frontImageUrl, String backImageUrl, String stampImageUrl) { + this.frontImageUrl = frontImageUrl; + this.backImageUrl = backImageUrl; + this.stampImageUrl = stampImageUrl; + } + + public void addCouponStampCoordinate(CouponStampCoordinate couponStampCoordinate) { + couponStampCoordinates.add(couponStampCoordinate); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/coupon/CouponPolicy.java b/backend/src/main/java/com/stampcrush/backend/entity/coupon/CouponPolicy.java new file mode 100644 index 000000000..63fbce740 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/coupon/CouponPolicy.java @@ -0,0 +1,46 @@ +package com.stampcrush.backend.entity.coupon; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import com.stampcrush.backend.entity.cafe.CafePolicy; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class CouponPolicy extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + private Integer maxStampCount; + + private String rewardName; + + private Integer expiredPeriod; + + public CouponPolicy(Integer maxStampCount, String rewardName, Integer expiredPeriod) { + this.maxStampCount = maxStampCount; + this.rewardName = rewardName; + this.expiredPeriod = expiredPeriod; + } + + public boolean isSameMaxStampCount(int stampCount) { + return stampCount == maxStampCount; + } + + public boolean isPrevious(CafePolicy currentCafePolicy) { + boolean isSameMaxStampCount = this.maxStampCount.equals(currentCafePolicy.getMaxStampCount()); + boolean isSameReward = this.rewardName.equals(currentCafePolicy.getReward()); + boolean isSameExpirePeriod = this.expiredPeriod.equals(currentCafePolicy.getExpirePeriod()); + + return !(isSameMaxStampCount && isSameReward && isSameExpirePeriod); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/coupon/CouponStampCoordinate.java b/backend/src/main/java/com/stampcrush/backend/entity/coupon/CouponStampCoordinate.java new file mode 100644 index 000000000..d114e2e04 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/coupon/CouponStampCoordinate.java @@ -0,0 +1,37 @@ +package com.stampcrush.backend.entity.coupon; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class CouponStampCoordinate extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + private Integer stampOrder; + + private Integer xCoordinate; + + private Integer yCoordinate; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "coupon_design_id") + private CouponDesign couponDesign; + + public CouponStampCoordinate(Integer stampOrder, Integer xCoordinate, Integer yCoordinate, CouponDesign couponDesign) { + this.stampOrder = stampOrder; + this.xCoordinate = xCoordinate; + this.yCoordinate = yCoordinate; + this.couponDesign = couponDesign; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/coupon/CouponStatus.java b/backend/src/main/java/com/stampcrush/backend/entity/coupon/CouponStatus.java new file mode 100644 index 000000000..2943229ee --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/coupon/CouponStatus.java @@ -0,0 +1,5 @@ +package com.stampcrush.backend.entity.coupon; + +public enum CouponStatus { + ACCUMULATING, EXPIRED, REWARDED; +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/coupon/Coupons.java b/backend/src/main/java/com/stampcrush/backend/entity/coupon/Coupons.java new file mode 100644 index 000000000..f6e65a655 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/coupon/Coupons.java @@ -0,0 +1,39 @@ +package com.stampcrush.backend.entity.coupon; + +import com.stampcrush.backend.application.manager.coupon.CustomerCouponStatistics; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@RequiredArgsConstructor +public class Coupons { + + private final List coupons; + + public CustomerCouponStatistics calculateStatistics() { + int stampCount = 0; + int rewardCount = 0; + int maxStampCount = 0; + + for (Coupon coupon : coupons) { + stampCount = calculateCurrentStampWhenUsingCoupon(stampCount, coupon); + rewardCount += addRewardCouponCount(coupon); + maxStampCount = coupon.calculateMaxStampCountWhenAccumulating(); + } + return new CustomerCouponStatistics(stampCount, rewardCount, maxStampCount); + } + + private int calculateCurrentStampWhenUsingCoupon(int stampCount, Coupon coupon) { + if (coupon.isAccumulating()) { + return coupon.getStampCount(); + } + return stampCount; + } + + private int addRewardCouponCount(Coupon coupon) { + if (coupon.isRewarded()) { + return 1; + } + return 0; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/coupon/Stamp.java b/backend/src/main/java/com/stampcrush/backend/entity/coupon/Stamp.java new file mode 100644 index 000000000..718280b22 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/coupon/Stamp.java @@ -0,0 +1,26 @@ +package com.stampcrush.backend.entity.coupon; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import jakarta.persistence.*; +import lombok.Getter; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; + +@Getter +@Entity +public class Stamp extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "coupon_id") + private Coupon coupon; + + public void registerCoupon(Coupon coupon) { + this.coupon = coupon; + coupon.getStamps().add(this); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/favorites/Favorites.java b/backend/src/main/java/com/stampcrush/backend/entity/favorites/Favorites.java new file mode 100644 index 000000000..d6d63728f --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/favorites/Favorites.java @@ -0,0 +1,47 @@ +package com.stampcrush.backend.entity.favorites; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.user.Customer; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class Favorites extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "cafe_id") + private Cafe cafe; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "customer_id") + private Customer customer; + + private Boolean isFavorites = false; + + public Favorites(Cafe cafe, Customer customer) { + this.cafe = cafe; + this.customer = customer; + } + + public Favorites(Cafe cafe, Customer customer, Boolean isFavorites) { + this.cafe = cafe; + this.customer = customer; + this.isFavorites = isFavorites; + } + + public void changeFavorites(boolean isFavorites) { + this.isFavorites = isFavorites; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/image/ImageUploader.java b/backend/src/main/java/com/stampcrush/backend/entity/image/ImageUploader.java new file mode 100644 index 000000000..a7f6121bd --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/image/ImageUploader.java @@ -0,0 +1,8 @@ +package com.stampcrush.backend.entity.image; + +import org.springframework.web.multipart.MultipartFile; + +public interface ImageUploader { + + String upload(MultipartFile image); +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/image/S3ImageUploader.java b/backend/src/main/java/com/stampcrush/backend/entity/image/S3ImageUploader.java new file mode 100644 index 000000000..9c10572e7 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/image/S3ImageUploader.java @@ -0,0 +1,59 @@ +package com.stampcrush.backend.entity.image; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.stampcrush.backend.exception.ImageUploadFailException; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@RequiredArgsConstructor +@Component +public class S3ImageUploader implements ImageUploader { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss-SSSSSS"); + private static final String EXTENSION_DELIMITER = "."; + private static final String DIRECTORY_DELIMITER = "/"; + + private final AmazonS3 s3; + + @Value("${s3.bucket}") + private String bucket; + + @Value("${s3.base-url}") + private String BASE_URL; + + @Value("${s3.dir}") + private String DIRECTORY; + + @Override + public String upload(MultipartFile image) { + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType(image.getContentType()); + objectMetadata.setContentLength(image.getSize()); + String formattedFileName = formatFileName(image.getOriginalFilename()); + try { + s3.putObject( + new PutObjectRequest( + bucket, + DIRECTORY + DIRECTORY_DELIMITER + formattedFileName, + image.getInputStream(), + objectMetadata + ) + ); + } catch (IOException exception) { + throw new ImageUploadFailException("이미지 저장실패 + " + exception.getMessage()); + } + return BASE_URL + DIRECTORY + DIRECTORY_DELIMITER + formattedFileName; + } + + private String formatFileName(String originalFileName) { + return FORMATTER.format(LocalDateTime.now()) + originalFileName.substring(originalFileName.lastIndexOf(EXTENSION_DELIMITER)); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/reward/Reward.java b/backend/src/main/java/com/stampcrush/backend/entity/reward/Reward.java new file mode 100644 index 000000000..7798f6885 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/reward/Reward.java @@ -0,0 +1,89 @@ +package com.stampcrush.backend.entity.reward; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.user.Customer; +import jakarta.persistence.*; +import lombok.Getter; + +import java.time.LocalDateTime; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; + +@Getter +@Entity +public class Reward extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + private String name; + + private Boolean used = false; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "customer_id") + private Customer customer; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "cafe_id") + private Cafe cafe; + + public Reward( + LocalDateTime createdAt, + LocalDateTime updatedAt, + Long id, + String name, + Boolean used, + Customer customer, + Cafe cafe + ) { + super(createdAt, updatedAt); + this.id = id; + this.name = name; + this.used = used; + this.customer = customer; + this.cafe = cafe; + } + + public Reward(Long id, String name, Boolean used, Customer customer, Cafe cafe) { + this(null, null, id, name, used, customer, cafe); + } + + public Reward(String name, Customer customer, Cafe cafe) { + this(null, name, false, customer, cafe); + } + + protected Reward() { + } + + public void useReward(Customer customer, Cafe cafe) { + if (used) { + throw new IllegalArgumentException("이미 사용된 리워드 입니다."); + } + if (isTemporaryCustomer()) { + throw new IllegalArgumentException("임시회원은 리워드를 사용할 수 없습니다."); + } + if (isNotPublisher(cafe)) { + throw new IllegalArgumentException("해당 카페에서 발행된 리워드가 아닙니다."); + } + if (isNotOwner(customer)) { + throw new IllegalArgumentException("해당 리워드의 소유자가 아닙니다."); + } + used = true; + } + + private boolean isTemporaryCustomer() { + return !customer.isRegistered(); + } + + private boolean isNotPublisher(Cafe cafe) { + return !cafe.equals(this.cafe); + } + + private boolean isNotOwner(Customer customer) { + return !customer.equals(this.customer); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/sample/SampleBackImage.java b/backend/src/main/java/com/stampcrush/backend/entity/sample/SampleBackImage.java new file mode 100644 index 000000000..b9273bc9c --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/sample/SampleBackImage.java @@ -0,0 +1,39 @@ +package com.stampcrush.backend.entity.sample; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class SampleBackImage extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + private String imageUrl; + + @OneToMany(mappedBy = "sampleBackImage") + private List sampleStampCoordinates = new ArrayList<>(); + + public SampleBackImage(String imageUrl) { + this.imageUrl = imageUrl; + } + + public SampleBackImage(Long id, String imageUrl) { + this.id = id; + this.imageUrl = imageUrl; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/sample/SampleFrontImage.java b/backend/src/main/java/com/stampcrush/backend/entity/sample/SampleFrontImage.java new file mode 100644 index 000000000..4c25ecd3b --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/sample/SampleFrontImage.java @@ -0,0 +1,32 @@ +package com.stampcrush.backend.entity.sample; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.Getter; + +import static jakarta.persistence.GenerationType.IDENTITY; + +@Getter +@Entity +public class SampleFrontImage extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + private String imageUrl; + + public SampleFrontImage(String imageUrl) { + this.imageUrl = imageUrl; + } + + public SampleFrontImage(Long id, String imageUrl) { + this.id = id; + this.imageUrl = imageUrl; + } + + protected SampleFrontImage() { + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/sample/SampleStampCoordinate.java b/backend/src/main/java/com/stampcrush/backend/entity/sample/SampleStampCoordinate.java new file mode 100644 index 000000000..57aa91c6b --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/sample/SampleStampCoordinate.java @@ -0,0 +1,45 @@ +package com.stampcrush.backend.entity.sample; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import jakarta.persistence.*; +import lombok.Getter; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; + +@Getter +@Entity +public class SampleStampCoordinate extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + private Integer stampOrder; + + private Integer xCoordinate; + + private Integer yCoordinate; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "sample_back_image_id") + private SampleBackImage sampleBackImage; + + public SampleStampCoordinate(Integer stampOrder, Integer xCoordinate, Integer yCoordinate, SampleBackImage sampleBackImage) { + this.stampOrder = stampOrder; + this.xCoordinate = xCoordinate; + this.yCoordinate = yCoordinate; + this.sampleBackImage = sampleBackImage; + } + + public SampleStampCoordinate(Integer stampOrder, Integer xCoordinate, Integer yCoordinate) { + this(stampOrder, xCoordinate, yCoordinate, null); + } + + public void setSampleBackImage(SampleBackImage sampleBackImage) { + this.sampleBackImage = sampleBackImage; + } + + protected SampleStampCoordinate() { + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/sample/SampleStampImage.java b/backend/src/main/java/com/stampcrush/backend/entity/sample/SampleStampImage.java new file mode 100644 index 000000000..f58b55355 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/sample/SampleStampImage.java @@ -0,0 +1,32 @@ +package com.stampcrush.backend.entity.sample; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.Getter; + +import static jakarta.persistence.GenerationType.IDENTITY; + +@Getter +@Entity +public class SampleStampImage extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + private String imageUrl; + + public SampleStampImage(String imageUrl) { + this.imageUrl = imageUrl; + } + + public SampleStampImage(Long id, String imageUrl) { + this.id = id; + this.imageUrl = imageUrl; + } + + protected SampleStampImage() { + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/user/Customer.java b/backend/src/main/java/com/stampcrush/backend/entity/user/Customer.java new file mode 100644 index 000000000..6d0b1c078 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/user/Customer.java @@ -0,0 +1,38 @@ +package com.stampcrush.backend.entity.user; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static jakarta.persistence.InheritanceType.JOINED; +import static lombok.AccessLevel.PROTECTED; + +@AllArgsConstructor +@NoArgsConstructor(access = PROTECTED) +@Getter +@Entity +@DiscriminatorColumn(name = "dtype") +@Inheritance(strategy = JOINED) +public abstract class Customer { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "customer_id") + private Long id; + private String nickname; + + @Column(unique = true) + private String phoneNumber; + + public Customer(String nickname, String phoneNumber) { + this(null, nickname, phoneNumber); + } + + public void registerPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public abstract boolean isRegistered(); +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/user/Owner.java b/backend/src/main/java/com/stampcrush/backend/entity/user/Owner.java new file mode 100644 index 000000000..512499ae9 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/user/Owner.java @@ -0,0 +1,70 @@ +package com.stampcrush.backend.entity.user; + +import com.stampcrush.backend.auth.OAuthProvider; +import com.stampcrush.backend.entity.baseentity.BaseDate; +import com.stampcrush.backend.exception.OwnerUnAuthorizationException; +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@NoArgsConstructor(access = PROTECTED) +@Getter +@Entity +public class Owner extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @Column(name = "name") + private String nickname; + private String loginId; + private String encryptedPassword; + private String phoneNumber; + private String email; + + @Enumerated(EnumType.STRING) + @Column(name = "oauth_provider") + private OAuthProvider oAuthProvider; + + @Column(name = "oauth_id") + private Long oAuthId; + + public Owner(Long id, String nickname, String loginId, String encryptedPassword, String phoneNumber) { + this.id = id; + this.nickname = nickname; + this.loginId = loginId; + this.encryptedPassword = encryptedPassword; + this.phoneNumber = phoneNumber; + } + + public Owner(String nickname, String loginId, String encryptedPassword, String phoneNumber) { + this.nickname = nickname; + this.loginId = loginId; + this.encryptedPassword = encryptedPassword; + this.phoneNumber = phoneNumber; + } + + @Builder + public Owner( + String nickname, + String email, + OAuthProvider oAuthProvider, + Long oAuthId + ) { + this.nickname = nickname; + this.email = email; + this.oAuthProvider = oAuthProvider; + this.oAuthId = oAuthId; + } + + public void checkPassword(String encryptedPassword) { + if (!this.encryptedPassword.equals(encryptedPassword)) { + throw new OwnerUnAuthorizationException("아이디와 패스워드를 다시 확인 후 로그인해주세요."); + } + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/user/RegisterCustomer.java b/backend/src/main/java/com/stampcrush/backend/entity/user/RegisterCustomer.java new file mode 100644 index 000000000..69aace179 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/user/RegisterCustomer.java @@ -0,0 +1,73 @@ +package com.stampcrush.backend.entity.user; + +import com.stampcrush.backend.auth.OAuthProvider; +import com.stampcrush.backend.exception.CustomerUnAuthorizationException; +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static lombok.AccessLevel.PROTECTED; + +@NoArgsConstructor(access = PROTECTED) +@Getter +@DiscriminatorValue("register") +@Entity +public class RegisterCustomer extends Customer { + + private String loginId; + private String encryptedPassword; + + private String email; + + @Enumerated(EnumType.STRING) + @Column(name = "oauth_provider") + private OAuthProvider oAuthProvider; + + @Column(name = "oauth_id") + private Long oAuthId; + + public RegisterCustomer(String nickname, String phoneNumber, String loginId, String encryptedPassword) { + super(nickname, phoneNumber); + this.loginId = loginId; + this.encryptedPassword = encryptedPassword; + } + + public RegisterCustomer(Long id, String nickname, String phoneNumber, String loginId, String encryptedPassword) { + super(id, nickname, phoneNumber); + this.loginId = loginId; + this.encryptedPassword = encryptedPassword; + } + + @Builder + public RegisterCustomer( + String nickname, + String email, + OAuthProvider oAuthProvider, + Long oAuthId + ) { + super(nickname, null); + this.email = email; + this.oAuthProvider = oAuthProvider; + this.oAuthId = oAuthId; + } + + @Override + public boolean isRegistered() { + return true; + } + + public void checkPassword(String encryptedPassword) { + if (!this.encryptedPassword.equals(encryptedPassword)) { + throw new CustomerUnAuthorizationException("아이디와 패스워드를 다시 확인 후 로그인해주세요."); + } + } + + public void registerLoginId(String loginId) { + this.loginId = loginId; + } + + public void registerEncryptedPassword(String encryptedPassword) { + this.encryptedPassword = encryptedPassword; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/user/TemporaryCustomer.java b/backend/src/main/java/com/stampcrush/backend/entity/user/TemporaryCustomer.java new file mode 100644 index 000000000..66d09d80e --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/user/TemporaryCustomer.java @@ -0,0 +1,40 @@ +package com.stampcrush.backend.entity.user; + +import com.stampcrush.backend.exception.CustomerBadRequestException; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import lombok.NoArgsConstructor; + +import static lombok.AccessLevel.PROTECTED; + +@NoArgsConstructor(access = PROTECTED) +@DiscriminatorValue("temporary") +@Entity +public class TemporaryCustomer extends Customer { + + private static final int NICKNAME_LENGTH = 4; + + private TemporaryCustomer(String nickname, String phoneNumber) { + super(nickname, phoneNumber); + } + + public TemporaryCustomer(Long id, String nickname, String phoneNumber) { + super(id, nickname, phoneNumber); + } + + public static TemporaryCustomer from(String phoneNumber) { + return new TemporaryCustomer(formatNickname(phoneNumber), phoneNumber); + } + + private static String formatNickname(String phoneNumber) { + if (phoneNumber.length() < NICKNAME_LENGTH) { + throw new CustomerBadRequestException("임시 닉네임을 사용하려면 4글자 이상의 전화번호가 필요합니다"); + } + return phoneNumber.substring(phoneNumber.length() - NICKNAME_LENGTH); + } + + @Override + public boolean isRegistered() { + return false; + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/visithistory/VisitHistories.java b/backend/src/main/java/com/stampcrush/backend/entity/visithistory/VisitHistories.java new file mode 100644 index 000000000..364cc8de1 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/visithistory/VisitHistories.java @@ -0,0 +1,29 @@ +package com.stampcrush.backend.entity.visithistory; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.List; + +@RequiredArgsConstructor +public class VisitHistories { + + private final List visitHistories; + + public int getVisitCount() { + return visitHistories.size(); + } + + public LocalDateTime getFirstVisitDate() { + if (visitHistories.isEmpty()) { + return LocalDateTime.now(); + } + VisitHistory firstVisit = visitHistories.stream() + .min(Comparator.comparing(BaseDate::getCreatedAt)) + .get(); + + return firstVisit.getCreatedAt(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/entity/visithistory/VisitHistory.java b/backend/src/main/java/com/stampcrush/backend/entity/visithistory/VisitHistory.java new file mode 100644 index 000000000..3ffb8007a --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/entity/visithistory/VisitHistory.java @@ -0,0 +1,51 @@ +package com.stampcrush.backend.entity.visithistory; + +import com.stampcrush.backend.entity.baseentity.BaseDate; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.user.Customer; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +import static jakarta.persistence.FetchType.LAZY; +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +@AllArgsConstructor +@NoArgsConstructor(access = PROTECTED) +@Getter +@Entity +public class VisitHistory extends BaseDate { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "cafe_id") + private Cafe cafe; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "customer_id") + private Customer customer; + + private int stampCount; + + public VisitHistory(LocalDateTime createdAt, LocalDateTime updatedAt, Cafe cafe, Customer customer, int stampCount) { + super(createdAt, updatedAt); + this.cafe = cafe; + this.customer = customer; + this.stampCount = stampCount; + } + + public VisitHistory(Cafe cafe, Customer customer, int stampCount) { + this(null, cafe, customer, stampCount); + } + + public String getCafeName() { + return cafe.getName(); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/exception/BadRequestException.java b/backend/src/main/java/com/stampcrush/backend/exception/BadRequestException.java new file mode 100644 index 000000000..1d36c8128 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/exception/BadRequestException.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.exception; + +public class BadRequestException extends StampCrushException { + + public BadRequestException(String message) { + super(message); + } + + public BadRequestException(Throwable cause) { + super(cause); + } + + public BadRequestException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/exception/CafeCouponSettingNotFoundException.java b/backend/src/main/java/com/stampcrush/backend/exception/CafeCouponSettingNotFoundException.java new file mode 100644 index 000000000..32f63772d --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/exception/CafeCouponSettingNotFoundException.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.exception; + +public class CafeCouponSettingNotFoundException extends NotFoundException { + + public CafeCouponSettingNotFoundException(String message) { + super(message); + } + + public CafeCouponSettingNotFoundException(Throwable cause) { + super(cause); + } + + public CafeCouponSettingNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/exception/CafeNotFoundException.java b/backend/src/main/java/com/stampcrush/backend/exception/CafeNotFoundException.java new file mode 100644 index 000000000..3ed8245fc --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/exception/CafeNotFoundException.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.exception; + +public class CafeNotFoundException extends NotFoundException { + + public CafeNotFoundException(String message) { + super(message); + } + + public CafeNotFoundException(Throwable cause) { + super(cause); + } + + public CafeNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/exception/CafePolicyNotFoundException.java b/backend/src/main/java/com/stampcrush/backend/exception/CafePolicyNotFoundException.java new file mode 100644 index 000000000..65d7443e5 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/exception/CafePolicyNotFoundException.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.exception; + +public class CafePolicyNotFoundException extends NotFoundException { + + public CafePolicyNotFoundException(String message) { + super(message); + } + + public CafePolicyNotFoundException(Throwable cause) { + super(cause); + } + + public CafePolicyNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/exception/CouponNotFoundException.java b/backend/src/main/java/com/stampcrush/backend/exception/CouponNotFoundException.java new file mode 100644 index 000000000..f701be6ce --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/exception/CouponNotFoundException.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.exception; + +public class CouponNotFoundException extends NotFoundException { + + public CouponNotFoundException(String message) { + super(message); + } + + public CouponNotFoundException(Throwable cause) { + super(cause); + } + + public CouponNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/exception/CustomerBadRequestException.java b/backend/src/main/java/com/stampcrush/backend/exception/CustomerBadRequestException.java new file mode 100644 index 000000000..ceaa3a162 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/exception/CustomerBadRequestException.java @@ -0,0 +1,15 @@ +package com.stampcrush.backend.exception; + +public class CustomerBadRequestException extends BadRequestException { + public CustomerBadRequestException(String message) { + super(message); + } + + public CustomerBadRequestException(Throwable cause) { + super(cause); + } + + public CustomerBadRequestException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/exception/CustomerNotFoundException.java b/backend/src/main/java/com/stampcrush/backend/exception/CustomerNotFoundException.java new file mode 100644 index 000000000..d5e7f480d --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/exception/CustomerNotFoundException.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.exception; + +public class CustomerNotFoundException extends NotFoundException { + + public CustomerNotFoundException(String message) { + super(message); + } + + public CustomerNotFoundException(Throwable cause) { + super(cause); + } + + public CustomerNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/exception/CustomerUnAuthorizationException.java b/backend/src/main/java/com/stampcrush/backend/exception/CustomerUnAuthorizationException.java new file mode 100644 index 000000000..347033bb4 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/exception/CustomerUnAuthorizationException.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.exception; + +public class CustomerUnAuthorizationException extends UnAuthorizationException { + + public CustomerUnAuthorizationException(String message) { + super(message); + } + + public CustomerUnAuthorizationException(Throwable cause) { + super(cause); + } + + public CustomerUnAuthorizationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/exception/ForbiddenException.java b/backend/src/main/java/com/stampcrush/backend/exception/ForbiddenException.java new file mode 100644 index 000000000..8db47c091 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/exception/ForbiddenException.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.exception; + +public class ForbiddenException extends StampCrushException { + + public ForbiddenException(String message) { + super(message); + } + + public ForbiddenException(Throwable cause) { + super(cause); + } + + public ForbiddenException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/exception/ImageUploadFailException.java b/backend/src/main/java/com/stampcrush/backend/exception/ImageUploadFailException.java new file mode 100644 index 000000000..c336dbe4e --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/exception/ImageUploadFailException.java @@ -0,0 +1,15 @@ +package com.stampcrush.backend.exception; + +public class ImageUploadFailException extends StampCrushException { + public ImageUploadFailException(String message) { + super(message); + } + + public ImageUploadFailException(Throwable cause) { + super(cause); + } + + public ImageUploadFailException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/exception/NotFoundException.java b/backend/src/main/java/com/stampcrush/backend/exception/NotFoundException.java new file mode 100644 index 000000000..c63cf53cc --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/exception/NotFoundException.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.exception; + +public class NotFoundException extends StampCrushException { + + public NotFoundException(String message) { + super(message); + } + + public NotFoundException(Throwable cause) { + super(cause); + } + + public NotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/exception/OwnerNotFoundException.java b/backend/src/main/java/com/stampcrush/backend/exception/OwnerNotFoundException.java new file mode 100644 index 000000000..4b02cc361 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/exception/OwnerNotFoundException.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.exception; + +public class OwnerNotFoundException extends NotFoundException { + + public OwnerNotFoundException(String message) { + super(message); + } + + public OwnerNotFoundException(Throwable cause) { + super(cause); + } + + public OwnerNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/exception/OwnerUnAuthorizationException.java b/backend/src/main/java/com/stampcrush/backend/exception/OwnerUnAuthorizationException.java new file mode 100644 index 000000000..f74d8db9f --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/exception/OwnerUnAuthorizationException.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.exception; + +public class OwnerUnAuthorizationException extends UnAuthorizationException { + + public OwnerUnAuthorizationException(String message) { + super(message); + } + + public OwnerUnAuthorizationException(Throwable cause) { + super(cause); + } + + public OwnerUnAuthorizationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/exception/StampCrushException.java b/backend/src/main/java/com/stampcrush/backend/exception/StampCrushException.java new file mode 100644 index 000000000..c742b43bf --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/exception/StampCrushException.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.exception; + +public class StampCrushException extends RuntimeException { + + public StampCrushException(String message) { + super(message); + } + + public StampCrushException(Throwable cause) { + super(cause); + } + + public StampCrushException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/exception/UnAuthorizationException.java b/backend/src/main/java/com/stampcrush/backend/exception/UnAuthorizationException.java new file mode 100644 index 000000000..2f4a73ea5 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/exception/UnAuthorizationException.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.exception; + +public class UnAuthorizationException extends StampCrushException { + + public UnAuthorizationException(String message) { + super(message); + } + + public UnAuthorizationException(Throwable cause) { + super(cause); + } + + public UnAuthorizationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/exception/VisitHistoryNotFoundException.java b/backend/src/main/java/com/stampcrush/backend/exception/VisitHistoryNotFoundException.java new file mode 100644 index 000000000..70ffcfbd3 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/exception/VisitHistoryNotFoundException.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.exception; + +public class VisitHistoryNotFoundException extends NotFoundException { + + public VisitHistoryNotFoundException(String message) { + super(message); + } + + public VisitHistoryNotFoundException(Throwable cause) { + super(cause); + } + + public VisitHistoryNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/http/coupon-edit-request.http b/backend/src/main/java/com/stampcrush/backend/http/coupon-edit-request.http new file mode 100644 index 000000000..ffdec404a --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/http/coupon-edit-request.http @@ -0,0 +1,22 @@ +POST localhost:8080/coupon-setting?cafe-id=1 +Content-Type: application/json + +{ + "frontImageUrl": "http://localhost:3000", + "backImageUrl": "http://localhost:3000", + "stampImageUrl": "http://localhost:3000", + "coordinates": [ + { + "order": 1, + "xCoordinate": 2, + "yCoordinate": 5 + }, + { + "order": 2, + "xCoordinate": 5, + "yCoordinate": 5 + } + ], + "reward": "아메리카노", + "expirePeriod": 6 +} diff --git a/backend/src/main/java/com/stampcrush/backend/http/customer-request.http b/backend/src/main/java/com/stampcrush/backend/http/customer-request.http new file mode 100644 index 000000000..eba166c5c --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/http/customer-request.http @@ -0,0 +1,17 @@ +### 임시 가입 고객 생성 (같은 번호로 가입하면 에러 발생) + +POST localhost:8080/temporary-customers +Content-Type: application/json + +{ + "phoneNumber" : "01011112222" +} + +### 전화번호로 고객 조회 + +GET localhost:8080/customers?phone-number=01011112222 + + +### 전화번호로 고객 조회 시 조회되는 값이 없으면 빈 배열 응답 + +GET localhost:8080/customers?phone-number=0109876 diff --git a/backend/src/main/java/com/stampcrush/backend/http/manager-cafe-create-request.http b/backend/src/main/java/com/stampcrush/backend/http/manager-cafe-create-request.http new file mode 100644 index 000000000..66f4e9d1d --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/http/manager-cafe-create-request.http @@ -0,0 +1,19 @@ +### 사장의 액세스 토큰 포함 요청 + +POST http://localhost:8080/api/admin/cafes +Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwiZXhwIjoxNjkyMTUyNjg4fQ.VRWHEij1ugEU93tfJSS9XcWtVchRdATLruds2rbdth1ytyp8h3B7Q9PUm_RhTP8PfoqcoOyVKH1citNuqQFWyQ + +{ + "name": "우아한카페", + "roadAddress": "서울시 잠실", + "detailAddress": "루터회관 13층", + "businessRegistrationNumber": "000-111-222" +} + +### 고객의 액세스 토큰 포함 요청 + +GET http://localhost:8080/api/coupons +Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwiZXhwIjoxNjkyMTUyOTUyfQ.zclLXxrZ7B-OW6EpuUKuZnPApw_77i0eg8Y0upTawzifgnMWecFlE_UvxKtm-hZE2Iv4-0dR6SSToyqpdQq0hg + diff --git a/backend/src/main/java/com/stampcrush/backend/http/phone-number-update-request.http b/backend/src/main/java/com/stampcrush/backend/http/phone-number-update-request.http new file mode 100644 index 000000000..96ddb3a50 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/http/phone-number-update-request.http @@ -0,0 +1,3 @@ +POST http://localhost:8080/api/profiles/phone-number +Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwiZXhwIjoxNjkyMTUyNjg4fQ.VRWHEij1ugEU93tfJSS9XcWtVchRdATLruds2rbdth1ytyp8h3B7Q9PUm_RhTP8PfoqcoOyVKH1citNuqQFWyQ diff --git a/backend/src/main/java/com/stampcrush/backend/http/reward-request.http b/backend/src/main/java/com/stampcrush/backend/http/reward-request.http new file mode 100644 index 000000000..ce1fafe0b --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/http/reward-request.http @@ -0,0 +1,5 @@ +### 고객의 리워드 조회 요청 + +GET localhost:8080/customers/1/rewards?cafeId=1&used=false +Content-Type: application/json + diff --git a/backend/src/main/java/com/stampcrush/backend/http/sample-coupon-request.http b/backend/src/main/java/com/stampcrush/backend/http/sample-coupon-request.http new file mode 100644 index 000000000..a61055dd2 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/http/sample-coupon-request.http @@ -0,0 +1,11 @@ +### 전체 쿠폰 샘플 조회 + +GET http://localhost:8080/coupon-samples + +### 쿠폰 샘플 중 뒷면의 경우 스탬프 8개용만 조회 + +GET http://localhost:8080/coupon-samples?maxStampCount=8 + +### 쿠폰 샘플 중 뒷면의 경우 스탬프 10개용만 조회 + +GET http://localhost:8080/coupon-samples?maxStampCount=10 diff --git a/backend/src/main/java/com/stampcrush/backend/initializer/DataInitializer.java b/backend/src/main/java/com/stampcrush/backend/initializer/DataInitializer.java new file mode 100644 index 000000000..a058fa120 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/initializer/DataInitializer.java @@ -0,0 +1,134 @@ +package com.stampcrush.backend.initializer; + +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.cafe.CafeCouponDesign; +import com.stampcrush.backend.entity.cafe.CafePolicy; +import com.stampcrush.backend.entity.cafe.CafeStampCoordinate; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.entity.user.TemporaryCustomer; +import com.stampcrush.backend.repository.cafe.CafeCouponDesignRepository; +import com.stampcrush.backend.repository.cafe.CafePolicyRepository; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.cafe.CafeStampCoordinateRepository; +import com.stampcrush.backend.repository.user.OwnerRepository; +import com.stampcrush.backend.repository.user.RegisterCustomerRepository; +import com.stampcrush.backend.repository.user.TemporaryCustomerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; + +import java.time.LocalTime; + +@RequiredArgsConstructor +public class DataInitializer implements ApplicationRunner { + + private final TemporaryCustomerRepository temporaryCustomerRepository; + private final RegisterCustomerRepository registerCustomerRepository; + private final CafeRepository cafeRepository; + private final OwnerRepository ownerRepository; + private final CafeStampCoordinateRepository cafeStampCoordinateRepository; + private final CafePolicyRepository cafePolicyRepository; + private final CafeCouponDesignRepository cafeCouponDesignRepository; + + public void run(ApplicationArguments args) { + + Owner owner_1 = ownerRepository.save(new Owner("stampcrush_1", "id", "1234", "01011111111")); + Owner owner_2 = ownerRepository.save(new Owner("stampcrush_2", "id", "1234", "01011111111")); + Cafe cafe = cafeRepository.save(new Cafe( + "스탬프크러쉬카페", + LocalTime.of(12, 30), + LocalTime.of(18, 30), + "0211111111", + "http://www.cafeImage.com", + "안녕하세요", + "잠실동12길", + "14층", + "11111111", + owner_1)); + + RegisterCustomer customer1 = registerCustomerRepository.save(new RegisterCustomer("레오", "01038626099", "leo", "1234")); + RegisterCustomer customer2 = registerCustomerRepository.save(new RegisterCustomer("하디", "01064394814", "hardy", "5678")); + TemporaryCustomer temporaryCustomer = temporaryCustomerRepository.save(TemporaryCustomer.from("01012345678")); + + CafePolicy savedCafePolicy = cafePolicyRepository.save( + new CafePolicy( + 10, + "아메리카노", + 12, + false, + cafe + ) + ); + + CafeCouponDesign savedCafeCouponDesign = cafeCouponDesignRepository.save( + new CafeCouponDesign( + "#", + "#", + "#", + false, + cafe + ) + ); + + CafeStampCoordinate savedCafeStampCoordinate1 = cafeStampCoordinateRepository.save( + new CafeStampCoordinate( + 1, 1, 1, savedCafeCouponDesign + ) + ); + + CafeStampCoordinate savedCafeStampCoordinate2 = cafeStampCoordinateRepository.save( + new CafeStampCoordinate( + 2, 2, 1, savedCafeCouponDesign + ) + ); + + CafeStampCoordinate savedCafeStampCoordinate3 = cafeStampCoordinateRepository.save( + new CafeStampCoordinate( + 3, 3, 1, savedCafeCouponDesign + ) + ); + + CafeStampCoordinate savedCafeStampCoordinate4 = cafeStampCoordinateRepository.save( + new CafeStampCoordinate( + 4, 4, 1, savedCafeCouponDesign + ) + ); + + CafeStampCoordinate savedCafeStampCoordinate5 = cafeStampCoordinateRepository.save( + new CafeStampCoordinate( + 5, 5, 1, savedCafeCouponDesign + ) + ); + + CafeStampCoordinate savedCafeStampCoordinate6 = cafeStampCoordinateRepository.save( + new CafeStampCoordinate( + 6, 6, 1, savedCafeCouponDesign + ) + ); + + CafeStampCoordinate savedCafeStampCoordinate7 = cafeStampCoordinateRepository.save( + new CafeStampCoordinate( + 7, 7, 1, savedCafeCouponDesign + ) + ); + + CafeStampCoordinate savedCafeStampCoordinate8 = cafeStampCoordinateRepository.save( + new CafeStampCoordinate( + 8, 8, 1, savedCafeCouponDesign + ) + ); + + CafeStampCoordinate savedCafeStampCoordinate9 = cafeStampCoordinateRepository.save( + new CafeStampCoordinate( + 9, 9, 1, savedCafeCouponDesign + ) + ); + + CafeStampCoordinate savedCafeStampCoordinate10 = cafeStampCoordinateRepository.save( + new CafeStampCoordinate( + 10, 10, 1, savedCafeCouponDesign + ) + ); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/initializer/SampleDataInitializer.java b/backend/src/main/java/com/stampcrush/backend/initializer/SampleDataInitializer.java new file mode 100644 index 000000000..8798fbe94 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/initializer/SampleDataInitializer.java @@ -0,0 +1,62 @@ +package com.stampcrush.backend.initializer; + +import com.stampcrush.backend.entity.sample.SampleBackImage; +import com.stampcrush.backend.entity.sample.SampleFrontImage; +import com.stampcrush.backend.entity.sample.SampleStampCoordinate; +import com.stampcrush.backend.entity.sample.SampleStampImage; +import com.stampcrush.backend.repository.sample.SampleBackImageRepository; +import com.stampcrush.backend.repository.sample.SampleFrontImageRepository; +import com.stampcrush.backend.repository.sample.SampleStampCoordinateRepository; +import com.stampcrush.backend.repository.sample.SampleStampImageRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; + +@RequiredArgsConstructor +public class SampleDataInitializer implements ApplicationRunner { + + private static final String SAMPLE_FRONT_IMAGE_URL = "https://picsum.photos/270/150"; + private static final String SAMPLE_BACK_IMAGE_URL = "https://picsum.photos/270/150"; + private static final String SAMPLE_BACK_IMAGE_URL_2 = "https://picsum.photos/270/150"; + private static final String SAMPLE_STAMP_IMAGE_URL = "https://picsum.photos/50"; + + private final SampleFrontImageRepository sampleFrontImageRepository; + private final SampleBackImageRepository sampleBackImageRepository; + private final SampleStampCoordinateRepository sampleStampCoordinateRepository; + private final SampleStampImageRepository sampleStampImageRepository; + + @Override + public void run(ApplicationArguments args) throws Exception { + SampleFrontImage sampleFrontImage = new SampleFrontImage(SAMPLE_FRONT_IMAGE_URL); + SampleFrontImage savedFrontImage = sampleFrontImageRepository.save(sampleFrontImage); + + SampleBackImage sampleBackImage = new SampleBackImage(SAMPLE_BACK_IMAGE_URL); + SampleBackImage savedSampleBackImage = sampleBackImageRepository.save(sampleBackImage); + + sampleStampCoordinateRepository.save(new SampleStampCoordinate(1, 1, 2, sampleBackImage)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(2, 2, 2, sampleBackImage)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(3, 3, 2, sampleBackImage)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(4, 4, 2, sampleBackImage)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(5, 1, 1, sampleBackImage)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(6, 2, 1, sampleBackImage)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(7, 3, 1, sampleBackImage)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(8, 4, 1, sampleBackImage)); + + SampleStampImage sampleStampImage = new SampleStampImage(SAMPLE_STAMP_IMAGE_URL); + + SampleBackImage sampleBackImage_2 = sampleBackImageRepository.save(new SampleBackImage(SAMPLE_BACK_IMAGE_URL_2)); + + sampleStampCoordinateRepository.save(new SampleStampCoordinate(1, 1, 2, sampleBackImage_2)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(2, 2, 2, sampleBackImage_2)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(3, 3, 2, sampleBackImage_2)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(4, 4, 2, sampleBackImage_2)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(5, 1, 1, sampleBackImage_2)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(6, 2, 1, sampleBackImage_2)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(7, 3, 1, sampleBackImage_2)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(8, 4, 1, sampleBackImage_2)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(9, 4, 1, sampleBackImage_2)); + sampleStampCoordinateRepository.save(new SampleStampCoordinate(10, 4, 1, sampleBackImage_2)); + + sampleStampImageRepository.save(sampleStampImage); + } +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/cafe/CafeCouponDesignRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/cafe/CafeCouponDesignRepository.java new file mode 100644 index 000000000..3bf956d16 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/cafe/CafeCouponDesignRepository.java @@ -0,0 +1,12 @@ +package com.stampcrush.backend.repository.cafe; + +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.cafe.CafeCouponDesign; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface CafeCouponDesignRepository extends JpaRepository { + + Optional findByCafe(Cafe cafe); +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/cafe/CafePolicyRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/cafe/CafePolicyRepository.java new file mode 100644 index 000000000..69159a584 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/cafe/CafePolicyRepository.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.repository.cafe; + +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.cafe.CafePolicy; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface CafePolicyRepository extends JpaRepository { + + Optional findByCafe(Cafe cafe); + + List findByCafeAndCreatedAtGreaterThan(Cafe cafe, LocalDateTime createdAt); +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/cafe/CafeRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/cafe/CafeRepository.java new file mode 100644 index 000000000..58c4dbbb0 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/cafe/CafeRepository.java @@ -0,0 +1,14 @@ +package com.stampcrush.backend.repository.cafe; + +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.user.Owner; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CafeRepository extends JpaRepository { + + List findAllByOwnerId(Long ownerId); + + List findAllByOwner(Owner owner); +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/cafe/CafeStampCoordinateRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/cafe/CafeStampCoordinateRepository.java new file mode 100644 index 000000000..0f76baff9 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/cafe/CafeStampCoordinateRepository.java @@ -0,0 +1,7 @@ +package com.stampcrush.backend.repository.cafe; + +import com.stampcrush.backend.entity.cafe.CafeStampCoordinate; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CafeStampCoordinateRepository extends JpaRepository { +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/coupon/CouponDesignRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/coupon/CouponDesignRepository.java new file mode 100644 index 000000000..9181ce038 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/coupon/CouponDesignRepository.java @@ -0,0 +1,7 @@ +package com.stampcrush.backend.repository.coupon; + +import com.stampcrush.backend.entity.coupon.CouponDesign; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CouponDesignRepository extends JpaRepository { +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/coupon/CouponPolicyRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/coupon/CouponPolicyRepository.java new file mode 100644 index 000000000..dc8c8ecca --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/coupon/CouponPolicyRepository.java @@ -0,0 +1,7 @@ +package com.stampcrush.backend.repository.coupon; + +import com.stampcrush.backend.entity.coupon.CouponPolicy; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CouponPolicyRepository extends JpaRepository { +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/coupon/CouponRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/coupon/CouponRepository.java new file mode 100644 index 000000000..8fa7ec651 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/coupon/CouponRepository.java @@ -0,0 +1,25 @@ +package com.stampcrush.backend.repository.coupon; + +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.coupon.Coupon; +import com.stampcrush.backend.entity.coupon.CouponStatus; +import com.stampcrush.backend.entity.user.Customer; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface CouponRepository extends JpaRepository { + + @Query("select distinct cp from Coupon cp left join fetch cp.stamps join fetch cp.cafe join fetch cp.customer where cp.cafe = :cafe") + List findByCafe(@Param("cafe") Cafe cafe); + + List findByCafeAndCustomerAndStatus(@Param("cafe") Cafe cafe, @Param("customer") Customer customer, @Param("status") CouponStatus status); + + @Query("SELECT c FROM Coupon c LEFT JOIN Favorites f ON c.cafe.id = f.cafe.id WHERE c.customer = :customer AND c.status = :status ORDER BY f.isFavorites DESC, c.expiredDate ASC") + List findFilteredAndSortedCoupons(@Param("customer") Customer customer, @Param("status") CouponStatus status); + + Optional findByIdAndCustomerId(Long id, Long customerId); +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/coupon/CouponStampCoordinateRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/coupon/CouponStampCoordinateRepository.java new file mode 100644 index 000000000..8e7e1678f --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/coupon/CouponStampCoordinateRepository.java @@ -0,0 +1,12 @@ +package com.stampcrush.backend.repository.coupon; + +import com.stampcrush.backend.entity.coupon.CouponDesign; +import com.stampcrush.backend.entity.coupon.CouponStampCoordinate; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CouponStampCoordinateRepository extends JpaRepository { + + List findByCouponDesign(CouponDesign couponDesign); +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/coupon/StampRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/coupon/StampRepository.java new file mode 100644 index 000000000..e61b99b54 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/coupon/StampRepository.java @@ -0,0 +1,7 @@ +package com.stampcrush.backend.repository.coupon; + +import com.stampcrush.backend.entity.coupon.Stamp; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StampRepository extends JpaRepository { +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/favorites/FavoritesRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/favorites/FavoritesRepository.java new file mode 100644 index 000000000..96fd26c37 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/favorites/FavoritesRepository.java @@ -0,0 +1,13 @@ +package com.stampcrush.backend.repository.favorites; + +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.favorites.Favorites; +import com.stampcrush.backend.entity.user.Customer; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface FavoritesRepository extends JpaRepository { + + Optional findByCafeAndCustomer(Cafe cafe, Customer customer); +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/reward/RewardRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/reward/RewardRepository.java new file mode 100644 index 000000000..674f3311a --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/reward/RewardRepository.java @@ -0,0 +1,17 @@ +package com.stampcrush.backend.repository.reward; + +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.reward.Reward; +import com.stampcrush.backend.entity.user.Customer; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface RewardRepository extends JpaRepository { + + List findAllByCustomerIdAndCafeIdAndUsed(Long CustomerId, Long CafeId, boolean used); + + List findAllByCustomerAndUsed(Customer customer, boolean used); + + Long countByCafeAndCustomerAndUsed(Cafe cafe, Customer customer, Boolean used); +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/sample/SampleBackImageRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/sample/SampleBackImageRepository.java new file mode 100644 index 000000000..718a091d9 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/sample/SampleBackImageRepository.java @@ -0,0 +1,7 @@ +package com.stampcrush.backend.repository.sample; + +import com.stampcrush.backend.entity.sample.SampleBackImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SampleBackImageRepository extends JpaRepository { +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/sample/SampleFrontImageRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/sample/SampleFrontImageRepository.java new file mode 100644 index 000000000..af260fa40 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/sample/SampleFrontImageRepository.java @@ -0,0 +1,7 @@ +package com.stampcrush.backend.repository.sample; + +import com.stampcrush.backend.entity.sample.SampleFrontImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SampleFrontImageRepository extends JpaRepository { +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/sample/SampleStampCoordinateRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/sample/SampleStampCoordinateRepository.java new file mode 100644 index 000000000..3421c67e0 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/sample/SampleStampCoordinateRepository.java @@ -0,0 +1,12 @@ +package com.stampcrush.backend.repository.sample; + +import com.stampcrush.backend.entity.sample.SampleBackImage; +import com.stampcrush.backend.entity.sample.SampleStampCoordinate; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface SampleStampCoordinateRepository extends JpaRepository { + + List findSampleStampCoordinateBySampleBackImage(SampleBackImage sampleBackImage); +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/sample/SampleStampImageRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/sample/SampleStampImageRepository.java new file mode 100644 index 000000000..232ec7156 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/sample/SampleStampImageRepository.java @@ -0,0 +1,7 @@ +package com.stampcrush.backend.repository.sample; + +import com.stampcrush.backend.entity.sample.SampleStampImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SampleStampImageRepository extends JpaRepository { +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/user/CustomerRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/user/CustomerRepository.java new file mode 100644 index 000000000..304dfdb35 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/user/CustomerRepository.java @@ -0,0 +1,11 @@ +package com.stampcrush.backend.repository.user; + +import com.stampcrush.backend.entity.user.Customer; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CustomerRepository extends JpaRepository { + + List findByPhoneNumber(String phoneNumber); +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/user/OwnerRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/user/OwnerRepository.java new file mode 100644 index 000000000..d81633270 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/user/OwnerRepository.java @@ -0,0 +1,22 @@ +package com.stampcrush.backend.repository.user; + +import com.stampcrush.backend.auth.OAuthProvider; +import com.stampcrush.backend.entity.user.Owner; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface OwnerRepository extends JpaRepository { + + Optional findByLoginId(String loginId); + + Optional findByNickname(String nickname); + + @Query("SELECT o FROM Owner o WHERE o.oAuthProvider = :oAuthProvider AND o.oAuthId = :oAuthId") + Optional findByOAuthProviderAndOAuthId( + @Param("oAuthProvider") OAuthProvider oAuthProvider, + @Param("oAuthId") Long oAuthId + ); +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/user/RegisterCustomerRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/user/RegisterCustomerRepository.java new file mode 100644 index 000000000..1c23c8fab --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/user/RegisterCustomerRepository.java @@ -0,0 +1,25 @@ +package com.stampcrush.backend.repository.user; + +import com.stampcrush.backend.auth.OAuthProvider; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface RegisterCustomerRepository extends JpaRepository { + + Optional findByLoginId(String loginId); + + Optional findByNickname(String nickname); + + @Query("SELECT c FROM RegisterCustomer c WHERE c.oAuthProvider = :oAuthProvider AND c.oAuthId = :oAuthId") + Optional findByOAuthProviderAndOAuthId( + @Param("oAuthProvider") OAuthProvider oAuthProvider, + @Param("oAuthId") Long oAuthId + ); + + // TODO: 기본 제공되는 findById 가 인자를 Integer 로 받아서 재정의함 + Optional findById(Long id); +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/user/TemporaryCustomerRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/user/TemporaryCustomerRepository.java new file mode 100644 index 000000000..7d17abae9 --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/user/TemporaryCustomerRepository.java @@ -0,0 +1,7 @@ +package com.stampcrush.backend.repository.user; + +import com.stampcrush.backend.entity.user.TemporaryCustomer; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TemporaryCustomerRepository extends JpaRepository { +} diff --git a/backend/src/main/java/com/stampcrush/backend/repository/visithistory/VisitHistoryRepository.java b/backend/src/main/java/com/stampcrush/backend/repository/visithistory/VisitHistoryRepository.java new file mode 100644 index 000000000..19914fe1c --- /dev/null +++ b/backend/src/main/java/com/stampcrush/backend/repository/visithistory/VisitHistoryRepository.java @@ -0,0 +1,15 @@ +package com.stampcrush.backend.repository.visithistory; + +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.visithistory.VisitHistory; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface VisitHistoryRepository extends JpaRepository { + + List findByCafeAndCustomer(Cafe cafe, Customer customer); + + List findVisitHistoriesByCustomer(Customer customer); +} diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml new file mode 100644 index 000000000..833c38476 --- /dev/null +++ b/backend/src/main/resources/application-dev.yml @@ -0,0 +1,6 @@ +spring: + config: + activate: + on-profile: dev + import: + - security/application-dev.yml diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml new file mode 100644 index 000000000..4c902490e --- /dev/null +++ b/backend/src/main/resources/application-prod.yml @@ -0,0 +1,6 @@ +spring: + config: + activate: + on-profile: prod + import: + - security/application-prod.yml diff --git a/backend/src/main/resources/application-test.yml b/backend/src/main/resources/application-test.yml new file mode 100644 index 000000000..10bb8a043 --- /dev/null +++ b/backend/src/main/resources/application-test.yml @@ -0,0 +1,26 @@ +spring: + config: + activate: + on-profile: test + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: create + properties: + hibernate: + show_sql: true + format_sql: true + use_sql_comments: true + jdbc: + batch_size: 10 + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:test;Mode=MySql + +jwt: + secret-key: eyJpc3MiOiJ2ZWxvcGVydC5jb20iLCJleHAiOiIxNDg1MjcwMDAwMDAwIiwiaHR0cHM6Ly92ZWxvcGVydC5jb20vand0X2NsYWltcy9pc19hZG1pbiI6dHJ1ZSwidXNlcklkIjoiMTEwMjgzNzM3MjcxMDIiLCJ1c2VybmFtZSI6InZlbG9wZXJ0In0 + +s3: + bucket: test + base-url: test + dir: test diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 000000000..4887888ba --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,4 @@ +spring: + config: + import: + - security/application.yml diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml new file mode 100644 index 000000000..ae5b711e8 --- /dev/null +++ b/backend/src/main/resources/logback-spring.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + + + + + + ./logs/warn.log + + WARN + ACCEPT + DENY + + + ${FILE_LOG_PATTERN} + + + ./was-logs/warn.%d{yyyy-MM-dd}.%i.log.gz + + 100MB + + 180 + + + + + + ./logs/error.log + + ERROR + ACCEPT + DENY + + + ${FILE_LOG_PATTERN} + + + ./was-logs/error.%d{yyyy-MM-dd}.%i.log.gz + + 100MB + + 180 + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/security b/backend/src/main/resources/security new file mode 160000 index 000000000..3025c834f --- /dev/null +++ b/backend/src/main/resources/security @@ -0,0 +1 @@ +Subproject commit 3025c834f59f7b7a99feb3387a2625b5e6edaabf diff --git a/backend/src/test/java/com/stampcrush/backend/BackendApplicationTests.java b/backend/src/test/java/com/stampcrush/backend/BackendApplicationTests.java new file mode 100644 index 000000000..278bdaeef --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/BackendApplicationTests.java @@ -0,0 +1,12 @@ +package com.stampcrush.backend; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BackendApplicationTests { + + @Test + void contextLoads() { + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/AcceptanceTest.java b/backend/src/test/java/com/stampcrush/backend/acceptance/AcceptanceTest.java new file mode 100644 index 000000000..40ef4ddd4 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/AcceptanceTest.java @@ -0,0 +1,36 @@ +package com.stampcrush.backend.acceptance; + +import com.stampcrush.backend.common.DataCleaner; +import com.stampcrush.backend.common.DataClearExtension; +import com.stampcrush.backend.common.KorNamingConverter; +import io.restassured.RestAssured; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@KorNamingConverter +@ExtendWith(DataClearExtension.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +public class AcceptanceTest { + + @LocalServerPort + private int port; + + @Autowired + private DataCleaner cleaner; + + @BeforeEach + void setup() { + RestAssured.port = port; + } + + @AfterEach + void tearDown() { + cleaner.clear(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerCafeCouponSettingCommandUpdateAcceptanceTest.java b/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerCafeCouponSettingCommandUpdateAcceptanceTest.java new file mode 100644 index 000000000..6887e5c06 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerCafeCouponSettingCommandUpdateAcceptanceTest.java @@ -0,0 +1,87 @@ +package com.stampcrush.backend.acceptance; + +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.cafe.CafeCouponDesign; +import com.stampcrush.backend.entity.cafe.CafePolicy; +import com.stampcrush.backend.entity.cafe.CafeStampCoordinate; +import com.stampcrush.backend.fixture.OwnerFixture; +import com.stampcrush.backend.repository.cafe.CafeCouponDesignRepository; +import com.stampcrush.backend.repository.cafe.CafePolicyRepository; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.cafe.CafeStampCoordinateRepository; +import com.stampcrush.backend.repository.user.OwnerRepository; +import io.restassured.http.ContentType; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import static com.stampcrush.backend.acceptance.step.ManagerCafeCouponSettingUpdateStep.CAFE_COUPON_SETTING_UPDATE_REQUEST; +import static com.stampcrush.backend.fixture.CafeFixture.cafeOfSavedOwner; +import static com.stampcrush.backend.fixture.CouponDesignFixture.cafeCouponDesignOfSavedCafe; +import static com.stampcrush.backend.fixture.CouponPolicyFixture.cafePolicyOfSavedCafe; +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +public class ManagerCafeCouponSettingCommandUpdateAcceptanceTest extends AcceptanceTest { + + @Autowired + private CafeRepository cafeRepository; + + @Autowired + private CafeCouponDesignRepository cafeCouponDesignRepository; + + @Autowired + private CafePolicyRepository cafePolicyRepository; + + @Autowired + private CafeStampCoordinateRepository cafeStampCoordinateRepository; + + @Autowired + private OwnerRepository ownerRepository; + + @Test + void 카페_사장은_쿠폰_세팅에_대한_내용을_수정할_수_있다() { + // given, when + Cafe savedCafe = cafeRepository.save(cafeOfSavedOwner(ownerRepository.save(OwnerFixture.GITCHAN))); + + CafePolicy savedCafePolicy = cafePolicyRepository.save(cafePolicyOfSavedCafe(savedCafe)); + CafeCouponDesign savedCafeCouponDesign = cafeCouponDesignRepository.save(cafeCouponDesignOfSavedCafe(savedCafe)); + + CafeStampCoordinate savedCafeStampCoordinate1 = cafeStampCoordinateRepository.save( + new CafeStampCoordinate( + 1, 1, 1, savedCafeCouponDesign + ) + ); + + CafeStampCoordinate savedCafeStampCoordinate2 = cafeStampCoordinateRepository.save( + new CafeStampCoordinate( + 1, 2, 1, savedCafeCouponDesign + ) + ); + + ExtractableResponse response = given() + .log().all() + .contentType(ContentType.JSON) + .auth().preemptive().basic(OwnerFixture.GITCHAN.getLoginId(), OwnerFixture.GITCHAN.getEncryptedPassword()) + .body(CAFE_COUPON_SETTING_UPDATE_REQUEST) + + .when() + .post("/api/admin/coupon-setting?cafe-id=" + savedCafe.getId()) + + .then() + .log().all() + .extract(); + + // then + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()), + () -> assertThat(cafeCouponDesignRepository.findById(savedCafeCouponDesign.getId())).isEmpty(), + () -> assertThat(cafeCouponDesignRepository.findByCafe(savedCafe)).isNotEmpty(), + () -> assertThat(cafePolicyRepository.findById(savedCafePolicy.getId())).isEmpty(), + () -> assertThat(cafePolicyRepository.findByCafe(savedCafe)).isNotEmpty() + ); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerCouponCommandAcceptanceTest.java b/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerCouponCommandAcceptanceTest.java new file mode 100644 index 000000000..568d333a6 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerCouponCommandAcceptanceTest.java @@ -0,0 +1,318 @@ +package com.stampcrush.backend.acceptance; + +import com.stampcrush.backend.api.manager.cafe.request.CafeCouponSettingUpdateRequest; +import com.stampcrush.backend.api.manager.coupon.request.CouponCreateRequest; +import com.stampcrush.backend.api.manager.coupon.request.StampCreateRequest; +import com.stampcrush.backend.api.manager.coupon.response.CafeCustomerFindResponse; +import com.stampcrush.backend.api.manager.coupon.response.CustomerAccumulatingCouponFindResponse; +import com.stampcrush.backend.api.manager.coupon.response.CustomerAccumulatingCouponsFindResponse; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.repository.user.OwnerRepository; +import com.stampcrush.backend.repository.user.RegisterCustomerRepository; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static com.stampcrush.backend.acceptance.step.ManagerCafeCouponSettingUpdateStep.CAFE_COUPON_SETTING_UPDATE_REQUEST; +import static com.stampcrush.backend.acceptance.step.ManagerCafeCouponSettingUpdateStep.카페_쿠폰_정책_수정_요청; +import static com.stampcrush.backend.acceptance.step.ManagerCafeCreateStep.CAFE_CREATE_REQUEST; +import static com.stampcrush.backend.acceptance.step.ManagerCafeCreateStep.카페_생성_요청하고_아이디_반환; +import static com.stampcrush.backend.acceptance.step.ManagerCouponCreateStep.쿠폰_생성_요청하고_아이디_반환; +import static com.stampcrush.backend.acceptance.step.ManagerCouponFindStep.고객의_쿠폰_조회_요청; +import static com.stampcrush.backend.acceptance.step.ManagerCouponFindStep.고객의_쿠폰_조회하고_결과_반환; +import static com.stampcrush.backend.acceptance.step.ManagerStampCreateStep.쿠폰에_스탬프를_적립_요청; +import static com.stampcrush.backend.fixture.CustomerFixture.REGISTER_CUSTOMER_YOUNGHO; +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; + +public class ManagerCouponCommandAcceptanceTest extends AcceptanceTest { + + @Autowired + private OwnerRepository ownerRepository; + + @Autowired + private RegisterCustomerRepository registerCustomerRepository; + + private static Response 고객_조회_요청(Owner owner, Long savedCafeId) { + return given() + .log().all() + .auth().preemptive() + .basic(owner.getLoginId(), owner.getEncryptedPassword()) + + .when() + .get("/api/admin/cafes/{cafeId}/customers", savedCafeId); + } + + @Test + void 쿠폰을_발급한다() { + // given + Owner owner = 사장_생성(); + Long savedCafeId = 카페_생성_요청하고_아이디_반환(owner, CAFE_CREATE_REQUEST); + RegisterCustomer savedCustomer = registerCustomerRepository.save(REGISTER_CUSTOMER_YOUNGHO); + CouponCreateRequest request = new CouponCreateRequest(savedCafeId); + + // when + Long couponId = 쿠폰_생성_요청하고_아이디_반환(owner, request, savedCustomer.getId()); + + List coupons = 고객의_쿠폰_조회하고_결과_반환(owner, savedCafeId, savedCustomer); + + // then + assertAll( + () -> assertThat(coupons.size()).isEqualTo(1), + () -> assertThat(coupons) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields("expireDate") + .containsExactlyInAnyOrder( + new CustomerAccumulatingCouponFindResponse( + couponId, + savedCustomer.getId(), + savedCustomer.getNickname(), + 0, + null, + false, + 10 + ) + ) + ); + } + + @Test + void 스탬프를_적립한다() { + // given + Owner owner = 사장_생성(); + Long savedCafeId = 카페_생성_요청하고_아이디_반환(owner, CAFE_CREATE_REQUEST); + RegisterCustomer savedCustomer = registerCustomerRepository.save(new RegisterCustomer("name", "phone", "id", "pw")); + CouponCreateRequest request = new CouponCreateRequest(savedCafeId); + + // when + Long couponId = 쿠폰_생성_요청하고_아이디_반환(owner, request, savedCustomer.getId()); + + StampCreateRequest stampCreateRequest = new StampCreateRequest(4); + 쿠폰에_스탬프를_적립_요청(owner, savedCustomer, couponId, stampCreateRequest); + + List coupons = 고객의_쿠폰_조회하고_결과_반환(owner, savedCafeId, savedCustomer); + + // then + assertAll( + () -> assertThat(coupons.size()).isEqualTo(1), + () -> assertThat(coupons) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields("expireDate") + .containsExactlyInAnyOrder( + new CustomerAccumulatingCouponFindResponse( + couponId, + savedCustomer.getId(), + savedCustomer.getNickname(), + stampCreateRequest.getEarningStampCount(), + null, + false, + 10 + ) + ) + ); + } + + @Test + void 사장님_인증_정보_없이_쿠폰을_생성하려고_하면_예외발생() { + // given + CouponCreateRequest reqeust = new CouponCreateRequest(1L); + // when + ExtractableResponse response = given() + .log().all() + .contentType(JSON) + .body(reqeust) + + .when() + .post("/api/admin/customers/{customerId}/coupons", 1) + + .then() + .log().all() + .extract(); + // then + int status = response.statusCode(); + + assertThat(status).isEqualTo(UNAUTHORIZED.value()); + } + + @Test + void 사장님_인증_정보_없이_스탬프_적립_하려고_하면_예외발생() { + // given + StampCreateRequest request = new StampCreateRequest(4); + + // when + ExtractableResponse response = given() + .log().all() + .body(request) + .contentType(JSON) + + .when() + .post("/api/admin/customers/{customerId}/coupons/{couponId}/stamps", 1, 1) + + .then().log().all() + .extract(); + // then + int status = response.statusCode(); + assertThat(status).isEqualTo(UNAUTHORIZED.value()); + } + + @Test + void 특정_카페의_방문한_고객들의_정보를_조회한다() { + // given + Owner owner = 사장_생성(); + Long savedCafeId = 카페_생성_요청하고_아이디_반환(owner, CAFE_CREATE_REQUEST); + RegisterCustomer customer1 = registerCustomerRepository.save(new RegisterCustomer("name", "phone", "id", "pw")); + CouponCreateRequest reqeust1 = new CouponCreateRequest(savedCafeId); + Long coupon1Id = 쿠폰_생성_요청하고_아이디_반환(owner, reqeust1, customer1.getId()); + + RegisterCustomer customer2 = registerCustomerRepository.save(new RegisterCustomer("name2", "phone2", "id2", "pw2")); + CouponCreateRequest reqeust2 = new CouponCreateRequest(savedCafeId); + Long coupon2Id = 쿠폰_생성_요청하고_아이디_반환(owner, reqeust2, customer2.getId()); + + StampCreateRequest coupon1StampCreateRequest = new StampCreateRequest(5); + 쿠폰에_스탬프를_적립_요청(owner, customer1, coupon1Id, coupon1StampCreateRequest); + + StampCreateRequest coupon2StampCreateRequest = new StampCreateRequest(21); + 쿠폰에_스탬프를_적립_요청(owner, customer2, coupon2Id, coupon2StampCreateRequest); + + // when + List customers = 고객_조회_요청(owner, savedCafeId) + + .thenReturn() + .jsonPath() + .getList("customers", CafeCustomerFindResponse.class); + + // then + CafeCustomerFindResponse customer1Expected = new CafeCustomerFindResponse( + coupon1Id, + customer1.getNickname(), + coupon1StampCreateRequest.getEarningStampCount(), + 0, + 1, + 10, + null, + true); + + CafeCustomerFindResponse customer2Expected = new CafeCustomerFindResponse( + coupon2Id, + customer2.getNickname(), + coupon2StampCreateRequest.getEarningStampCount() % 10, + coupon2StampCreateRequest.getEarningStampCount() / 10, + 1, + 10, + null, + true); + + assertThat(customers).usingRecursiveFieldByFieldElementComparatorIgnoringFields("visitCount", "firstVisitDate") + .containsExactlyInAnyOrder(customer1Expected, customer2Expected); + } + + @Test + void 인증정보_없이_특정_카페의_방문한_고객들의_정보를_조회하면_예외발생() { + // when + ExtractableResponse extract = given() + .log().all() + + .when() + .get("/api/admin/cafes/{cafeId}/customers", 1) + + .then() + .log().all() + .extract(); + + // then + int statusCode = extract.statusCode(); + assertThat(statusCode).isEqualTo(UNAUTHORIZED.value()); + } + + @Test + void 특정_고객의_특정_카페에_적립중인_쿠폰정보를_조회한다() { + // given + Owner owner = 사장_생성(); + Long savedCafeId = 카페_생성_요청하고_아이디_반환(owner, CAFE_CREATE_REQUEST); + + RegisterCustomer customer = registerCustomerRepository.save(new RegisterCustomer("name2", "phone2", "id2", "pw2")); + CouponCreateRequest couponCreateRequest = new CouponCreateRequest(savedCafeId); + Long oldCouponId = 쿠폰_생성_요청하고_아이디_반환(owner, couponCreateRequest, customer.getId()); + StampCreateRequest oldCouponStampCreate = new StampCreateRequest(3); + 쿠폰에_스탬프를_적립_요청(owner, customer, oldCouponId, oldCouponStampCreate); + + Long newCouponId = 쿠폰_생성_요청하고_아이디_반환(owner, couponCreateRequest, customer.getId()); + StampCreateRequest newCouponStampCreate = new StampCreateRequest(8); + 쿠폰에_스탬프를_적립_요청(owner, customer, newCouponId, newCouponStampCreate); + + // when + ExtractableResponse response = 고객의_쿠폰_조회_요청(savedCafeId, owner, customer); + CustomerAccumulatingCouponsFindResponse coupons = response.body().as(CustomerAccumulatingCouponsFindResponse.class); + // then + CustomerAccumulatingCouponFindResponse expected = new CustomerAccumulatingCouponFindResponse(newCouponId, + customer.getId(), + customer.getNickname(), + newCouponStampCreate.getEarningStampCount(), + null, + false, + 10); + + assertThat(coupons.getCoupons()).usingRecursiveFieldByFieldElementComparatorIgnoringFields("expireDate") + .containsExactlyInAnyOrder(expected); + } + + @Test + void 특정_카페의_특정_고객의_적립중인_쿠폰_조회_시_적립중인_쿠폰이_없으면_빈_리스트_반환() { + // given + Owner owner = 사장_생성(); + Long savedCafeId = 카페_생성_요청하고_아이디_반환(owner, CAFE_CREATE_REQUEST); + + RegisterCustomer customer = registerCustomerRepository.save(new RegisterCustomer("name2", "phone2", "id2", "pw2")); + CouponCreateRequest couponCreateRequest = new CouponCreateRequest(savedCafeId); + Long oldCouponId = 쿠폰_생성_요청하고_아이디_반환(owner, couponCreateRequest, customer.getId()); + StampCreateRequest oldCouponStampCreate = new StampCreateRequest(10); // 리워드 받고 쿠폰 만료됨 + 쿠폰에_스탬프를_적립_요청(owner, customer, oldCouponId, oldCouponStampCreate); + + // when + List coupons = 고객의_쿠폰_조회하고_결과_반환(owner, savedCafeId, customer); + + // then + assertThat(coupons).isEmpty(); + } + + @Test + void 카페_정책을_바꾸고_현재_적립_중인_쿠폰이_이전_정책의_쿠폰일때_isPrevious_true_확인() { + // given + Owner owner = 사장_생성(); + Long savedCafeId = 카페_생성_요청하고_아이디_반환(owner, CAFE_CREATE_REQUEST); + + RegisterCustomer customer = registerCustomerRepository.save(new RegisterCustomer("name2", "phone2", "id2", "pw2")); + CouponCreateRequest couponCreateRequest = new CouponCreateRequest(savedCafeId); + Long couponId = 쿠폰_생성_요청하고_아이디_반환(owner, couponCreateRequest, customer.getId()); + StampCreateRequest stampCreateRequest = new StampCreateRequest(3); + 쿠폰에_스탬프를_적립_요청(owner, customer, couponId, stampCreateRequest); + + CafeCouponSettingUpdateRequest updateRequest = CAFE_COUPON_SETTING_UPDATE_REQUEST; + 카페_쿠폰_정책_수정_요청(updateRequest, owner, savedCafeId); + + // when + ExtractableResponse response = 고객의_쿠폰_조회_요청(savedCafeId, owner, customer); + CustomerAccumulatingCouponsFindResponse coupons = response.body().as(CustomerAccumulatingCouponsFindResponse.class); + + // then + CustomerAccumulatingCouponFindResponse expected = new CustomerAccumulatingCouponFindResponse(couponId, + customer.getId(), + customer.getNickname(), + stampCreateRequest.getEarningStampCount(), + null, + true, + 10); + + assertThat(coupons.getCoupons()).usingRecursiveFieldByFieldElementComparatorIgnoringFields("expireDate") + .containsExactlyInAnyOrder(expected); + } + + private Owner 사장_생성() { + return ownerRepository.save(new Owner("owner", "id", "pw", "phone")); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerCouponFindAcceptanceTest.java b/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerCouponFindAcceptanceTest.java new file mode 100644 index 000000000..c925c6744 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerCouponFindAcceptanceTest.java @@ -0,0 +1,70 @@ +package com.stampcrush.backend.acceptance; + +import com.stampcrush.backend.api.manager.coupon.request.CouponCreateRequest; +import com.stampcrush.backend.api.manager.coupon.request.StampCreateRequest; +import com.stampcrush.backend.api.manager.coupon.response.CafeCustomerFindResponse; +import com.stampcrush.backend.api.manager.coupon.response.CafeCustomersFindResponse; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.repository.user.OwnerRepository; +import com.stampcrush.backend.repository.user.RegisterCustomerRepository; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import static com.stampcrush.backend.acceptance.step.ManagerCafeCreateStep.CAFE_CREATE_REQUEST; +import static com.stampcrush.backend.acceptance.step.ManagerCafeCreateStep.카페_생성_요청하고_아이디_반환; +import static com.stampcrush.backend.acceptance.step.ManagerCouponCreateStep.쿠폰_생성_요청하고_아이디_반환; +import static com.stampcrush.backend.acceptance.step.ManagerCustomerFindStep.고객_목록_조회_요청; +import static com.stampcrush.backend.acceptance.step.ManagerStampCreateStep.쿠폰에_스탬프를_적립_요청; +import static com.stampcrush.backend.fixture.CustomerFixture.REGISTER_CUSTOMER_GITCHAN; +import static com.stampcrush.backend.fixture.CustomerFixture.REGISTER_CUSTOMER_YOUNGHO; +import static com.stampcrush.backend.fixture.OwnerFixture.JENA; +import static org.assertj.core.api.Assertions.assertThat; + +public class ManagerCouponFindAcceptanceTest extends AcceptanceTest { + + @Autowired + private OwnerRepository ownerRepository; + + @Autowired + private RegisterCustomerRepository registerCustomerRepository; + + @Test + void 고객_목록을_조회한다() { + // given + Owner owner = ownerRepository.save(JENA); + + Long savedCafeId = 카페_생성_요청하고_아이디_반환(owner, CAFE_CREATE_REQUEST); + RegisterCustomer youngho = registerCustomerRepository.save(REGISTER_CUSTOMER_YOUNGHO); + RegisterCustomer gitchan = registerCustomerRepository.save(REGISTER_CUSTOMER_GITCHAN); + CouponCreateRequest request = new CouponCreateRequest(savedCafeId); + + Long couponId1 = 쿠폰_생성_요청하고_아이디_반환(owner, request, youngho.getId()); + Long couponId2 = 쿠폰_생성_요청하고_아이디_반환(owner, request, gitchan.getId()); + + StampCreateRequest stampCreateRequest1 = new StampCreateRequest(7); + StampCreateRequest stampCreateRequest2 = new StampCreateRequest(5); + + 쿠폰에_스탬프를_적립_요청(owner, youngho, couponId1, stampCreateRequest1); + 쿠폰에_스탬프를_적립_요청(owner, youngho, couponId1, stampCreateRequest2); + 쿠폰에_스탬프를_적립_요청(owner, gitchan, couponId2, stampCreateRequest1); + + String firstVisitDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd")); + + // when + CafeCustomerFindResponse expected1 = new CafeCustomerFindResponse(1L, youngho.getNickname(), 2, 1, 2, 10, firstVisitDate, true); + CafeCustomerFindResponse expected2 = new CafeCustomerFindResponse(2L, gitchan.getNickname(), 7, 0, 1, 10, firstVisitDate, true); + + ExtractableResponse response = 고객_목록_조회_요청(owner, savedCafeId); + + CafeCustomersFindResponse actual = response.body().as(CafeCustomersFindResponse.class); + + // then + assertThat(actual.getCustomers()).containsExactlyInAnyOrder(expected1, expected2); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerCustomerCommandAcceptanceTest.java b/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerCustomerCommandAcceptanceTest.java new file mode 100644 index 000000000..60de3dda6 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerCustomerCommandAcceptanceTest.java @@ -0,0 +1,161 @@ +package com.stampcrush.backend.acceptance; + +import com.stampcrush.backend.api.manager.customer.request.TemporaryCustomerCreateRequest; +import com.stampcrush.backend.api.manager.customer.response.CustomerFindResponse; +import com.stampcrush.backend.api.manager.customer.response.CustomersFindResponse; +import com.stampcrush.backend.application.manager.customer.dto.CustomerFindDto; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.entity.user.TemporaryCustomer; +import com.stampcrush.backend.repository.user.CustomerRepository; +import com.stampcrush.backend.repository.user.OwnerRepository; +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import static com.stampcrush.backend.fixture.OwnerFixture.OWNER3; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.OK; + +public class ManagerCustomerCommandAcceptanceTest extends AcceptanceTest { + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private OwnerRepository ownerRepository; + + private Owner owner; + + @BeforeEach + void setUp() { + owner = ownerRepository.save(OWNER3); + } + + @Test + void 전화번호로_가입_고객을_조회한다() { + // given + Customer customer = new RegisterCustomer("제나", "01012345678", "jena", "1234"); + customerRepository.save(customer); + + // when + ExtractableResponse response = requestFindCustomerByPhoneNumber(owner, "01012345678"); + CustomersFindResponse customers = response.body().as(CustomersFindResponse.class); + + // then + assertSoftly(softAssertions -> { + softAssertions.assertThat(response.statusCode()).isEqualTo(OK.value()); + softAssertions.assertThat(customers.getCustomer()).containsExactlyInAnyOrder(CustomerFindResponse.from(CustomerFindDto.from(customer))); + }); + } + + @Test + void 전화번호로_임시_고객을_조회한다() { + // given + Customer customer = TemporaryCustomer.from("01012345678"); + customerRepository.save(customer); + + // when + ExtractableResponse response = requestFindCustomerByPhoneNumber(owner, "01012345678"); + CustomersFindResponse customers = response.body().as(CustomersFindResponse.class); + + // then + assertSoftly(softAssertions -> { + softAssertions.assertThat(response.statusCode()).isEqualTo(OK.value()); + softAssertions.assertThat(customers.getCustomer()).containsExactly(CustomerFindResponse.from(CustomerFindDto.from(customer))); + }); + } + + @Test + void 고객이_존재하지_않는_경우_빈_배열을_반환한다() { + // given, when + ExtractableResponse response = requestFindCustomerByPhoneNumber(owner, "01012345678"); + CustomersFindResponse customers = response.body().as(CustomersFindResponse.class); + + // then + assertSoftly(softAssertions -> { + softAssertions.assertThat(response.statusCode()).isEqualTo(OK.value()); + softAssertions.assertThat(customers.getCustomer().size()).isEqualTo(0); + }); + } + + @Test + void 임시_고객을_생성한다() { + // given + TemporaryCustomerCreateRequest temporaryCustomerCreateRequest = new TemporaryCustomerCreateRequest("01012345678"); + + // when + Long temporaryCustomerId = createTemporaryCustomer(owner, temporaryCustomerCreateRequest); + Customer temporaryCustomer = customerRepository.findById(temporaryCustomerId).get(); + + // then + assertSoftly(softly -> { + softly.assertThat(temporaryCustomer.getNickname()).isEqualTo("5678"); + softly.assertThat(temporaryCustomer.getPhoneNumber()).isEqualTo("01012345678"); + }); + } + + @Test + void 존재하는_회원의_번호로_고객을_생성하려면_에러를_발생한다() { + // given + Customer customer = TemporaryCustomer.from("01012345678"); + customerRepository.save(customer); + TemporaryCustomerCreateRequest temporaryCustomerCreateRequest = new TemporaryCustomerCreateRequest(customer.getPhoneNumber()); + + // when, then + RestAssured.given() + .log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(temporaryCustomerCreateRequest) + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + + .when() + .post("/api/admin/temporary-customers") + + .then() + .statusCode(BAD_REQUEST.value()) + .extract(); + } + + private ExtractableResponse requestFindCustomerByPhoneNumber(Owner owner, String phoneNumber) { + return RestAssured.given(). + log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .param("phone-number", phoneNumber) + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + + .when() + .get("/api/admin/customers") + + .then() + .log().all() + .extract(); + } + + private Long createTemporaryCustomer(Owner owner, TemporaryCustomerCreateRequest request) { + ExtractableResponse response = RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + + .when() + .post("/api/admin/temporary-customers") + + .then() + .statusCode(HttpStatus.CREATED.value()) + .extract(); + + return getIdFromCreatedResponse(response); + } + + private long getIdFromCreatedResponse(ExtractableResponse response) { + return Long.parseLong(response.header("Location").split("/")[2]); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerRewardCommandAcceptanceTest.java b/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerRewardCommandAcceptanceTest.java new file mode 100644 index 000000000..6e0f98587 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerRewardCommandAcceptanceTest.java @@ -0,0 +1,181 @@ +package com.stampcrush.backend.acceptance; + +import com.stampcrush.backend.api.manager.cafe.request.CafeCreateRequest; +import com.stampcrush.backend.api.manager.coupon.request.CouponCreateRequest; +import com.stampcrush.backend.api.manager.coupon.request.StampCreateRequest; +import com.stampcrush.backend.api.manager.reward.request.RewardUsedUpdateRequest; +import com.stampcrush.backend.api.manager.reward.response.RewardFindResponse; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.entity.user.TemporaryCustomer; +import com.stampcrush.backend.repository.user.OwnerRepository; +import com.stampcrush.backend.repository.user.RegisterCustomerRepository; +import com.stampcrush.backend.repository.user.TemporaryCustomerRepository; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static org.assertj.core.api.Assertions.assertThat; + +public class ManagerRewardCommandAcceptanceTest extends AcceptanceTest { + + // TODO 회원가입, 로그인 구현 후 제거 + @Autowired + private OwnerRepository ownerRepository; + + // TODO 회원가입, 로그인 구현 후 제거 + @Autowired + private RegisterCustomerRepository registerCustomerRepository; + + // TODO 회원가입, 로그인 구현 후 제거 + @Autowired + private TemporaryCustomerRepository temporaryCustomerRepository; + + @Test + void 카페사장이_가입_회원의_리워드를_사용한다() { + // given + Customer customer = 가입_회원_생성_후_가입_고객_반환(); + Owner owner = 카페_사장_생성_후_사장_반환(); + CafeCreateRequest cafeCreateRequest = new CafeCreateRequest("cafe", "잠실", "루터회관", "111111111"); + Long cafeId = 카페_생성_후_카페_아이디_반환(owner, cafeCreateRequest); + CouponCreateRequest couponCreateRequest = new CouponCreateRequest(cafeId); + Long couponId = 쿠폰_생성_후_쿠폰_아이디_반환(owner, couponCreateRequest, customer.getId()); + StampCreateRequest stampCreateRequest = new StampCreateRequest(10); + 스탬프_찍은_후_리워드_생성(owner, customer.getId(), couponId, stampCreateRequest); + List rewards = 리워드_목록_조회(owner, cafeId, customer.getId()); + Long rewardId = rewards.get(0).getId(); + + //when + RewardUsedUpdateRequest request = new RewardUsedUpdateRequest(cafeId, true); + 리워드_사용(owner, request, customer.getId(), rewardId); + List restRewards = 리워드_목록_조회(owner, cafeId, customer.getId()); + + // then + SoftAssertions softAssertions = new SoftAssertions(); + softAssertions.assertThat(rewards.size()).isEqualTo(1); + softAssertions.assertThat(restRewards.size()).isEqualTo(0); + softAssertions.assertAll(); + } + + @Test + void 카페사장이_임시_회원의_리워드를_사용할_수_없다() { + // given + Customer customer = 임시_회원_생성_후_가입_고객_반환(); + Owner owner = 카페_사장_생성_후_사장_반환(); + CafeCreateRequest cafeCreateRequest = new CafeCreateRequest("cafe", "잠실", "루터회관", "111111111"); + Long cafeId = 카페_생성_후_카페_아이디_반환(owner, cafeCreateRequest); + CouponCreateRequest couponCreateRequest = new CouponCreateRequest(cafeId); + Long couponId = 쿠폰_생성_후_쿠폰_아이디_반환(owner, couponCreateRequest, customer.getId()); + StampCreateRequest stampCreateRequest = new StampCreateRequest(10); + 스탬프_찍은_후_리워드_생성(owner, customer.getId(), couponId, stampCreateRequest); + List rewards = 리워드_목록_조회(owner, cafeId, customer.getId()); + Long rewardId = rewards.get(0).getId(); + + //when + RewardUsedUpdateRequest request = new RewardUsedUpdateRequest(cafeId, true); + 리워드_사용(owner, request, customer.getId(), rewardId); + List restRewards = 리워드_목록_조회(owner, cafeId, customer.getId()); + + // then + SoftAssertions softAssertions = new SoftAssertions(); + softAssertions.assertThat(rewards.size()).isEqualTo(1); + softAssertions.assertThat(restRewards.size()).isEqualTo(1); + softAssertions.assertAll(); + } + + @Test + void 카페사장이_가입_회원의_리워드를_조회한다() { + // given + Customer customer = 가입_회원_생성_후_가입_고객_반환(); + Owner owner = 카페_사장_생성_후_사장_반환(); + CafeCreateRequest cafeCreateRequest = new CafeCreateRequest("cafe", "잠실", "루터회관", "111111111"); + Long cafeId = 카페_생성_후_카페_아이디_반환(owner, cafeCreateRequest); + CouponCreateRequest couponCreateRequest = new CouponCreateRequest(cafeId); + Long couponId = 쿠폰_생성_후_쿠폰_아이디_반환(owner, couponCreateRequest, customer.getId()); + StampCreateRequest stampCreateRequest = new StampCreateRequest(10); + 스탬프_찍은_후_리워드_생성(owner, customer.getId(), couponId, stampCreateRequest); + List all = temporaryCustomerRepository.findAll(); + + // when + List rewards = 리워드_목록_조회(owner, cafeId, customer.getId()); + + // then + assertThat(rewards.size()).isEqualTo(1); + } + + // TODO 회원가입, 로그인 구현 후 API CAll 로 대체 + private Customer 가입_회원_생성_후_가입_고객_반환() { + return registerCustomerRepository.save(new RegisterCustomer("leo", "01022222222", "leoId", "5678")); + } + + // TODO 회원가입, 로그인 구현 후 API CAll 로 대체 + private Customer 임시_회원_생성_후_가입_고객_반환() { + return temporaryCustomerRepository.save(TemporaryCustomer.from("01011111111")); + } + + // TODO 회원가입, 로그인 구현 후 API CAll 로 대체 + private Owner 카페_사장_생성_후_사장_반환() { + return ownerRepository.save(new Owner("hardy", "hardyId", "1234", "01011111111")); + } + + private Long 카페_생성_후_카페_아이디_반환(Owner owner, CafeCreateRequest cafeCreateRequest) { + return Long.valueOf( + given() + .contentType(JSON) + .body(cafeCreateRequest) + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + .when() + .post("/api/admin/cafes") + .thenReturn() + .header("Location") + .split("/")[2]); + } + + private Long 쿠폰_생성_후_쿠폰_아이디_반환(Owner owner, CouponCreateRequest couponCreateRequest, Long customerId) { + return given() + .contentType(JSON) + .body(couponCreateRequest) + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + .when() + .post("/api/admin/customers/" + customerId + "/coupons") + .thenReturn() + .jsonPath() + .getLong("couponId"); + } + + private void 스탬프_찍은_후_리워드_생성(Owner owner, Long customerId, Long couponId, StampCreateRequest stampCreateRequest) { + given() + .contentType(JSON) + .body(stampCreateRequest) + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + .when() + .post("/api/admin/customers/" + customerId + "/coupons/" + couponId + "/stamps"); + } + + private List 리워드_목록_조회(Owner owner, Long cafeId, Long customerId) { + return given() + .queryParam("cafe-id", cafeId) + .queryParam("used", false) + .contentType(JSON) + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + .when() + .get("/api/admin/customers/" + customerId + "/rewards") + .thenReturn() + .jsonPath() + .getList("rewards", RewardFindResponse.class); + } + + private void 리워드_사용(Owner owner, RewardUsedUpdateRequest rewardUsedUpdateRequest, Long customerId, Long rewardId) { + given() + .contentType(JSON) + .body(rewardUsedUpdateRequest) + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + .when() + .patch("/api/admin/customers/" + customerId + "/rewards/" + rewardId); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerSampleCouponFindAcceptanceTest.java b/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerSampleCouponFindAcceptanceTest.java new file mode 100644 index 000000000..83d04d4d8 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/ManagerSampleCouponFindAcceptanceTest.java @@ -0,0 +1,121 @@ +package com.stampcrush.backend.acceptance; + +import com.stampcrush.backend.entity.sample.SampleBackImage; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.repository.sample.SampleBackImageRepository; +import com.stampcrush.backend.repository.sample.SampleFrontImageRepository; +import com.stampcrush.backend.repository.sample.SampleStampCoordinateRepository; +import com.stampcrush.backend.repository.sample.SampleStampImageRepository; +import com.stampcrush.backend.repository.user.OwnerRepository; +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import static com.stampcrush.backend.fixture.OwnerFixture.OWNER3; +import static com.stampcrush.backend.fixture.SampleCouponFixture.*; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +public class ManagerSampleCouponFindAcceptanceTest extends AcceptanceTest { + + @Autowired + private SampleFrontImageRepository sampleFrontImageRepository; + + @Autowired + private SampleBackImageRepository sampleBackImageRepository; + + @Autowired + private SampleStampCoordinateRepository sampleStampCoordinateRepository; + + @Autowired + private SampleStampImageRepository sampleStampImageRepository; + + @Autowired + private OwnerRepository ownerRepository; + + private Owner owner; + + @BeforeEach + void setUp() { + sampleFrontImageRepository.save(SAMPLE_FRONT_IMAGE); + SampleBackImage savedSampleBackImage = sampleBackImageRepository.save(SAMPLE_BACK_IMAGE); + SAMPLE_COORDINATES_SIZE_EIGHT.stream() + .peek(e -> e.setSampleBackImage(savedSampleBackImage)) + .forEach(e -> sampleStampCoordinateRepository.save(e)); + sampleStampImageRepository.save(SAMPLE_STAMP_IMAGE); + + owner = ownerRepository.save(OWNER3); + } + + @Disabled + @Test + void 전체_쿠폰_샘플을_조회한다() { + // given, when + ExtractableResponse response = RestAssured.given() + .log().all() + + .when() + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + .get("/api/admin/coupon-samples") + + .then() + .extract(); + + // then + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(response.jsonPath().getList("sampleFrontImages").size()).isEqualTo(1), + () -> assertThat(response.jsonPath().getList("sampleBackImages").size()).isEqualTo(1), + () -> assertThat(response.jsonPath().getList("sampleStampImages").size()).isEqualTo(1) + ); + } + + @Test + void 최대_스탬프_개수로_쿠폰_샘플을_필터링해서_조회한다() { + // given, when + ExtractableResponse response = RestAssured.given() + .log().all() + + .when() + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + .get("/api/admin/coupon-samples?max-stamp-count=8") + + .then() + .extract(); + + // then + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(response.jsonPath().getList("sampleFrontImages").size()).isEqualTo(1), + () -> assertThat(response.jsonPath().getList("sampleBackImages").size()).isEqualTo(1), + () -> assertThat(response.jsonPath().getList("sampleStampImages").size()).isEqualTo(1) + ); + } + + @Test + void 해당하는_최대_스탬프_개수의_샘플_쿠폰이_없으면_뒷면이_조회되지_않는다() { + // given, when + ExtractableResponse response = RestAssured.given() + .log().all() + + .when() + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + .get("/api/admin/coupon-samples?max-stamp-count=10") + + .then() + .extract(); + + // then + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(response.jsonPath().getList("sampleFrontImages").size()).isEqualTo(1), + () -> assertThat(response.jsonPath().getList("sampleBackImages").size()).isEqualTo(0), + () -> assertThat(response.jsonPath().getList("sampleStampImages").size()).isEqualTo(1) + ); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/VisitorCafeFindAcceptanceTest.java b/backend/src/test/java/com/stampcrush/backend/acceptance/VisitorCafeFindAcceptanceTest.java new file mode 100644 index 000000000..7abc93ab3 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/VisitorCafeFindAcceptanceTest.java @@ -0,0 +1,61 @@ +package com.stampcrush.backend.acceptance; + +import com.stampcrush.backend.api.visitor.cafe.response.CafeInfoFindByCustomerResponse; +import com.stampcrush.backend.api.visitor.cafe.response.CafeInfoFindResponse; +import com.stampcrush.backend.application.visitor.cafe.dto.CafeInfoFindByCustomerResultDto; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.fixture.CustomerFixture; +import com.stampcrush.backend.fixture.OwnerFixture; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.user.CustomerRepository; +import com.stampcrush.backend.repository.user.OwnerRepository; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static com.stampcrush.backend.acceptance.step.VisitorCafeFindStep.고객의_카페_정보_조회_요청; +import static com.stampcrush.backend.fixture.CafeFixture.cafeOfSavedOwner; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +public class VisitorCafeFindAcceptanceTest extends AcceptanceTest { + + @Autowired + private CafeRepository cafeRepository; + + @Autowired + private OwnerRepository ownerRepository; + + @Autowired + private CustomerRepository customerRepository; + + @Test + void 고객이_카페정보를_조회한다() { + // given + Customer customer = customerRepository.save(CustomerFixture.REGISTER_CUSTOMER_JENA); + Cafe savedCafe = cafeRepository.save(cafeOfSavedOwner(ownerRepository.save(OwnerFixture.GITCHAN)) + ); + + // when + ExtractableResponse response = 고객의_카페_정보_조회_요청((RegisterCustomer) customer, savedCafe.getId()); + CafeInfoFindByCustomerResponse cafeInfoFindByCustomerResponse = response.body().as(CafeInfoFindByCustomerResponse.class); + + // then + assertThat(cafeInfoFindByCustomerResponse.getCafe()) + .usingRecursiveComparison() + .isEqualTo(CafeInfoFindResponse.from(CafeInfoFindByCustomerResultDto.from(savedCafe))); + } + + @Test + void 고객이_존재하지_않는_카페를_조회하면_에러가_발생한다() { + Customer customer = customerRepository.save(CustomerFixture.REGISTER_CUSTOMER_JENA); + + long NOT_EXIST_CAFE_ID = 1L; + ExtractableResponse response = 고객의_카페_정보_조회_요청((RegisterCustomer) customer, NOT_EXIST_CAFE_ID); + + assertThat(response.statusCode()).isEqualTo(NOT_FOUND.value()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/VisitorCouponFindAcceptanceTest.java b/backend/src/test/java/com/stampcrush/backend/acceptance/VisitorCouponFindAcceptanceTest.java new file mode 100644 index 000000000..a180abf67 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/VisitorCouponFindAcceptanceTest.java @@ -0,0 +1,126 @@ +package com.stampcrush.backend.acceptance; + +import com.stampcrush.backend.api.manager.coupon.request.CouponCreateRequest; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.coupon.Coupon; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.coupon.CouponRepository; +import com.stampcrush.backend.repository.user.CustomerRepository; +import com.stampcrush.backend.repository.user.OwnerRepository; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import static com.stampcrush.backend.acceptance.step.ManagerCafeCouponSettingUpdateStep.CAFE_COUPON_SETTING_UPDATE_REQUEST; +import static com.stampcrush.backend.acceptance.step.ManagerCafeCouponSettingUpdateStep.카페_쿠폰_정책_수정_요청; +import static com.stampcrush.backend.acceptance.step.ManagerCafeCreateStep.CAFE_CREATE_REQUEST; +import static com.stampcrush.backend.acceptance.step.ManagerCafeCreateStep.카페_생성_요청하고_아이디_반환; +import static com.stampcrush.backend.acceptance.step.ManagerCouponCreateStep.쿠폰_생성_요청하고_아이디_반환; +import static com.stampcrush.backend.acceptance.step.VisitorCouponFindStep.고객의_쿠폰_카페별로_1개씩_조회_요청; +import static com.stampcrush.backend.fixture.OwnerFixture.GITCHAN; +import static com.stampcrush.backend.fixture.OwnerFixture.JENA; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +public class VisitorCouponFindAcceptanceTest extends AcceptanceTest { + + private static final RegisterCustomer REGISTER_CUSTOMER = new RegisterCustomer("깃짱", "01012345678", "customer1", "customer1"); + + @Autowired + private OwnerRepository ownerRepository; + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private CafeRepository cafeRepository; + + @Autowired + private CouponRepository couponRepository; + + @Test + void 카페당_하나의_쿠폰을_조회할_수_있다() { + // given + + // TODO: Owner에 대한 회원가입 로직이 생기면 요청으로 대체한다. + Owner gitchan = ownerRepository.save(GITCHAN); + Owner jena = ownerRepository.save(JENA); + + RegisterCustomer customer = customerRepository.save(REGISTER_CUSTOMER); + + Long gitchanCafeId = 카페_생성_요청하고_아이디_반환(gitchan, CAFE_CREATE_REQUEST); + Cafe gitchanCafe = cafeRepository.findById(gitchanCafeId).get(); + 카페_쿠폰_정책_수정_요청(CAFE_COUPON_SETTING_UPDATE_REQUEST, gitchan, gitchanCafeId); + + Long jenaCafeId = 카페_생성_요청하고_아이디_반환(jena, CAFE_CREATE_REQUEST); + Cafe jenaCafe = cafeRepository.findById(jenaCafeId).get(); + 카페_쿠폰_정책_수정_요청(CAFE_COUPON_SETTING_UPDATE_REQUEST, jena, jenaCafeId); + + Long gitchanCafeCouponId = 쿠폰_생성_요청하고_아이디_반환(gitchan, new CouponCreateRequest(gitchanCafeId), customer.getId()); + Coupon gitchanCafeCoupon = couponRepository.findById(gitchanCafeCouponId).get(); + + Long jenaCafeCouponId = 쿠폰_생성_요청하고_아이디_반환(jena, new CouponCreateRequest(jenaCafeId), customer.getId()); + Coupon jenaCafeCoupon = couponRepository.findById(jenaCafeCouponId).get(); + + // when + ExtractableResponse response = 고객의_쿠폰_카페별로_1개씩_조회_요청(customer); + + // then + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(response.jsonPath().getList("coupons")).isNotEmpty(), + () -> assertThat(response.jsonPath().getList("coupons").size()).isEqualTo(2), + () -> assertThat(response.jsonPath().getLong("coupons[0].cafeInfo.id")).isEqualTo(gitchanCafeId), + () -> assertThat(response.jsonPath().getString("coupons[0].cafeInfo.name")).isEqualTo(gitchanCafe.getName()), + () -> assertThat(response.jsonPath().getLong("coupons[0].couponInfos[0].id")).isEqualTo(gitchanCafeCouponId), + () -> assertThat(response.jsonPath().getString("coupons[0].couponInfos[0].status")).isEqualTo(gitchanCafeCoupon.getStatus().name()), + () -> assertThat(response.jsonPath().getList("coupons[0].couponInfos[0].coordinates")).isNotEmpty(), + () -> assertThat(response.jsonPath().getLong("coupons[1].cafeInfo.id")).isEqualTo(jenaCafeCouponId), + () -> assertThat(response.jsonPath().getLong("coupons[1].couponInfos[0].id")).isEqualTo(jenaCafeCouponId) + ); + } + + @Test + @Disabled + void 여러_개의_쿠폰이_있는_경우_ACCUMULATING인_쿠폰만_조회된다() { + // given + + // TODO: Owner에 대한 회원가입 로직이 생기면 요청으로 대체한다. + Owner gitchan = ownerRepository.save(GITCHAN); + Owner jena = ownerRepository.save(JENA); + + RegisterCustomer customer = customerRepository.save(REGISTER_CUSTOMER); + + Long gitchanCafeId = 카페_생성_요청하고_아이디_반환(gitchan, CAFE_CREATE_REQUEST); + Long jenaCafeId = 카페_생성_요청하고_아이디_반환(jena, CAFE_CREATE_REQUEST); + + Long gitchanCafeCouponId = 쿠폰_생성_요청하고_아이디_반환(gitchan, new CouponCreateRequest(gitchanCafeId), customer.getId()); + Long jenaCafeCouponId = 쿠폰_생성_요청하고_아이디_반환(jena, new CouponCreateRequest(jenaCafeId), customer.getId()); + accumulateCouponUntilRewarded(gitchanCafeCouponId); + + // when + ExtractableResponse response = 고객의_쿠폰_카페별로_1개씩_조회_요청(customer); + + // then + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(response.jsonPath().getList("coupons")).isNotEmpty(), + () -> assertThat(response.jsonPath().getList("coupons").size()).isEqualTo(1), + () -> assertThat(response.jsonPath().getLong("coupons[0].cafeInfo.id")).isEqualTo(jenaCafeId), + () -> assertThat(response.jsonPath().getLong("coupons[0].couponInfos[0].id")).isEqualTo(jenaCafeCouponId) + ); + } + + private void accumulateCouponUntilRewarded(Long couponId) { + Coupon gitchanCafeCoupon = couponRepository.findById(couponId).get(); + int couponMaxStampCount = gitchanCafeCoupon.getCouponMaxStampCount(); + for (int i = 0; i < couponMaxStampCount; i++) { + gitchanCafeCoupon.accumulate(1); + } + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/VisitorFavoritesCommandAcceptanceTest.java b/backend/src/test/java/com/stampcrush/backend/acceptance/VisitorFavoritesCommandAcceptanceTest.java new file mode 100644 index 000000000..03e444237 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/VisitorFavoritesCommandAcceptanceTest.java @@ -0,0 +1,117 @@ +package com.stampcrush.backend.acceptance; + +import com.stampcrush.backend.api.manager.cafe.request.CafeCreateRequest; +import com.stampcrush.backend.api.manager.reward.request.RewardUsedUpdateRequest; +import com.stampcrush.backend.api.manager.reward.response.RewardFindResponse; +import com.stampcrush.backend.api.visitor.favorites.request.FavoritesUpdateRequest; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.entity.user.TemporaryCustomer; +import com.stampcrush.backend.repository.user.OwnerRepository; +import com.stampcrush.backend.repository.user.RegisterCustomerRepository; +import com.stampcrush.backend.repository.user.TemporaryCustomerRepository; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import java.util.List; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static org.assertj.core.api.Assertions.assertThat; + +public class VisitorFavoritesCommandAcceptanceTest extends AcceptanceTest { + + // TODO 회원가입, 로그인 구현 후 제거 + @Autowired + private OwnerRepository ownerRepository; + + // TODO 회원가입, 로그인 구현 후 제거 + @Autowired + private RegisterCustomerRepository registerCustomerRepository; + + // TODO 회원가입, 로그인 구현 후 제거 + @Autowired + private TemporaryCustomerRepository temporaryCustomerRepository; + + @Test + void 즐겨찾기를_등록한다() { + // given + RegisterCustomer customer = 가입_회원_생성_후_가입_고객_반환(); + Owner owner = 카페_사장_생성_후_사장_반환(); + CafeCreateRequest cafeCreateRequest = new CafeCreateRequest("cafe", "잠실", "루터회관", "111111111"); + Long cafeId = 카페_생성_후_카페_아이디_반환(owner, cafeCreateRequest); + FavoritesUpdateRequest request = new FavoritesUpdateRequest(Boolean.TRUE); + + // when + int statusCode = 즐겨찾기_등록(request, customer, cafeId) + .statusCode(); + + // then + assertThat(statusCode).isEqualTo(HttpStatus.OK.value()); + } + + // TODO 회원가입, 로그인 구현 후 API CAll 로 대체 + private RegisterCustomer 가입_회원_생성_후_가입_고객_반환() { + return registerCustomerRepository.save(new RegisterCustomer("leo", "01022222222", "leoId", "5678")); + } + + // TODO 회원가입, 로그인 구현 후 API CAll 로 대체 + private Customer 임시_회원_생성_후_가입_고객_반환() { + return temporaryCustomerRepository.save(TemporaryCustomer.from("01011111111")); + } + + // TODO 회원가입, 로그인 구현 후 API CAll 로 대체 + private Owner 카페_사장_생성_후_사장_반환() { + return ownerRepository.save(new Owner("hardy", "hardyId", "1234", "01011111111")); + } + + private Long 카페_생성_후_카페_아이디_반환(Owner owner, CafeCreateRequest cafeCreateRequest) { + return Long.valueOf( + given() + .contentType(JSON) + .body(cafeCreateRequest) + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + .when() + .post("/api/admin/cafes") + .thenReturn() + .header("Location") + .split("/")[2]); + } + + private List 리워드_목록_조회(Owner owner, Long cafeId, Long customerId) { + return given() + .queryParam("cafe-id", cafeId) + .queryParam("used", false) + .contentType(JSON) + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + .when() + .get("/api/admin/customers/" + customerId + "/rewards") + .thenReturn() + .jsonPath() + .getList("rewards", RewardFindResponse.class); + } + + private void 리워드_사용(Owner owner, RewardUsedUpdateRequest rewardUsedUpdateRequest, Long customerId, Long rewardId) { + given() + .contentType(JSON) + .body(rewardUsedUpdateRequest) + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + .when() + .patch("/api/admin/customers/" + customerId + "/rewards/" + rewardId); + } + + private ExtractableResponse 즐겨찾기_등록(FavoritesUpdateRequest favoritesUpdateRequest, RegisterCustomer customer, Long cafeId) { + return given() + .contentType(JSON) + .body(favoritesUpdateRequest) + .auth().preemptive().basic(customer.getLoginId(), customer.getEncryptedPassword()) + .when() + .post("/api/cafes/" + cafeId + "/favorites") + .then() + .extract(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/VisitorProfileFindAcceptanceTest.java b/backend/src/test/java/com/stampcrush/backend/acceptance/VisitorProfileFindAcceptanceTest.java new file mode 100644 index 000000000..be2eb4144 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/VisitorProfileFindAcceptanceTest.java @@ -0,0 +1,85 @@ +package com.stampcrush.backend.acceptance; + +import com.stampcrush.backend.api.visitor.profile.response.VisitorProfilesFindResponse; +import com.stampcrush.backend.application.visitor.profile.dto.VisitorProfileFindResultDto; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.repository.user.RegisterCustomerRepository; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import static com.stampcrush.backend.fixture.CustomerFixture.REGISTER_CUSTOMER_GITCHAN; +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +public class VisitorProfileFindAcceptanceTest extends AcceptanceTest { + + @Autowired + private RegisterCustomerRepository registerCustomerRepository; + + @Test + void 고객의_프로필을_조회한다() { + // given + RegisterCustomer customer = registerCustomerRepository.save(REGISTER_CUSTOMER_GITCHAN); + + ExtractableResponse response = given() + .log().all() + .auth().preemptive() + .basic(customer.getLoginId(), customer.getEncryptedPassword()) + + .when() + .get("/api/profiles") + + .then() + .log().all() + .extract(); + + // when + VisitorProfilesFindResponse expected = VisitorProfilesFindResponse.from( + new VisitorProfileFindResultDto(customer.getId(), + customer.getNickname(), customer.getPhoneNumber(), customer.getEmail())); + VisitorProfilesFindResponse result = response.body().as(VisitorProfilesFindResponse.class); + + // then + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(result).usingRecursiveComparison().isEqualTo(expected) + ); + } + + @Test + void 고객의_전화번호가_등록되어있지_않을때_전화번호_필드가_null이다() { + // given + RegisterCustomer customer = RegisterCustomer.builder().nickname("jena").build(); + customer.registerLoginId("jenaId"); + customer.registerEncryptedPassword("jenaPw"); + RegisterCustomer savedCustomer = registerCustomerRepository.save(customer); + + ExtractableResponse response = given() + .log().all() + .auth().preemptive() + .basic(customer.getLoginId(), customer.getEncryptedPassword()) + + .when() + .get("/api/profiles") + + .then() + .log().all() + .extract(); + + // when + VisitorProfilesFindResponse expected = VisitorProfilesFindResponse.from( + new VisitorProfileFindResultDto(customer.getId(), + customer.getNickname(), customer.getPhoneNumber(), customer.getEmail())); + VisitorProfilesFindResponse result = response.body().as(VisitorProfilesFindResponse.class); + + // then + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(result).usingRecursiveComparison().isEqualTo(expected) + ); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/VisitorProfilesCommandAcceptanceTest.java b/backend/src/test/java/com/stampcrush/backend/acceptance/VisitorProfilesCommandAcceptanceTest.java new file mode 100644 index 000000000..a8a728f09 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/VisitorProfilesCommandAcceptanceTest.java @@ -0,0 +1,77 @@ +package com.stampcrush.backend.acceptance; + +import com.stampcrush.backend.api.visitor.profile.request.VisitorProfilesPhoneNumberUpdateRequest; +import com.stampcrush.backend.auth.OAuthProvider; +import com.stampcrush.backend.auth.request.OAuthRegisterCustomerCreateRequest; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.repository.user.CustomerRepository; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityTransaction; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import static com.stampcrush.backend.acceptance.step.VisitorJoinStep.회원_가입_요청하고_액세스_토큰_반환; +import static com.stampcrush.backend.acceptance.step.VisitorProfilesCommandStep.고객의_전화번호_등록_요청; +import static com.stampcrush.backend.acceptance.step.VisitorProfilesCommandStep.고객의_전화번호_등록_요청_token; +import static com.stampcrush.backend.fixture.CustomerFixture.REGISTER_CUSTOMER_GITCHAN; +import static org.assertj.core.api.Assertions.assertThat; + +public class VisitorProfilesCommandAcceptanceTest extends AcceptanceTest { + + private static final RegisterCustomer OAUTH_REGISTER_CUSTOMER = RegisterCustomer + .builder() + .nickname("깃짱") + .email("gitchan@naver.com") + .oAuthId(1L) + .oAuthProvider(OAuthProvider.KAKAO) + .build(); + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private EntityManager entityManager; + + @Test + void 고객의_전화번호를_저장한다() { + RegisterCustomer customer = OAUTH_REGISTER_CUSTOMER; + OAuthRegisterCustomerCreateRequest request = new OAuthRegisterCustomerCreateRequest( + customer.getNickname(), + customer.getEmail(), + customer.getOAuthProvider(), + customer.getOAuthId() + ); + + String accessToken = 회원_가입_요청하고_액세스_토큰_반환(request); + + ExtractableResponse response = 고객의_전화번호_등록_요청_token(accessToken, new VisitorProfilesPhoneNumberUpdateRequest("01012345678")); + + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + + @Test + @Disabled + // TODO: 상황 연출이 어려움....! + void 중복되는_전화번호로_요청하면_예외가_발생한다() { + EntityTransaction transaction = entityManager.getTransaction(); + + transaction.begin(); + RegisterCustomer recentCustomer = customerRepository.save(REGISTER_CUSTOMER_GITCHAN); + transaction.commit(); + + transaction.begin(); + RegisterCustomer newOAuthRegisterCustomer = OAUTH_REGISTER_CUSTOMER; + newOAuthRegisterCustomer.registerLoginId("loginId"); + newOAuthRegisterCustomer.registerEncryptedPassword("password"); + RegisterCustomer newCustomer = customerRepository.save(newOAuthRegisterCustomer); + transaction.commit(); + + ExtractableResponse response = 고객의_전화번호_등록_요청(newCustomer, new VisitorProfilesPhoneNumberUpdateRequest(recentCustomer.getPhoneNumber())); + + assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerCafeCouponSettingUpdateStep.java b/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerCafeCouponSettingUpdateStep.java new file mode 100644 index 000000000..e2e34962c --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerCafeCouponSettingUpdateStep.java @@ -0,0 +1,42 @@ +package com.stampcrush.backend.acceptance.step; + +import com.stampcrush.backend.api.manager.cafe.request.CafeCouponSettingUpdateRequest; +import com.stampcrush.backend.entity.user.Owner; +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; + +import java.util.List; + +import static io.restassured.http.ContentType.JSON; + +public class ManagerCafeCouponSettingUpdateStep { + + public static final CafeCouponSettingUpdateRequest CAFE_COUPON_SETTING_UPDATE_REQUEST = new CafeCouponSettingUpdateRequest( + "frontImageUrl", + "backImageUrl", + "stampImageUrl", + List.of( + new CafeCouponSettingUpdateRequest.CouponStampCoordinateRequest(1, 1, 1), + new CafeCouponSettingUpdateRequest.CouponStampCoordinateRequest(2, 2, 2) + ), + "reward", + 6 + ); + + public static ExtractableResponse 카페_쿠폰_정책_수정_요청(CafeCouponSettingUpdateRequest request, Owner owner, Long cafeId) { + return RestAssured.given() + .log().all() + .contentType(JSON) + .auth().preemptive() + .basic(owner.getLoginId(), owner.getEncryptedPassword()) + .body(request) + + .when() + .post("/api/admin/coupon-setting?cafe-id=" + cafeId) + + .then() + .log().all() + .extract(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerCafeCreateStep.java b/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerCafeCreateStep.java new file mode 100644 index 000000000..fecb476a0 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerCafeCreateStep.java @@ -0,0 +1,35 @@ +package com.stampcrush.backend.acceptance.step; + +import com.stampcrush.backend.api.manager.cafe.request.CafeCreateRequest; +import com.stampcrush.backend.entity.user.Owner; +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; + +import static io.restassured.http.ContentType.JSON; + +public class ManagerCafeCreateStep { + + public static final CafeCreateRequest CAFE_CREATE_REQUEST = new CafeCreateRequest("깃짱카페", "서초구", "우리집", "01010101010"); + + public static Long 카페_생성_요청하고_아이디_반환(Owner owner, CafeCreateRequest cafeCreateRequest) { + ExtractableResponse response = 카페_생성_요청(owner, cafeCreateRequest); + String location = response.header("Location"); + return Long.valueOf(location.split("/")[2]); + } + + public static ExtractableResponse 카페_생성_요청(Owner owner, CafeCreateRequest cafeCreateRequest) { + return RestAssured.given() + .log().all() + .contentType(JSON) + .body(cafeCreateRequest) + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + + .when() + .post("/api/admin/cafes") + + .then() + .log().all() + .extract(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerCouponCreateStep.java b/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerCouponCreateStep.java new file mode 100644 index 000000000..a675c73c1 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerCouponCreateStep.java @@ -0,0 +1,34 @@ +package com.stampcrush.backend.acceptance.step; + +import com.stampcrush.backend.api.manager.coupon.request.CouponCreateRequest; +import com.stampcrush.backend.api.manager.coupon.response.CouponCreateResponse; +import com.stampcrush.backend.entity.user.Owner; +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; + +import static io.restassured.http.ContentType.JSON; + +public class ManagerCouponCreateStep { + + public static Long 쿠폰_생성_요청하고_아이디_반환(Owner owner, CouponCreateRequest request, Long customerId) { + ExtractableResponse response = 쿠폰_생성_요청(owner, request, customerId); + CouponCreateResponse couponCreateResponse = response.body().as(CouponCreateResponse.class); + return couponCreateResponse.getCouponId(); + } + + public static ExtractableResponse 쿠폰_생성_요청(Owner owner, CouponCreateRequest request, Long customerId) { + return RestAssured.given() + .log().all() + .contentType(JSON) + .body(request) + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + + .when() + .post("/api/admin/customers/" + customerId + "/coupons") + + .then() + .log().all() + .extract(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerCouponFindStep.java b/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerCouponFindStep.java new file mode 100644 index 000000000..0a1d8eea4 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerCouponFindStep.java @@ -0,0 +1,36 @@ +package com.stampcrush.backend.acceptance.step; + +import com.stampcrush.backend.api.manager.coupon.response.CustomerAccumulatingCouponFindResponse; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; + +import java.util.List; + +import static io.restassured.RestAssured.given; + +public class ManagerCouponFindStep { + + public static List 고객의_쿠폰_조회하고_결과_반환(Owner owner, Long savedCafeId, RegisterCustomer savedCustomer) { + ExtractableResponse response = 고객의_쿠폰_조회_요청(savedCafeId, owner, savedCustomer); + return response.jsonPath() + .getList("coupons", CustomerAccumulatingCouponFindResponse.class); + } + + public static ExtractableResponse 고객의_쿠폰_조회_요청(Long savedCafeId, Owner owner, RegisterCustomer savedCustomer) { + return given() + .log().all() + .auth().preemptive() + .basic(owner.getLoginId(), owner.getEncryptedPassword()) + .queryParam("cafe-id", savedCafeId) + .queryParam("active", true) + + .when() + .get("api/admin/customers/{customerId}/coupons", savedCustomer.getId()) + + .then() + .log().all() + .extract(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerCustomerFindStep.java b/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerCustomerFindStep.java new file mode 100644 index 000000000..ac67677c2 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerCustomerFindStep.java @@ -0,0 +1,26 @@ +package com.stampcrush.backend.acceptance.step; + +import com.stampcrush.backend.entity.user.Owner; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; + +public class ManagerCustomerFindStep { + + public static ExtractableResponse 고객_목록_조회_요청(Owner owner, Long cafeId) { + return given() + .log().all() + .contentType(JSON) + .auth().preemptive() + .basic(owner.getLoginId(), owner.getEncryptedPassword()) + + .when() + .get("/api/admin/cafes/{cafeId}/customers", cafeId) + + .then() + .log().all() + .extract(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerStampCreateStep.java b/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerStampCreateStep.java new file mode 100644 index 000000000..ea6db6e3a --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerStampCreateStep.java @@ -0,0 +1,29 @@ +package com.stampcrush.backend.acceptance.step; + +import com.stampcrush.backend.api.manager.coupon.request.StampCreateRequest; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; + +public class ManagerStampCreateStep { + + public static ExtractableResponse 쿠폰에_스탬프를_적립_요청(Owner owner, RegisterCustomer customer, Long couponId, StampCreateRequest stampCreateRequest) { + return given() + .log().all() + .body(stampCreateRequest) + .contentType(JSON) + .auth().preemptive() + .basic(owner.getLoginId(), owner.getEncryptedPassword()) + + .when() + .post("/api/admin/customers/{customerId}/coupons/{couponId}/stamps", customer.getId(), couponId) + + .then() + .log().all() + .extract(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerTemporaryCustomerCreateStep.java b/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerTemporaryCustomerCreateStep.java new file mode 100644 index 000000000..785566452 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/step/ManagerTemporaryCustomerCreateStep.java @@ -0,0 +1,34 @@ +package com.stampcrush.backend.acceptance.step; + +import com.stampcrush.backend.api.manager.customer.request.TemporaryCustomerCreateRequest; +import com.stampcrush.backend.entity.user.Owner; +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; + +import static io.restassured.http.ContentType.JSON; + +public class ManagerTemporaryCustomerCreateStep { + + public static final TemporaryCustomerCreateRequest TEMPORARY_CUSTOMER_CREATE_REQUEST = new TemporaryCustomerCreateRequest("01012345678"); + + public static Long 전화번호로_임시_고객_등록_요청하고_아이디_반환(Owner owner, TemporaryCustomerCreateRequest request) { + ExtractableResponse response = 전화번호로_임시_고객_등록_요청(owner, request); + return Long.valueOf(response.header("Location").split("/")[2]); + } + + public static ExtractableResponse 전화번호로_임시_고객_등록_요청(Owner owner, TemporaryCustomerCreateRequest request) { + return RestAssured.given() + .log().all() + .contentType(JSON) + .body(request) + .auth().preemptive().basic(owner.getLoginId(), owner.getEncryptedPassword()) + + .when() + .post("/api/admin/temporary-customers") + + .then() + .log().all() + .extract(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/step/VisitorCafeFindStep.java b/backend/src/test/java/com/stampcrush/backend/acceptance/step/VisitorCafeFindStep.java new file mode 100644 index 000000000..59ac640d4 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/step/VisitorCafeFindStep.java @@ -0,0 +1,26 @@ +package com.stampcrush.backend.acceptance.step; + +import com.stampcrush.backend.entity.user.RegisterCustomer; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.springframework.http.MediaType; + +import static io.restassured.RestAssured.given; + +public final class VisitorCafeFindStep { + + public static ExtractableResponse 고객의_카페_정보_조회_요청(RegisterCustomer customer, Long cafeId) { + return given() + .log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .auth().preemptive() + .basic(customer.getLoginId(), customer.getEncryptedPassword()) + + .when() + .get("/api/cafes/" + cafeId) + + .then() + .log().all() + .extract(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/step/VisitorCouponFindStep.java b/backend/src/test/java/com/stampcrush/backend/acceptance/step/VisitorCouponFindStep.java new file mode 100644 index 000000000..e8f68ea10 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/step/VisitorCouponFindStep.java @@ -0,0 +1,22 @@ +package com.stampcrush.backend.acceptance.step; + +import com.stampcrush.backend.entity.user.RegisterCustomer; +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; + +public class VisitorCouponFindStep { + + public static ExtractableResponse 고객의_쿠폰_카페별로_1개씩_조회_요청(RegisterCustomer customer) { + return RestAssured.given() + .log().all() + .auth().preemptive().basic(customer.getLoginId(), customer.getEncryptedPassword()) + + .when() + .get("/api/coupons") + + .then() + .log().all() + .extract(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/step/VisitorJoinStep.java b/backend/src/test/java/com/stampcrush/backend/acceptance/step/VisitorJoinStep.java new file mode 100644 index 000000000..0d019e03e --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/step/VisitorJoinStep.java @@ -0,0 +1,28 @@ +package com.stampcrush.backend.acceptance.step; + +import com.stampcrush.backend.auth.request.OAuthRegisterCustomerCreateRequest; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; + +import static io.restassured.RestAssured.given; + +public class VisitorJoinStep { + + public static String 회원_가입_요청하고_액세스_토큰_반환(OAuthRegisterCustomerCreateRequest request) { + ExtractableResponse response = 회원_가입_요청(request); + return response.jsonPath().getString("accessToken"); + } + + public static ExtractableResponse 회원_가입_요청(OAuthRegisterCustomerCreateRequest request) { + return given() + .log().all() + .body(request) + + .when() + .post("/api/login/test/token") + + .then() + .log().all() + .extract(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/acceptance/step/VisitorProfilesCommandStep.java b/backend/src/test/java/com/stampcrush/backend/acceptance/step/VisitorProfilesCommandStep.java new file mode 100644 index 000000000..b06cbc413 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/acceptance/step/VisitorProfilesCommandStep.java @@ -0,0 +1,48 @@ +package com.stampcrush.backend.acceptance.step; + +import com.stampcrush.backend.api.visitor.profile.request.VisitorProfilesPhoneNumberUpdateRequest; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; + +public final class VisitorProfilesCommandStep { + + public static ExtractableResponse 고객의_전화번호_등록_요청( + RegisterCustomer customer, + VisitorProfilesPhoneNumberUpdateRequest request + ) { + return given() + .log().all() + .contentType(JSON) + .body(request) + .auth().preemptive() + .basic(customer.getLoginId(), customer.getEncryptedPassword()) + + .when() + .post("/api/profiles/phone-number") + + .then() + .extract(); + } + + public static ExtractableResponse 고객의_전화번호_등록_요청_token( + String accessToken, + VisitorProfilesPhoneNumberUpdateRequest request + ) { + return given() + .log().all() + .contentType(JSON) + .body(request) + .auth().preemptive() + .oauth2(accessToken) + + .when() + .post("/api/profiles/phone-number") + + .then() + .extract(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/ControllerSliceTest.java b/backend/src/test/java/com/stampcrush/backend/api/ControllerSliceTest.java new file mode 100644 index 000000000..7973db0e0 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/ControllerSliceTest.java @@ -0,0 +1,27 @@ +package com.stampcrush.backend.api; + +import com.stampcrush.backend.auth.application.util.AuthTokensGenerator; +import com.stampcrush.backend.common.KorNamingConverter; +import com.stampcrush.backend.repository.user.OwnerRepository; +import com.stampcrush.backend.repository.user.RegisterCustomerRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +@KorNamingConverter +@WebMvcTest +public class ControllerSliceTest { + + @Autowired + public MockMvc mockMvc; + + @MockBean + public OwnerRepository ownerRepository; + + @MockBean + public RegisterCustomerRepository registerCustomerRepository; + + @MockBean + public AuthTokensGenerator authTokensGenerator; +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/DocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/DocsControllerTest.java new file mode 100644 index 000000000..429dcece4 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/DocsControllerTest.java @@ -0,0 +1,173 @@ +package com.stampcrush.backend.api.docs; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stampcrush.backend.api.manager.cafe.ManagerCafeCommandApiController; +import com.stampcrush.backend.api.manager.cafe.ManagerCafeCouponSettingCommandApiController; +import com.stampcrush.backend.api.manager.cafe.ManagerCafeFindApiController; +import com.stampcrush.backend.api.manager.coupon.ManagerCouponCommandApiController; +import com.stampcrush.backend.api.manager.coupon.ManagerCouponFindApiController; +import com.stampcrush.backend.api.manager.customer.ManagerCustomerCommandApiController; +import com.stampcrush.backend.api.manager.customer.ManagerCustomerFindApiController; +import com.stampcrush.backend.api.manager.reward.ManagerRewardCommandApiController; +import com.stampcrush.backend.api.manager.reward.ManagerRewardFindApiController; +import com.stampcrush.backend.api.manager.sample.ManagerSampleCouponFindApiController; +import com.stampcrush.backend.api.visitor.cafe.VisitorCafeFindApiController; +import com.stampcrush.backend.api.visitor.coupon.VisitorCouponCommandApiController; +import com.stampcrush.backend.api.visitor.coupon.VisitorCouponFindApiController; +import com.stampcrush.backend.api.visitor.favorites.VisitorFavoritesCommandApiController; +import com.stampcrush.backend.api.visitor.reward.VisitorRewardsFindController; +import com.stampcrush.backend.api.visitor.visithistory.VisitorVisitHistoryFindApiController; +import com.stampcrush.backend.application.manager.cafe.ManagerCafeCommandService; +import com.stampcrush.backend.application.manager.cafe.ManagerCafeCouponSettingCommandService; +import com.stampcrush.backend.application.manager.cafe.ManagerCafeFindService; +import com.stampcrush.backend.application.manager.coupon.ManagerCouponCommandService; +import com.stampcrush.backend.application.manager.coupon.ManagerCouponFindService; +import com.stampcrush.backend.application.manager.customer.ManagerCustomerCommandService; +import com.stampcrush.backend.application.manager.customer.ManagerCustomerFindService; +import com.stampcrush.backend.application.manager.reward.ManagerRewardCommandService; +import com.stampcrush.backend.application.manager.reward.ManagerRewardFindService; +import com.stampcrush.backend.application.manager.sample.ManagerSampleCouponFindService; +import com.stampcrush.backend.application.visitor.cafe.VisitorCafeFindService; +import com.stampcrush.backend.application.visitor.coupon.VisitorCouponCommandService; +import com.stampcrush.backend.application.visitor.coupon.VisitorCouponFindService; +import com.stampcrush.backend.application.visitor.favorites.VisitorFavoritesCommandService; +import com.stampcrush.backend.application.visitor.reward.VisitorRewardsFindService; +import com.stampcrush.backend.application.visitor.visithistory.VisitorVisitHistoryFindService; +import com.stampcrush.backend.auth.application.util.AuthTokensGenerator; +import com.stampcrush.backend.common.KorNamingConverter; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.repository.user.OwnerRepository; +import com.stampcrush.backend.repository.user.RegisterCustomerRepository; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; + +import java.util.Base64; + +import static com.stampcrush.backend.fixture.CustomerFixture.REGISTER_CUSTOMER_GITCHAN; +import static com.stampcrush.backend.fixture.OwnerFixture.OWNER3; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; + +@KorNamingConverter +@AutoConfigureRestDocs +@AutoConfigureMockMvc +@WebMvcTest({ManagerCafeFindApiController.class, + VisitorCafeFindApiController.class, + ManagerCustomerFindApiController.class, + ManagerCustomerCommandApiController.class, + VisitorCouponFindApiController.class, + VisitorFavoritesCommandApiController.class, + ManagerCafeCommandApiController.class, + ManagerCafeCouponSettingCommandApiController.class, + ManagerSampleCouponFindApiController.class, + ManagerCouponCommandApiController.class, + ManagerCouponFindApiController.class, + ManagerRewardCommandApiController.class, + ManagerRewardFindApiController.class, + VisitorCouponCommandApiController.class, + VisitorRewardsFindController.class, + VisitorVisitHistoryFindApiController.class +}) +@ExtendWith({RestDocumentationExtension.class}) +public abstract class DocsControllerTest { + + protected static final Long CAFE_ID = 1L; + protected static final Owner OWNER = OWNER3; + protected static final RegisterCustomer CUSTOMER = REGISTER_CUSTOMER_GITCHAN; + + protected static String OWNER_BASIC_HEADER; + protected static String CUSTOMER_BASIC_HEADER; + + protected MockMvc mockMvc; + + @Autowired + protected WebApplicationContext ctx; + + @Autowired + protected ObjectMapper objectMapper; + + @MockBean + protected ManagerCafeFindService managerCafeFindService; + + @MockBean + protected OwnerRepository ownerRepository; + + @MockBean + protected RegisterCustomerRepository customerRepository; + + @MockBean + protected VisitorCafeFindService visitorCafeFindService; + + @MockBean + protected ManagerCustomerFindService managerCustomerFindService; + + @MockBean + protected ManagerCustomerCommandService managerCustomerCommandService; + + @MockBean + protected VisitorCouponFindService visitorCouponFindService; + + @MockBean + protected VisitorFavoritesCommandService visitorFavoritesCommandService; + + @MockBean + protected ManagerCafeCommandService managerCafeCommandService; + + @MockBean + protected ManagerCafeCouponSettingCommandService managerCafeCouponSettingCommandService; + + @MockBean + protected ManagerSampleCouponFindService managerSampleCouponFindService; + + @MockBean + protected ManagerCouponCommandService managerCouponCommandService; + + @MockBean + protected ManagerCouponFindService managerCouponFindService; + + @MockBean + protected ManagerRewardCommandService managerRewardCommandService; + + @MockBean + protected ManagerRewardFindService managerRewardFindService; + + @MockBean + protected VisitorCouponCommandService visitorCouponCommandService; + + @MockBean + protected VisitorRewardsFindService visitorRewardsFindService; + + @MockBean + protected VisitorVisitHistoryFindService visitorVisitHistoryFindService; + + @MockBean + public AuthTokensGenerator authTokensGenerator; + + @BeforeAll + static void setUpAuth() { + OWNER_BASIC_HEADER = "Basic " + Base64.getEncoder().encodeToString((OWNER.getLoginId() + ":" + OWNER.getEncryptedPassword()).getBytes()); + CUSTOMER_BASIC_HEADER = "Basic " + Base64.getEncoder().encodeToString((CUSTOMER.getLoginId() + ":" + CUSTOMER.getEncryptedPassword()).getBytes()); + } + + @BeforeEach + void setUp(final RestDocumentationContextProvider restDocumentation) { + mockMvc = MockMvcBuilders.webAppContextSetup(ctx) + .apply(documentationConfiguration(restDocumentation)) + .addFilters(new CharacterEncodingFilter("UTF-8", true)) + .alwaysDo(print()) + .build(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/manager/cafe/ManagerCafeCommandApiDocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/cafe/ManagerCafeCommandApiDocsControllerTest.java new file mode 100644 index 000000000..071c7ce1f --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/cafe/ManagerCafeCommandApiDocsControllerTest.java @@ -0,0 +1,99 @@ +package com.stampcrush.backend.api.docs.manager.cafe; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.stampcrush.backend.api.docs.DocsControllerTest; +import com.stampcrush.backend.api.manager.cafe.request.CafeCreateRequest; +import com.stampcrush.backend.api.manager.cafe.request.CafeUpdateRequest; +import com.stampcrush.backend.application.manager.cafe.dto.CafeCreateDto; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; + +import java.time.LocalTime; +import java.util.Optional; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class ManagerCafeCommandApiDocsControllerTest extends DocsControllerTest { + + @Test + void 카페_상세_정보_변경() throws Exception { + // given + when(ownerRepository.findByLoginId(OWNER.getLoginId())).thenReturn(Optional.of(OWNER)); + CafeUpdateRequest request = new CafeUpdateRequest("안녕하세요", LocalTime.NOON, LocalTime.MIDNIGHT, "01012345678", "imageUrl"); + + // when, then + mockMvc.perform(RestDocumentationRequestBuilders.patch("/api/admin/cafes/{cafeId}", CAFE_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, OWNER_BASIC_HEADER)) + .andDo(document("manager/cafe/update-cafe", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("사장 모드") + .description("카페 상세 정보 업데이트") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .requestFields( + fieldWithPath("openTime").description("오픈 시간"), + fieldWithPath("closeTime").description("마감 시간"), + fieldWithPath("telephoneNumber").description("전화번호"), + fieldWithPath("cafeImageUrl").description("카페 이미지 URL"), + fieldWithPath("introduction").description("카페 소개글") + ) + .requestSchema(Schema.schema("CafeUpdateRequest")) + .build() + ) + ) + ) + .andExpect(status().isOk()); + } + + @Test + void 카페_등록() throws Exception { + // given + when(ownerRepository.findByLoginId(OWNER.getLoginId())).thenReturn(Optional.of(OWNER)); + CafeCreateRequest cafeCreateRequest = new CafeCreateRequest("우아한카페", "서울시 잠실", "루터회관 13층", "000-111-222"); + CafeCreateDto cafeCreateDto = new CafeCreateDto( + OWNER.getId(), + cafeCreateRequest.getName(), + cafeCreateRequest.getRoadAddress(), + cafeCreateRequest.getDetailAddress(), + cafeCreateRequest.getBusinessRegistrationNumber()); + when(managerCafeCommandService.createCafe(cafeCreateDto)).thenReturn(1L); + + // when, then + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/admin/cafes") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, OWNER_BASIC_HEADER) + .content(objectMapper.writeValueAsString(cafeCreateRequest))) + .andDo(document("manager/cafe/register-cafe", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("사장 모드") + .description("카페 등록") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .requestFields(fieldWithPath("name").description("카페 이름"), + fieldWithPath("roadAddress").description("도로명 주소"), + fieldWithPath("detailAddress").description("상세 주소"), + fieldWithPath("businessRegistrationNumber").description("사업자 등록번호")) + .requestSchema(Schema.schema("CafeCreateRequest")) + .responseHeaders(headerWithName("Location").description("/cafes/{cafesId}")) + .build() + ) + ) + ) + .andExpect(status().isCreated()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/manager/cafe/ManagerCafeCouponSettingCommandApiDocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/cafe/ManagerCafeCouponSettingCommandApiDocsControllerTest.java new file mode 100644 index 000000000..fa18a6fc3 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/cafe/ManagerCafeCouponSettingCommandApiDocsControllerTest.java @@ -0,0 +1,71 @@ +package com.stampcrush.backend.api.docs.manager.cafe; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.stampcrush.backend.api.docs.DocsControllerTest; +import com.stampcrush.backend.api.manager.cafe.request.CafeCouponSettingUpdateRequest; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; + +import java.util.List; +import java.util.Optional; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class ManagerCafeCouponSettingCommandApiDocsControllerTest extends DocsControllerTest { + + @Test + void 쿠폰_디자인_및_정책_수정() throws Exception { + // given + when(ownerRepository.findByLoginId(OWNER.getLoginId())).thenReturn(Optional.of(OWNER)); + CafeCouponSettingUpdateRequest request = new CafeCouponSettingUpdateRequest( + "frontImageUrl", + "backImageUrl", + "stampImageUrl", + List.of(new CafeCouponSettingUpdateRequest.CouponStampCoordinateRequest(1, 1, 1)), + "reward", + 6 + ); + + // when, then + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/admin/coupon-setting") + .param("cafe-id", "1") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, OWNER_BASIC_HEADER)) + .andDo(document("manager/cafe/cafe-coupon-setting", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("사장 모드") + .description("쿠폰 디자인 및 정책 수정") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .queryParameters(parameterWithName("cafe-id").description("카페 ID")) + .requestFields( + fieldWithPath("frontImageUrl").description("쿠폰 앞면 이미지 URL"), + fieldWithPath("backImageUrl").description("쿠폰 뒷면 이미지 URL"), + fieldWithPath("stampImageUrl").description("스탬프 이미지 URL"), + fieldWithPath("coordinates[].order").description("좌표 순서"), + fieldWithPath("coordinates[].xCoordinate").description("X 좌표"), + fieldWithPath("coordinates[].yCoordinate").description("Y 좌표"), + fieldWithPath("reward").description("보상 이름"), + fieldWithPath("expirePeriod").description("유효기간 (유효기간 미설정시 1200)") + ) + .requestSchema(Schema.schema("CafeCouponSettingUpdateRequest")) + .build() + ) + ) + ) + .andExpect(status().isNoContent()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/manager/cafe/ManagerCafeFindApiDocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/cafe/ManagerCafeFindApiDocsControllerTest.java new file mode 100644 index 000000000..72ab4ea5f --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/cafe/ManagerCafeFindApiDocsControllerTest.java @@ -0,0 +1,63 @@ +package com.stampcrush.backend.api.docs.manager.cafe; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.stampcrush.backend.api.docs.DocsControllerTest; +import com.stampcrush.backend.application.manager.cafe.dto.CafeFindResultDto; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; + +import java.time.LocalTime; +import java.util.List; +import java.util.Optional; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class ManagerCafeFindApiDocsControllerTest extends DocsControllerTest { + + @Test + void 카페_조회_요청_사장_모드() throws Exception { + // given + when(ownerRepository.findByLoginId(OWNER.getLoginId())).thenReturn(Optional.of(OWNER)); + when(managerCafeFindService.findCafesByOwner(OWNER.getId())).thenReturn(List.of(new CafeFindResultDto(1L, "우아한카페", LocalTime.MIDNIGHT, LocalTime.NOON, "01012345678", "http://imge.co", "서울시 송파구", "루터회관", "032-1234-87", "안녕하세요"))); + + // when, then + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/admin/cafes") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, OWNER_BASIC_HEADER)) + .andDo(document("manager/cafe/find-cafe", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("사장 모드") + .description("카페 정보 조회") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .responseFields( + fieldWithPath("cafes[].id").description("카페 ID"), + fieldWithPath("cafes[].name").description("카페 이름"), + fieldWithPath("cafes[].openTime").description("카페 오픈 시간"), + fieldWithPath("cafes[].closeTime").description("카페 닫는 시간"), + fieldWithPath("cafes[].telephoneNumber").description("카페 전화번호"), + fieldWithPath("cafes[].cafeImageUrl").description("카페 이미지 URL"), + fieldWithPath("cafes[].roadAddress").description("카페 도로명주소"), + fieldWithPath("cafes[].detailAddress").description("카페 상세 주소"), + fieldWithPath("cafes[].businessRegistrationNumber").description("카페 도로명주소"), + fieldWithPath("cafes[].introduction").description("카페 소개글") + ) + .responseSchema(Schema.schema("CafesFindResponse")) + .build() + ) + ) + ) + .andExpect(status().isOk()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/manager/coupon/ManagerCouponCommandApiDocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/coupon/ManagerCouponCommandApiDocsControllerTest.java new file mode 100644 index 000000000..3ec1a337e --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/coupon/ManagerCouponCommandApiDocsControllerTest.java @@ -0,0 +1,87 @@ +package com.stampcrush.backend.api.docs.manager.coupon; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.stampcrush.backend.api.docs.DocsControllerTest; +import com.stampcrush.backend.api.manager.coupon.request.CouponCreateRequest; +import com.stampcrush.backend.api.manager.coupon.request.StampCreateRequest; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; + +import java.util.Optional; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class ManagerCouponCommandApiDocsControllerTest extends DocsControllerTest { + + @Test + void 쿠폰_신규_발급() throws Exception { + // given + Long cafeId = 1L; + Long customerId = 1L; + when(ownerRepository.findByLoginId(OWNER.getLoginId())).thenReturn(Optional.of(OWNER)); + CouponCreateRequest request = new CouponCreateRequest(cafeId); + when(managerCouponCommandService.createCoupon(cafeId, customerId)).thenReturn(1L); + + // when, then + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/admin/customers/{customerId}/coupons", customerId) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, OWNER_BASIC_HEADER) + .content(objectMapper.writeValueAsString(request))) + .andDo(document("manager/coupon/create-coupon", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("사장 모드") + .description("쿠폰 신규 발급") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .requestFields(fieldWithPath("cafeId").description("카페 ID")) + .requestSchema(Schema.schema("CouponCreateRequest")) + .responseFields(fieldWithPath("couponId").description("쿠폰 ID")) + .responseSchema(Schema.schema("CouponCreateResponse")) + .build() + ) + ) + ) + .andExpect(status().isCreated()); + } + + @Test + void 스탬프_적립() throws Exception { + // given + Long couponId = 1L; + Long customerId = 1L; + when(ownerRepository.findByLoginId(OWNER.getLoginId())).thenReturn(Optional.of(OWNER)); + StampCreateRequest request = new StampCreateRequest(3); + + // when, then + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/admin/customers/{customerId}/coupons/{couponId}/stamps", customerId, couponId) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, OWNER_BASIC_HEADER) + .content(objectMapper.writeValueAsString(request))) + .andDo(document("manager/coupon/create-stamp", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("사장 모드") + .description("스탬프 적립") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .requestFields(fieldWithPath("earningStampCount").description("적립할 스탬프 개수")) + .requestSchema(Schema.schema("StampCreateRequest")) + .build() + ) + ) + ) + .andExpect(status().isCreated()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/manager/coupon/ManagerCouponFindApiDocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/coupon/ManagerCouponFindApiDocsControllerTest.java new file mode 100644 index 000000000..8493c3f8f --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/coupon/ManagerCouponFindApiDocsControllerTest.java @@ -0,0 +1,98 @@ +package com.stampcrush.backend.api.docs.manager.coupon; + +import com.epages.restdocs.apispec.ResourceDocumentation; +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.stampcrush.backend.api.docs.DocsControllerTest; +import com.stampcrush.backend.application.manager.coupon.dto.CafeCustomerFindResultDto; +import com.stampcrush.backend.application.manager.coupon.dto.CustomerAccumulatingCouponFindResultDto; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class ManagerCouponFindApiDocsControllerTest extends DocsControllerTest { + + @Test + void 고객의_쿠폰_조회() throws Exception { + // given + Long customerId = 1L; + Long cafeId = 1L; + + when(ownerRepository.findByLoginId(OWNER.getLoginId())).thenReturn(Optional.of(OWNER)); + when(managerCouponFindService.findAccumulatingCoupon(cafeId, customerId)).thenReturn(List.of(new CustomerAccumulatingCouponFindResultDto(1L, 1L, "윤생", 3, LocalDateTime.MIN, false, 10))); + + // when, then + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/admin/customers/{customerId}/coupons", customerId) + .queryParam("cafe-id", String.valueOf(cafeId)) + .queryParam("active", "true") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, OWNER_BASIC_HEADER)) + .andDo(document("manager/coupon/find-coupon", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("사장 모드") + .description("고객의 쿠폰 조회") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .queryParameters(ResourceDocumentation.parameterWithName("cafe-id").description("카페 Id"), + ResourceDocumentation.parameterWithName("active").description("true(활성화된 쿠폰만 조회)")) + .responseFields( + fieldWithPath("coupons[].id").description("쿠폰 ID"), + fieldWithPath("coupons[].customerId").description("고객 ID"), + fieldWithPath("coupons[].nickname").description("닉네임"), + fieldWithPath("coupons[].stampCount").description("스탬프 개수"), + fieldWithPath("coupons[].expireDate").description("유효기간"), + fieldWithPath("coupons[].isPrevious").description("지난 정책의 쿠폰인지 여부"), + fieldWithPath("coupons[].maxStampCount").description("최대 스탬프 개수")) + .responseSchema(Schema.schema("CustomerAccumulatingCouponsFindResponse")) + .build()))) + .andExpect(status().isOk()); + } + + @Test + void 고객_목록_조회() throws Exception { + // given + Long cafeId = 1L; + when(ownerRepository.findByLoginId(OWNER.getLoginId())).thenReturn(Optional.of(OWNER)); + when(managerCouponFindService.findCouponsByCafe(cafeId)).thenReturn(List.of(new CafeCustomerFindResultDto(1L, "레오", 3, 12, 30, LocalDateTime.MIN, true, 10))); + + // when, then + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/admin/cafes/{cafeId}/customers", cafeId) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, OWNER_BASIC_HEADER)) + .andDo(document("manager/coupon/find-customer-list", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("사장 모드") + .description("고객 목록 조회") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .responseFields( + fieldWithPath("customers[].id").description("고객 ID"), + fieldWithPath("customers[].nickname").description("닉네임"), + fieldWithPath("customers[].stampCount").description("스탬프 개수"), + fieldWithPath("customers[].rewardCount").description("리워드 개수"), + fieldWithPath("customers[].visitCount").description("방문 횟수"), + fieldWithPath("customers[].firstVisitDate").description("첫 방문 날짜"), + fieldWithPath("customers[].isRegistered").description("임시/가입 회원 여부"), + fieldWithPath("customers[].maxStampCount").description("최대 스탬프 개수")) + .responseSchema(Schema.schema("CafeCustomersFindResponse")) + .build()))) + .andExpect(status().isOk()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/manager/customer/ManagerCustomerCommandApiDocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/customer/ManagerCustomerCommandApiDocsControllerTest.java new file mode 100644 index 000000000..10effdfa8 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/customer/ManagerCustomerCommandApiDocsControllerTest.java @@ -0,0 +1,53 @@ +package com.stampcrush.backend.api.docs.manager.customer; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.stampcrush.backend.api.docs.DocsControllerTest; +import com.stampcrush.backend.api.manager.customer.request.TemporaryCustomerCreateRequest; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; + +import java.util.Optional; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class ManagerCustomerCommandApiDocsControllerTest extends DocsControllerTest { + + @Test + void 임시_고객_생성_요청_사장_모드() throws Exception { + // given + when(ownerRepository.findByLoginId(OWNER.getLoginId())).thenReturn(Optional.of(OWNER)); + TemporaryCustomerCreateRequest request = new TemporaryCustomerCreateRequest("01011112222"); + when(managerCustomerCommandService.createTemporaryCustomer(request.getPhoneNumber())).thenReturn(1L); + + // when, then + mockMvc.perform(RestDocumentationRequestBuilders.post("/api/admin/temporary-customers") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, OWNER_BASIC_HEADER) + .content(objectMapper.writeValueAsString(request))) + .andDo(document("manager/customer/create-temporary-customer", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("사장 모드") + .description("임시 고객 생성") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .requestFields(fieldWithPath("phoneNumber").description("고객 전화번호")) + .requestSchema(Schema.schema("TemporaryCustomerCreateRequest")) + .responseHeaders(headerWithName("Location").description("/customers/{customerId}")) + .build() + ) + ) + ) + .andExpect(status().isCreated()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/manager/customer/ManagerCustomerFindApiDocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/customer/ManagerCustomerFindApiDocsControllerTest.java new file mode 100644 index 000000000..8c56bcc00 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/customer/ManagerCustomerFindApiDocsControllerTest.java @@ -0,0 +1,58 @@ +package com.stampcrush.backend.api.docs.manager.customer; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.stampcrush.backend.api.docs.DocsControllerTest; +import com.stampcrush.backend.application.manager.customer.dto.CustomerFindDto; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; + +import java.util.List; +import java.util.Optional; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class ManagerCustomerFindApiDocsControllerTest extends DocsControllerTest { + + @Test + void 고객_조회_요청_사장_모드() throws Exception { + // given + when(ownerRepository.findByLoginId(OWNER.getLoginId())).thenReturn(Optional.of(OWNER)); + when(managerCustomerFindService.findCustomer("01012345678")).thenReturn(List.of(new CustomerFindDto(1L, "윤생1234", "01012345678"))); + + // when, then + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/admin/customers") + .param("phone-number", "01012345678") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, OWNER_BASIC_HEADER)) + .andDo(document("manager/customer/find-customer", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("사장 모드") + .description("전화번호로 고객 조회") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .queryParameters(parameterWithName("phone-number").description("입력한 전화번호")) + .responseFields( + fieldWithPath("customer[].id").description("고객 ID"), + fieldWithPath("customer[].nickname").description("고객 닉네임"), + fieldWithPath("customer[].phoneNumber").description("고객 전화번호") + ) + .responseSchema(Schema.schema("CustomersFindResponse")) + .build() + ) + ) + ) + .andExpect(status().isOk()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/manager/reward/ManagerRewardCommandApiDocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/reward/ManagerRewardCommandApiDocsControllerTest.java new file mode 100644 index 000000000..1201f1dfb --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/reward/ManagerRewardCommandApiDocsControllerTest.java @@ -0,0 +1,54 @@ +package com.stampcrush.backend.api.docs.manager.reward; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.stampcrush.backend.api.docs.DocsControllerTest; +import com.stampcrush.backend.api.manager.reward.request.RewardUsedUpdateRequest; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; + +import java.util.Optional; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class ManagerRewardCommandApiDocsControllerTest extends DocsControllerTest { + + @Test + void 리워드_사용() throws Exception { + // given + Long cafeId = 1L; + Long rewardId = 1L; + when(ownerRepository.findByLoginId(OWNER.getLoginId())).thenReturn(Optional.of(OWNER)); + RewardUsedUpdateRequest request = new RewardUsedUpdateRequest(cafeId, true); + + // when, then + mockMvc.perform(RestDocumentationRequestBuilders.patch("/api/admin/customers/{customerId}/rewards/{rewardId}", cafeId, rewardId) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, OWNER_BASIC_HEADER) + .content(objectMapper.writeValueAsString(request))) + .andDo(document("manager/reward/use-reward", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("사장 모드") + .description("리워드 사용") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .requestFields(fieldWithPath("cafeId").description("카페 Id"), + fieldWithPath("used").description("사용(true)")) + .requestSchema(Schema.schema("RewardUsedUpdateRequest")) + .build() + ) + ) + ) + .andExpect(status().isOk()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/manager/reward/ManagerRewardFindApiDocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/reward/ManagerRewardFindApiDocsControllerTest.java new file mode 100644 index 000000000..51bc85e09 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/reward/ManagerRewardFindApiDocsControllerTest.java @@ -0,0 +1,63 @@ +package com.stampcrush.backend.api.docs.manager.reward; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.stampcrush.backend.api.docs.DocsControllerTest; +import com.stampcrush.backend.application.manager.reward.dto.RewardFindDto; +import com.stampcrush.backend.application.manager.reward.dto.RewardFindResultDto; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; + +import java.util.List; +import java.util.Optional; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class ManagerRewardFindApiDocsControllerTest extends DocsControllerTest { + + @Test + void 고객의_리워드_조회() throws Exception { + // given + Long customerId = 1L; + Long cafeId = 1L; + when(ownerRepository.findByLoginId(OWNER.getLoginId())).thenReturn(Optional.of(OWNER)); + when(managerRewardFindService.findRewards(any(RewardFindDto.class))).thenReturn(List.of(new RewardFindResultDto(1L, "아메리카노"), new RewardFindResultDto(2L, "조각케익"))); + + // when, then + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/admin/customers/{customerId}/rewards", customerId) + .queryParam("cafe-id", String.valueOf(cafeId)) + .queryParam("used", "false") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, OWNER_BASIC_HEADER)) + .andDo(document("manager/reward/find-reward", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("사장 모드") + .description("고객의 리워드 조회") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .queryParameters(parameterWithName("cafe-id").description("카페 Id"), + parameterWithName("used").description("false")) + .responseFields( + fieldWithPath("rewards[].id").description("리워드 ID"), + fieldWithPath("rewards[].name").description("리워드 이름") + ) + .responseSchema(Schema.schema("RewardsFindResponse")) + .build() + ) + ) + ) + .andExpect(status().isOk()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/manager/sample/ManagerSampleCouponFindApiDocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/sample/ManagerSampleCouponFindApiDocsControllerTest.java new file mode 100644 index 000000000..ec3161e4e --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/manager/sample/ManagerSampleCouponFindApiDocsControllerTest.java @@ -0,0 +1,75 @@ +package com.stampcrush.backend.api.docs.manager.sample; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.stampcrush.backend.api.docs.DocsControllerTest; +import com.stampcrush.backend.application.manager.sample.dto.SampleCouponsFindResultDto; +import com.stampcrush.backend.fixture.SampleCouponFixture; +import org.junit.jupiter.api.Test; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; + +import java.util.List; +import java.util.Optional; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static com.stampcrush.backend.fixture.SampleCouponFixture.*; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class ManagerSampleCouponFindApiDocsControllerTest extends DocsControllerTest { + + @Test + void 스탬프_개수별로_기본_샘플_조회() throws Exception { + // given + int maxStampCount = 8; + when(ownerRepository.findByLoginId(OWNER.getLoginId())).thenReturn(Optional.of(OWNER)); + when(managerSampleCouponFindService.findSampleCouponsByMaxStampCount(maxStampCount)) + .thenReturn( + SampleCouponsFindResultDto.of( + List.of(SAMPLE_FRONT_IMAGE_SAVED), + List.of(SAMPLE_BACK_IMAGE_SAVED), + SampleCouponFixture.SAMPLE_COORDINATES_SIZE_EIGHT, + List.of(SAMPLE_STAMP_IMAGE_SAVED) + ) + ); + + mockMvc.perform( + RestDocumentationRequestBuilders.get("/api/admin/coupon-samples?max-stamp-count=" + maxStampCount) + .contentType(APPLICATION_JSON) + .header(AUTHORIZATION, OWNER_BASIC_HEADER) + ) + .andDo(document("manager/samples/find-samples", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("사장 모드") + .description("스탬프 개수별로 기본 샘플 조회") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .queryParameters(parameterWithName("max-stamp-count").description("스탬프 개수(8, 10, 12)")) + .responseFields( + fieldWithPath("sampleFrontImages[].id").description("쿠폰 앞면 이미지 ID"), + fieldWithPath("sampleFrontImages[].imageUrl").description("쿠폰 앞면 이미지 URL"), + fieldWithPath("sampleBackImages[].id").description("쿠폰 뒷면 이미지 ID"), + fieldWithPath("sampleBackImages[].imageUrl").description("쿠폰 뒷면 이미지 URL"), + fieldWithPath("sampleBackImages[].stampCoordinates[].order").description("스탬프 좌표 순서"), + fieldWithPath("sampleBackImages[].stampCoordinates[].xCoordinate").description("스탬프 X 좌표"), + fieldWithPath("sampleBackImages[].stampCoordinates[].yCoordinate").description("스탬프 Y 좌표"), + fieldWithPath("sampleStampImages[].id").description("스탬프 이미지 ID"), + fieldWithPath("sampleStampImages[].imageUrl").description("스탬프 이미지 URL") + ) + .responseSchema(Schema.schema("SampleCouponFindResponse")) + .build() + ) + ) + ) + .andExpect(status().isOk()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/cafe/VisitorCafeFindApiDocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/cafe/VisitorCafeFindApiDocsControllerTest.java new file mode 100644 index 000000000..682ed81a6 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/cafe/VisitorCafeFindApiDocsControllerTest.java @@ -0,0 +1,61 @@ +package com.stampcrush.backend.api.docs.visitor.cafe; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.stampcrush.backend.api.docs.DocsControllerTest; +import com.stampcrush.backend.application.visitor.cafe.dto.CafeInfoFindByCustomerResultDto; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; + +import java.time.LocalTime; +import java.util.Optional; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class VisitorCafeFindApiDocsControllerTest extends DocsControllerTest { + + @Test + void 카페_조회_요청_고객_모드() throws Exception { + // given + when(customerRepository.findByLoginId(CUSTOMER.getLoginId())).thenReturn(Optional.of(CUSTOMER)); + when(visitorCafeFindService.findCafeById(CAFE_ID)).thenReturn(new CafeInfoFindByCustomerResultDto(CAFE_ID, "우아한카페", "안녕하세요", LocalTime.MIDNIGHT, LocalTime.NOON, "01012345678", "http://imge.co", "서울시 송파구", "루터회관")); + + // when, then + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/cafes/{cafeId}", CAFE_ID) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, CUSTOMER_BASIC_HEADER)) + .andDo(document("visitor/cafe/find-cafe-info", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("고객 모드") + .description("카페 정보 조회") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .responseFields( + fieldWithPath("cafe.id").description("카페 ID"), + fieldWithPath("cafe.name").description("카페 이름"), + fieldWithPath("cafe.introduction").description("카페 소개글"), + fieldWithPath("cafe.openTime").description("카페 오픈 시간"), + fieldWithPath("cafe.closeTime").description("카페 닫는 시간"), + fieldWithPath("cafe.telephoneNumber").description("카페 전화번호"), + fieldWithPath("cafe.cafeImageUrl").description("카페 이미지 URL"), + fieldWithPath("cafe.roadAddress").description("카페 도로명주소"), + fieldWithPath("cafe.detailAddress").description("카페 상세 주소") + ) + .responseSchema(Schema.schema("CafeInfoFindByCustomerResponse")) + .build() + ) + ) + ) + .andExpect(status().isOk()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/coupon/VisitorCouponCommandApiDocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/coupon/VisitorCouponCommandApiDocsControllerTest.java new file mode 100644 index 000000000..1f884ae36 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/coupon/VisitorCouponCommandApiDocsControllerTest.java @@ -0,0 +1,52 @@ +package com.stampcrush.backend.api.docs.visitor.coupon; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.stampcrush.backend.api.docs.DocsControllerTest; +import org.junit.jupiter.api.Test; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; + +import java.util.Optional; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class VisitorCouponCommandApiDocsControllerTest extends DocsControllerTest { + + @Test + void 쿠폰_삭제() throws Exception { + // given + when(customerRepository.findByLoginId(CUSTOMER.getLoginId())) + .thenReturn(Optional.of(CUSTOMER)); + + doNothing() + .when(visitorCouponCommandService) + .deleteCoupon(any(), any()); + + mockMvc.perform( + RestDocumentationRequestBuilders.delete("/api/coupons/{couponId}", 1L) + .contentType(APPLICATION_JSON) + .header(AUTHORIZATION, CUSTOMER_BASIC_HEADER) + ) + .andDo(document("visitor/coupon/delete-coupon", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("고객 모드") + .description("쿠폰 삭제") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .build() + ) + ) + ) + .andExpect(status().isNoContent()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/coupon/VisitorCouponFindApiDocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/coupon/VisitorCouponFindApiDocsControllerTest.java new file mode 100644 index 000000000..4062e81a4 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/coupon/VisitorCouponFindApiDocsControllerTest.java @@ -0,0 +1,80 @@ +package com.stampcrush.backend.api.docs.visitor.coupon; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.stampcrush.backend.api.docs.DocsControllerTest; +import com.stampcrush.backend.application.visitor.coupon.dto.CustomerCouponFindResultDto; +import com.stampcrush.backend.fixture.CafeFixture; +import com.stampcrush.backend.fixture.CouponFixture; +import org.junit.jupiter.api.Test; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; + +import java.util.List; +import java.util.Optional; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class VisitorCouponFindApiDocsControllerTest extends DocsControllerTest { + + @Test + void 고객의_쿠폰_조회_요청_시_인증이_되면_200_상태코드와_응답을_반환한다() throws Exception { + when(customerRepository.findByLoginId(CUSTOMER.getLoginId())) + .thenReturn(Optional.of(CUSTOMER)); + + when(visitorCouponFindService.findOneCouponForOneCafe(CUSTOMER.getId())) + .thenReturn( + List.of( + CustomerCouponFindResultDto.of( + CafeFixture.GITCHAN_CAFE, + CouponFixture.GITCHAN_CAFE_COUPON, + true, + CouponFixture.GITCHAN_CAFE_COUPON_STAMP_COORDINATE + ) + ) + ); + + mockMvc.perform( + RestDocumentationRequestBuilders.get("/api/coupons") + .contentType(APPLICATION_JSON) + .header(AUTHORIZATION, CUSTOMER_BASIC_HEADER) + ) + .andDo(document("visitor/coupon/coupon-list", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("고객 모드") + .description("쿠폰 리스트 조회") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .responseFields( + fieldWithPath("coupons[].cafeInfo.id").description("카페 ID"), + fieldWithPath("coupons[].cafeInfo.name").description("카페 이름"), + fieldWithPath("coupons[].cafeInfo.isFavorites").description("카페 즐겨찾기 여부"), + fieldWithPath("coupons[].couponInfos[].id").description("쿠폰 ID"), + fieldWithPath("coupons[].couponInfos[].status").description("쿠폰 상태 (ACCUMULATING, REWARDED, EXPIRED)"), + fieldWithPath("coupons[].couponInfos[].stampCount").description("스탬프 개수"), + fieldWithPath("coupons[].couponInfos[].maxStampCount").description("최대 스탬프 개수"), + fieldWithPath("coupons[].couponInfos[].rewardName").description("보상 이름"), + fieldWithPath("coupons[].couponInfos[].frontImageUrl").description("쿠폰 앞면 이미지 URL"), + fieldWithPath("coupons[].couponInfos[].backImageUrl").description("쿠폰 뒷면 이미지 URL"), + fieldWithPath("coupons[].couponInfos[].stampImageUrl").description("스탬프 이미지 URL"), + fieldWithPath("coupons[].couponInfos[].coordinates[].order").description("좌표 순서"), + fieldWithPath("coupons[].couponInfos[].coordinates[].xCoordinate").description("X 좌표"), + fieldWithPath("coupons[].couponInfos[].coordinates[].yCoordinate").description("Y 좌표") + ) + .responseSchema(Schema.schema("CustomerCouponsFindResponse")) + .build() + ) + ) + ) + .andExpect(status().isOk()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/favorites/VisitorFavoritesCommandApiDocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/favorites/VisitorFavoritesCommandApiDocsControllerTest.java new file mode 100644 index 000000000..c770c2fb4 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/favorites/VisitorFavoritesCommandApiDocsControllerTest.java @@ -0,0 +1,52 @@ +package com.stampcrush.backend.api.docs.visitor.favorites; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.stampcrush.backend.api.docs.DocsControllerTest; +import com.stampcrush.backend.api.visitor.favorites.request.FavoritesUpdateRequest; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; + +import java.util.Optional; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class VisitorFavoritesCommandApiDocsControllerTest extends DocsControllerTest { + + @Test + void 즐겨찾기_등록_해제_요청() throws Exception { + // given + when(customerRepository.findByLoginId(CUSTOMER.getLoginId())).thenReturn(Optional.of(CUSTOMER)); + FavoritesUpdateRequest request = new FavoritesUpdateRequest(true); + + // when, then + mockMvc.perform( + RestDocumentationRequestBuilders.post("/api/cafes/{cafeId}/favorites", CAFE_ID) + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, CUSTOMER_BASIC_HEADER)) + .andDo(document("visitor/favorites/register-favorites", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("고객 모드") + .description("카페 즐겨찾기 등록/해제") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .requestFields(fieldWithPath("isFavorites").description("등록(true)/ 해제(false)")) + .requestSchema(Schema.schema("FavoritesUpdateRequest")) + .build() + ) + ) + ) + .andExpect(status().isOk()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/reward/VisitorRewardsFindApiDocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/reward/VisitorRewardsFindApiDocsControllerTest.java new file mode 100644 index 000000000..b52356ef2 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/reward/VisitorRewardsFindApiDocsControllerTest.java @@ -0,0 +1,73 @@ +package com.stampcrush.backend.api.docs.visitor.reward; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.stampcrush.backend.api.docs.DocsControllerTest; +import com.stampcrush.backend.application.visitor.reward.dto.VisitorRewardsFindResultDto; +import com.stampcrush.backend.entity.reward.Reward; +import com.stampcrush.backend.fixture.RewardFixture; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; + +import java.util.List; +import java.util.Optional; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class VisitorRewardsFindApiDocsControllerTest extends DocsControllerTest { + + @Test + void 리워드_조회() throws Exception { + // given, when + when(customerRepository.findByLoginId(CUSTOMER.getLoginId())).thenReturn(Optional.of(CUSTOMER)); + Reward reward = RewardFixture.REWARD_USED_FALSE; + + when(visitorRewardsFindService.findRewards(CUSTOMER.getId(), false)) + .thenReturn( + List.of( + VisitorRewardsFindResultDto.from(reward) + ) + ); + + // then + mockMvc.perform( + RestDocumentationRequestBuilders.get("/api/rewards") + .param("used", "false") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, CUSTOMER_BASIC_HEADER)) + .andDo(document("visitor/rewards/find-rewards", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("고객 모드") + .description("리워드 조회") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .queryParameters(parameterWithName("used").description("false(사용 가능한 리워드), true(사용한 리워드)")) + .responseFields( + fieldWithPath("rewards[].id").description("리워드 ID"), + fieldWithPath("rewards[].rewardName").description("리워드 이름"), + fieldWithPath("rewards[].cafeName").description("카페 이름"), + fieldWithPath("rewards[].createdAt").description("리워드 생성일"), + fieldWithPath("rewards[].usedAt").description("리워드 사용일") + ) + .responseSchema(Schema.schema("VisitorRewardsFindResponse")) + .build() + ) + ) + ) + .andExpect(status().isOk()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/visithistory/VisitorVisitHistoryFindApiDocsControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/visithistory/VisitorVisitHistoryFindApiDocsControllerTest.java new file mode 100644 index 000000000..ea1ea3ac0 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/docs/visitor/visithistory/VisitorVisitHistoryFindApiDocsControllerTest.java @@ -0,0 +1,67 @@ +package com.stampcrush.backend.api.docs.visitor.visithistory; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.stampcrush.backend.api.docs.DocsControllerTest; +import com.stampcrush.backend.application.visitor.visithistory.dto.CustomerStampHistoryFindResultDto; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class VisitorVisitHistoryFindApiDocsControllerTest extends DocsControllerTest { + + @Test + void 스탬프_적립내역_조회() throws Exception { + // given + when(customerRepository.findByLoginId(CUSTOMER.getLoginId())).thenReturn(Optional.of(CUSTOMER)); + + CustomerStampHistoryFindResultDto expected1 = + new CustomerStampHistoryFindResultDto(1L, "cafe1", 3, LocalDateTime.now()); + CustomerStampHistoryFindResultDto expected2 = + new CustomerStampHistoryFindResultDto(2L, "cafe2", 5, LocalDateTime.now()); + + // when + given(visitorVisitHistoryFindService.findStampHistoriesByCustomer(CUSTOMER.getId())) + .willReturn(List.of(expected1, expected2)); + + // then + mockMvc.perform( + RestDocumentationRequestBuilders.get("/api/stamp-histories") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, CUSTOMER_BASIC_HEADER)) + .andDo(document("visitor/visithistory/find-stamphistory", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("고객 모드") + .description("스탬프 적립내역 조회") + .requestHeaders(headerWithName("Authorization").description("임시(Basic)")) + .responseFields( + fieldWithPath("stampHistories[].id").description("적립내역 ID"), + fieldWithPath("stampHistories[].cafeName").description("카페 이름"), + fieldWithPath("stampHistories[].stampCount").description("스탬프 적립개수 "), + fieldWithPath("stampHistories[].createdAt").description("스탬프 적립일") + ) + .responseSchema(Schema.schema("CustomerStampHistoriesFindResponse")) + .build() + ) + ) + ) + .andExpect(status().isOk()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeCouponSettingCommandApiControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeCouponSettingCommandApiControllerTest.java new file mode 100644 index 000000000..fa47a28f2 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeCouponSettingCommandApiControllerTest.java @@ -0,0 +1,94 @@ +package com.stampcrush.backend.api.manager.cafe; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stampcrush.backend.api.ControllerSliceTest; +import com.stampcrush.backend.api.manager.cafe.request.CafeCouponSettingUpdateRequest; +import com.stampcrush.backend.application.manager.cafe.ManagerCafeCouponSettingCommandService; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.fixture.OwnerFixture; +import com.stampcrush.backend.helper.AuthHelper.OwnerAuthorization; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.List; +import java.util.Optional; + +import static com.stampcrush.backend.helper.AuthHelper.createOwnerAuthorization; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(ManagerCafeCouponSettingCommandApiController.class) +class ManagerCafeCouponSettingCommandApiControllerTest extends ControllerSliceTest { + + private static final CafeCouponSettingUpdateRequest CAFE_COUPON_SETTING_UPDATE_REQUEST = new CafeCouponSettingUpdateRequest( + "frontImageUrl", + "backImageUrl", + "stampImageUrl", + List.of(new CafeCouponSettingUpdateRequest.CouponStampCoordinateRequest(1, 1, 1)), + "reward", + 6 + ); + + @MockBean + private ManagerCafeCouponSettingCommandService managerCafeCouponSettingCommandService; + + @Test + void 카페_쿠폰_정책_변경_시_인증_헤더_정보가_없으면_401_상태코드를_반환한다() throws Exception { + String requestBody = formatRequestBody(CAFE_COUPON_SETTING_UPDATE_REQUEST); + + mockMvc.perform( + post("/api/admin/coupon-setting?cafe-id=1") + .contentType(APPLICATION_JSON) + .content(requestBody) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + void 카페_쿠폰_정책_변경_시_인증이_안되면_401_상태코드를_반환한다() throws Exception { + OwnerAuthorization ownerAuthorization = createOwnerAuthorization(OwnerFixture.GITCHAN); + + when(ownerRepository.findByLoginId(ownerAuthorization.getOwner().getLoginId())) + .thenReturn(Optional.empty()); + + String requestBody = formatRequestBody(CAFE_COUPON_SETTING_UPDATE_REQUEST); + + mockMvc.perform( + post("/api/admin/coupon-setting?cafe-id=1") + .contentType(APPLICATION_JSON) + .content(requestBody) + .header(AUTHORIZATION, ownerAuthorization.getBasicAuthHeader()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + void 카페_쿠폰_정책_변경_시_인증이_되면_204_상태코드와_응답을_반환한다() throws Exception { + Owner owner = OwnerFixture.GITCHAN; + + OwnerAuthorization ownerAuthorization = createOwnerAuthorization(owner); + + when(ownerRepository.findByLoginId(ownerAuthorization.getOwner().getLoginId())) + .thenReturn(Optional.of(owner)); + + String requestBody = formatRequestBody(CAFE_COUPON_SETTING_UPDATE_REQUEST); + + mockMvc.perform( + post("/api/admin/coupon-setting?cafe-id=1") + .contentType(APPLICATION_JSON) + .content(requestBody) + .header(AUTHORIZATION, ownerAuthorization.getBasicAuthHeader()) + ) + .andExpect(status().isNoContent()); + } + + private String formatRequestBody(CafeCouponSettingUpdateRequest request) throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.writeValueAsString(request); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeCouponSettingFindApiControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeCouponSettingFindApiControllerTest.java new file mode 100644 index 000000000..d56babb5e --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeCouponSettingFindApiControllerTest.java @@ -0,0 +1,75 @@ +package com.stampcrush.backend.api.manager.cafe; + +import com.stampcrush.backend.api.ControllerSliceTest; +import com.stampcrush.backend.application.manager.cafe.ManagerCafeCouponSettingFindService; +import com.stampcrush.backend.application.manager.cafe.dto.CafeCouponCoordinateFindResultDto; +import com.stampcrush.backend.application.manager.cafe.dto.CafeCouponSettingFindResultDto; +import com.stampcrush.backend.config.WebMvcConfig; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; + +import java.util.List; + +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(value = ManagerCafeCouponSettingFindApiController.class, + excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfig.class)) +public class ManagerCafeCouponSettingFindApiControllerTest extends ControllerSliceTest { + + @MockBean + private ManagerCafeCouponSettingFindService cafeCouponSettingFindService; + + @Test + void 카페의_카페_쿠폰_세팅을_조회한다() throws Exception { + // given + CafeCouponSettingFindResultDto resultDto + = new CafeCouponSettingFindResultDto( + "frontImageUrl", + "backImageUrl", + "stampImageUrl", + List.of( + new CafeCouponCoordinateFindResultDto(1, 1, 1), + new CafeCouponCoordinateFindResultDto(2, 2, 2), + new CafeCouponCoordinateFindResultDto(3, 3, 3), + new CafeCouponCoordinateFindResultDto(4, 4, 4), + new CafeCouponCoordinateFindResultDto(5, 5, 5) + ) + ); + + given(cafeCouponSettingFindService.findCouponSetting(anyLong(), anyLong())) + .willReturn(resultDto); + + // when, then + mockMvc.perform(get("/api/admin/coupon-setting/{couponId}", 1L) + .param("cafe-id", "1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("frontImageUrl").value(resultDto.getFrontImageUrl())) + .andExpect(jsonPath("backImageUrl").value(resultDto.getBackImageUrl())) + .andExpect(jsonPath("stampImageUrl").value(resultDto.getStampImageUrl())) + .andExpect(jsonPath("coordinates").isArray()) + .andExpect(jsonPath("coordinates", hasSize(resultDto.getCoordinates().size()))) + .andExpect(jsonPath("coordinates[0].order").value(resultDto.getCoordinates().get(0).getOrder())) + .andExpect(jsonPath("coordinates[0].xCoordinate").value(resultDto.getCoordinates().get(0).getXCoordinate())) + .andExpect(jsonPath("coordinates[0].yCoordinate").value(resultDto.getCoordinates().get(0).getYCoordinate())) + .andExpect(jsonPath("coordinates[1].order").value(resultDto.getCoordinates().get(1).getOrder())) + .andExpect(jsonPath("coordinates[1].xCoordinate").value(resultDto.getCoordinates().get(1).getXCoordinate())) + .andExpect(jsonPath("coordinates[1].yCoordinate").value(resultDto.getCoordinates().get(1).getYCoordinate())) + .andExpect(jsonPath("coordinates[2].order").value(resultDto.getCoordinates().get(2).getOrder())) + .andExpect(jsonPath("coordinates[2].xCoordinate").value(resultDto.getCoordinates().get(2).getXCoordinate())) + .andExpect(jsonPath("coordinates[2].yCoordinate").value(resultDto.getCoordinates().get(2).getYCoordinate())) + .andExpect(jsonPath("coordinates[3].order").value(resultDto.getCoordinates().get(3).getOrder())) + .andExpect(jsonPath("coordinates[3].xCoordinate").value(resultDto.getCoordinates().get(3).getXCoordinate())) + .andExpect(jsonPath("coordinates[3].yCoordinate").value(resultDto.getCoordinates().get(3).getYCoordinate())) + .andExpect(jsonPath("coordinates[4].order").value(resultDto.getCoordinates().get(4).getOrder())) + .andExpect(jsonPath("coordinates[4].xCoordinate").value(resultDto.getCoordinates().get(4).getXCoordinate())) + .andExpect(jsonPath("coordinates[4].yCoordinate").value(resultDto.getCoordinates().get(4).getYCoordinate())); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeFindApiControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeFindApiControllerTest.java new file mode 100644 index 000000000..a6d203545 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/manager/cafe/ManagerCafeFindApiControllerTest.java @@ -0,0 +1,85 @@ +package com.stampcrush.backend.api.manager.cafe; + +import com.stampcrush.backend.api.ControllerSliceTest; +import com.stampcrush.backend.application.manager.cafe.ManagerCafeFindService; +import com.stampcrush.backend.entity.user.Owner; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import java.util.Base64; +import java.util.Optional; + +import static com.stampcrush.backend.fixture.OwnerFixture.OWNER3; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(ManagerCafeFindApiController.class) +class ManagerCafeFindApiControllerTest extends ControllerSliceTest { + + @MockBean + private ManagerCafeFindService managerCafeFindService; + + private Owner owner; + private String basicAuthHeader; + + @BeforeEach + void setUp() { + owner = OWNER3; + + String username = owner.getLoginId(); + String password = owner.getEncryptedPassword(); + basicAuthHeader = "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()); + } + + @Test + void 카페_조회_요청_시_인증_헤더_정보가_없으면_401코드_반환() throws Exception { + // ownerId 로 넣어둔 1은 없어질거라 우선 매직넘버로 넣어둠 + mockMvc.perform(get("/api/admin/cafes") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnauthorized()); + } + + @Test + void 카페_조회_요청_시_사장_인증이_안되면_401코드_반환() throws Exception { + // given + when(ownerRepository.findByLoginId(owner.getLoginId())).thenReturn(Optional.empty()); + + // when, then + mockMvc.perform(get("/api/admin/cafes") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, basicAuthHeader)) + .andExpect(status().isUnauthorized()); + } + + @Test + void 카페_조회_요청_시_사장_인증_되면_200코드_반환() throws Exception { + // given + when(ownerRepository.findByLoginId(owner.getLoginId())).thenReturn(Optional.of(owner)); + + // when, then + mockMvc.perform( + get("/api/admin/cafes") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, basicAuthHeader) + ) + .andExpect(status().isOk()); + } + + @Test + void 카페_조회_요청_시_비밀번호가_틀리면_401코드_반환() throws Exception { + // given + when(ownerRepository.findByLoginId(owner.getLoginId())) + .thenReturn(Optional.of(new Owner("jena", "jenaId", "jnpw123", "01098765432"))); + + // when, then + mockMvc.perform(get("/api/admin/cafes") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, basicAuthHeader)) + .andExpect(status().isUnauthorized()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/manager/coupon/ManagerCouponCommandApiControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/manager/coupon/ManagerCouponCommandApiControllerTest.java new file mode 100644 index 000000000..3df1c66a9 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/manager/coupon/ManagerCouponCommandApiControllerTest.java @@ -0,0 +1,100 @@ +package com.stampcrush.backend.api.manager.coupon; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stampcrush.backend.api.ControllerSliceTest; +import com.stampcrush.backend.api.manager.coupon.request.CouponCreateRequest; +import com.stampcrush.backend.api.manager.coupon.request.StampCreateRequest; +import com.stampcrush.backend.api.manager.coupon.response.CouponCreateResponse; +import com.stampcrush.backend.application.manager.coupon.ManagerCouponCommandService; +import com.stampcrush.backend.config.WebMvcConfig; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.test.web.servlet.MvcResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(value = ManagerCouponCommandApiController.class, + excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfig.class)) +public class ManagerCouponCommandApiControllerTest extends ControllerSliceTest { + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private ManagerCouponCommandService managerCouponCommandService; + + public static String API_PREFIX = "/api/admin"; + + @Test + void 쿠폰을_신규_발급한다() throws Exception { + // given + given(managerCouponCommandService.createCoupon(anyLong(), anyLong())) + .willReturn(1L); + + CouponCreateRequest couponCreateRequest = new CouponCreateRequest(1L); + String request = objectMapper.writeValueAsString(couponCreateRequest); + + // when + MvcResult mvcResult = mockMvc.perform( + post("/api/admin/customers/{customerId}/coupons", 1L) + .content(request) + .contentType(APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andReturn(); + + // then + String responseBody = mvcResult.getResponse().getContentAsString(); + CouponCreateResponse couponCreateResponse = objectMapper.readValue(responseBody, CouponCreateResponse.class); + assertThat(couponCreateResponse.getCouponId()).isEqualTo(1); + } + + @Test + void 카페_id가_음수면_예외발생() throws Exception { + // given + CouponCreateRequest couponCreateRequest = new CouponCreateRequest(-1L); + String request = objectMapper.writeValueAsString(couponCreateRequest); + + // when, then + mockMvc.perform(post("/api/admin/customers/{customerId}/coupons", 1L) + .content(request) + .contentType(APPLICATION_JSON) + ).andExpect(status().isBadRequest()); + } + + @Test + void 스탬프를_적립한다() throws Exception { + // given, when + StampCreateRequest stampCreateRequest = new StampCreateRequest(4); + String request = objectMapper.writeValueAsString(stampCreateRequest); + + // then + mockMvc.perform(post(API_PREFIX + "/customers/{customerId}/coupons/{couponId}/stamps", 1L, 1L) + .content(request) + .contentType(APPLICATION_JSON) + ) + .andExpect(status().isCreated()); + } + + @Test + void 적립하려는_스탬프가_음수면_예외발생() throws Exception { + // given, when + StampCreateRequest stampCreateRequest = new StampCreateRequest(-1); + String request = objectMapper.writeValueAsString(stampCreateRequest); + + // then + mockMvc.perform(post(API_PREFIX + "/customers/{customerId}/coupons/{couponId}/stamps", 1L, 1L) + .content(request) + .contentType(APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/manager/coupon/ManagerCouponFindApiControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/manager/coupon/ManagerCouponFindApiControllerTest.java new file mode 100644 index 000000000..3844aa236 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/manager/coupon/ManagerCouponFindApiControllerTest.java @@ -0,0 +1,51 @@ +package com.stampcrush.backend.api.manager.coupon; + +import com.stampcrush.backend.api.ControllerSliceTest; +import com.stampcrush.backend.application.manager.coupon.ManagerCouponFindService; +import com.stampcrush.backend.application.manager.coupon.dto.CafeCustomerFindResultDto; +import com.stampcrush.backend.config.WebMvcConfig; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(value = ManagerCouponFindApiController.class, + excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfig.class)) +public class ManagerCouponFindApiControllerTest extends ControllerSliceTest { + + public static String API_PREFIX = "/api/admin"; + + @MockBean + private ManagerCouponFindService managerCouponFindService; + + @Test + void 카페에_방문한_고객들의_정보를_조회한다() throws Exception { + // given, when + CafeCustomerFindResultDto customerInfo1 = new CafeCustomerFindResultDto(1L, "name1", 5, 0, 3, LocalDateTime.now(), false, 10); + CafeCustomerFindResultDto customerInfo2 = new CafeCustomerFindResultDto(2L, "name2", 6, 0, 6, LocalDateTime.now(), true, 10); + given(managerCouponFindService.findCouponsByCafe(anyLong())) + .willReturn(List.of(customerInfo1, customerInfo2)); + + // then + mockMvc.perform(get(API_PREFIX + "/cafes/{cafeId}/customers", 1L)) + .andExpect(status().isOk()); + } + + @Test + void 특정_카페의_적립중인_쿠폰이_있는_고객을_조회한다() throws Exception { + // when, then + mockMvc.perform(get(API_PREFIX + "/customers/{customerId}/coupons", 1L) + .param("cafe-id", String.valueOf(1L)) + .param("active", "true")) + .andExpect(status().isOk()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/manager/reward/ManagerRewardFindApiControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/manager/reward/ManagerRewardFindApiControllerTest.java new file mode 100644 index 000000000..778b7af7b --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/manager/reward/ManagerRewardFindApiControllerTest.java @@ -0,0 +1,55 @@ +package com.stampcrush.backend.api.manager.reward; + +import com.stampcrush.backend.api.ControllerSliceTest; +import com.stampcrush.backend.application.manager.reward.ManagerRewardFindService; +import com.stampcrush.backend.application.manager.reward.dto.RewardFindDto; +import com.stampcrush.backend.application.manager.reward.dto.RewardFindResultDto; +import com.stampcrush.backend.config.WebMvcConfig; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.http.MediaType; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(value = ManagerRewardFindApiController.class, + excludeFilters = + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfig.class)) +public class ManagerRewardFindApiControllerTest extends ControllerSliceTest { + + @MockBean + private ManagerRewardFindService managerRewardFindService; + + @Test + void 리워드_목록을_조회한다() throws Exception { + // given + String ids = "$.rewards[?(@.id == '%s')]"; + String names = "$.rewards[?(@.name == '%s')]"; + List expectedServiceReturn = List.of( + new RewardFindResultDto(1L, "Americano"), + new RewardFindResultDto(3L, "HotChocoLatte") + ); + when(managerRewardFindService.findRewards(any(RewardFindDto.class))) + .thenReturn(expectedServiceReturn); + + // when, then + mockMvc.perform( + get("/api/admin/customers/1/rewards") + .param("cafe-id", "1") + .param("used", "false") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath(ids, "1").exists()) + .andExpect(jsonPath(ids, "3").exists()) + .andExpect(jsonPath(names, "Americano").exists()) + .andExpect(jsonPath(names, "HotChocoLatte").exists()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/manager/reward/MangerRewardCommandApiControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/manager/reward/MangerRewardCommandApiControllerTest.java new file mode 100644 index 000000000..f20a89bc7 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/manager/reward/MangerRewardCommandApiControllerTest.java @@ -0,0 +1,74 @@ +package com.stampcrush.backend.api.manager.reward; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stampcrush.backend.api.ControllerSliceTest; +import com.stampcrush.backend.api.manager.reward.request.RewardUsedUpdateRequest; +import com.stampcrush.backend.application.manager.reward.ManagerRewardCommandService; +import com.stampcrush.backend.application.manager.reward.dto.RewardUsedUpdateDto; +import com.stampcrush.backend.config.WebMvcConfig; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.http.MediaType; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(value = ManagerRewardCommandApiController.class, + excludeFilters = + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfig.class)) +public class MangerRewardCommandApiControllerTest extends ControllerSliceTest { + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private ManagerRewardCommandService managerRewardCommandService; + + @Test + void 리워드를_사용한다() throws Exception { + // given + RewardUsedUpdateRequest request = new RewardUsedUpdateRequest(1L, true); + doNothing() + .when(managerRewardCommandService) + .useReward(any(RewardUsedUpdateDto.class)); + + // when, then + mockMvc.perform( + patch("/api/admin/customers/1/rewards/1") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + void 리워드_사용시_used_가_false_이면_상태코드가_BAD_REQUEST_이다() throws Exception { + // given + RewardUsedUpdateRequest request = new RewardUsedUpdateRequest(1L, false); + + // when, then + mockMvc.perform( + patch("/api/admin/customers/1/rewards/1") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + void 리워드_사용시_cafeId_가_양수가_아니면_상태코드가_BAD_REQUEST_이다() throws Exception { + // given + RewardUsedUpdateRequest request = new RewardUsedUpdateRequest(-1L, true); + + // when, then + mockMvc.perform( + patch("/api/admin/customers/1/rewards/1") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/manager/sample/ManagerSampleCouponFindApiControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/manager/sample/ManagerSampleCouponFindApiControllerTest.java new file mode 100644 index 000000000..dac0eab59 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/manager/sample/ManagerSampleCouponFindApiControllerTest.java @@ -0,0 +1,87 @@ +package com.stampcrush.backend.api.manager.sample; + +import com.stampcrush.backend.api.ControllerSliceTest; +import com.stampcrush.backend.application.manager.sample.ManagerSampleCouponFindService; +import com.stampcrush.backend.application.manager.sample.dto.SampleCouponsFindResultDto; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.fixture.OwnerFixture; +import com.stampcrush.backend.fixture.SampleCouponFixture; +import com.stampcrush.backend.helper.AuthHelper; +import com.stampcrush.backend.helper.AuthHelper.OwnerAuthorization; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.List; +import java.util.Optional; + +import static com.stampcrush.backend.fixture.SampleCouponFixture.*; +import static com.stampcrush.backend.helper.AuthHelper.createOwnerAuthorization; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(ManagerSampleCouponFindApiController.class) +class ManagerSampleCouponFindApiControllerTest extends ControllerSliceTest { + + @MockBean + private ManagerSampleCouponFindService managerSampleCouponFindService; + + @Test + void 샘플_쿠폰_조회_요청_시_인증_헤더_정보가_없으면_401_상태코드를_반환한다() throws Exception { + mockMvc.perform( + get("/api/admin/coupon-samples") + .contentType(APPLICATION_JSON) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + void 샘플_쿠폰_조회_요청_시_인증이_안되면_401_상태코드를_반환한다() throws Exception { + OwnerAuthorization ownerAuthorization = AuthHelper.createOwnerAuthorization(OwnerFixture.GITCHAN); + + when(ownerRepository.findByLoginId(ownerAuthorization.getOwner().getLoginId())) + .thenReturn(Optional.empty()); + + mockMvc.perform( + get("/api/admin/coupon-samples") + .contentType(APPLICATION_JSON) + .header(AUTHORIZATION, ownerAuthorization.getBasicAuthHeader()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + void 샘플_쿠폰_조회_요청_시_인증이_되면_200_상태코드와_응답을_반환한다() throws Exception { + Owner owner = OwnerFixture.GITCHAN; + + OwnerAuthorization ownerAuthorization = createOwnerAuthorization(owner); + int maxStampCount = 8; + + when(ownerRepository.findByLoginId(ownerAuthorization.getOwner().getLoginId())) + .thenReturn(Optional.of(owner)); + + when(managerSampleCouponFindService.findSampleCouponsByMaxStampCount(maxStampCount)) + .thenReturn( + SampleCouponsFindResultDto.of( + List.of(SAMPLE_FRONT_IMAGE_SAVED), + List.of(SAMPLE_BACK_IMAGE_SAVED), + SampleCouponFixture.SAMPLE_COORDINATES_SIZE_EIGHT, + List.of(SAMPLE_STAMP_IMAGE_SAVED) + ) + ); + + mockMvc.perform( + get("/api/admin/coupon-samples?max-stamp-count=" + maxStampCount) + .contentType(APPLICATION_JSON) + .header(AUTHORIZATION, ownerAuthorization.getBasicAuthHeader()) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("sampleFrontImages").isNotEmpty()) + .andExpect(jsonPath("sampleBackImages").isNotEmpty()) + .andExpect(jsonPath("sampleStampImages").isNotEmpty()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/visitor/cafe/VisitorCafeFindApiControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/visitor/cafe/VisitorCafeFindApiControllerTest.java new file mode 100644 index 000000000..16f875a16 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/visitor/cafe/VisitorCafeFindApiControllerTest.java @@ -0,0 +1,86 @@ +package com.stampcrush.backend.api.visitor.cafe; + +import com.stampcrush.backend.api.ControllerSliceTest; +import com.stampcrush.backend.application.visitor.cafe.VisitorCafeFindService; +import com.stampcrush.backend.application.visitor.cafe.dto.CafeInfoFindByCustomerResultDto; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import java.time.LocalTime; +import java.util.Base64; +import java.util.Optional; + +import static com.stampcrush.backend.fixture.CustomerFixture.REGISTER_CUSTOMER_GITCHAN; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(VisitorCafeFindApiController.class) +class VisitorCafeFindApiControllerTest extends ControllerSliceTest { + + private static final Long CAFE_ID = 1L; + + @MockBean + private VisitorCafeFindService visitorCafeFindService; + + private RegisterCustomer customer; + private String basicAuthHeader; + + @BeforeEach + void setUp() { + customer = REGISTER_CUSTOMER_GITCHAN; + + String username = customer.getLoginId(); + String password = customer.getEncryptedPassword(); + basicAuthHeader = "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()); + } + + @Test + void 카페_조회_요청_시_인증_헤더_정보가_없으면_401코드_반환() throws Exception { + mockMvc.perform(get("/api/cafes/" + CAFE_ID) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnauthorized()); + } + + @Test + void 카페_조회_요청_시_고객_인증이_안되면_401코드_반환() throws Exception { + // given + when(registerCustomerRepository.findByLoginId(customer.getLoginId())).thenReturn(Optional.empty()); + + // when, then + mockMvc.perform(get("/api/cafes/" + CAFE_ID) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, basicAuthHeader)) + .andExpect(status().isUnauthorized()); + } + + @Test + void 카페_조회_요청_시_고객_인증되면_200코드_반환() throws Exception { + // given + when(registerCustomerRepository.findByLoginId(customer.getLoginId())).thenReturn(Optional.of(customer)); + when(visitorCafeFindService.findCafeById(CAFE_ID)).thenReturn(new CafeInfoFindByCustomerResultDto(CAFE_ID, "cafe", "안녕하세요", LocalTime.MIDNIGHT, LocalTime.NOON, "01012345678", "image", "address", "detail")); + + // when, then + mockMvc.perform(get("/api/cafes/" + CAFE_ID) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, basicAuthHeader)) + .andExpect(status().isOk()); + } + + @Test + void 카페_조회_요청_시_비밀번호가_틀리면_401코드_반환() throws Exception { + // given + when(registerCustomerRepository.findByLoginId(customer.getLoginId())).thenReturn(Optional.of(new RegisterCustomer("깃짱", "01012345678", "gitchan", "wrong"))); + + // when, then + mockMvc.perform(get("/api/cafes/" + 1) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, basicAuthHeader)) + .andExpect(status().isUnauthorized()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/visitor/coupon/VisitorCouponCommandApiControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/visitor/coupon/VisitorCouponCommandApiControllerTest.java new file mode 100644 index 000000000..adab67cfb --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/visitor/coupon/VisitorCouponCommandApiControllerTest.java @@ -0,0 +1,55 @@ +package com.stampcrush.backend.api.visitor.coupon; + +import com.stampcrush.backend.api.ControllerSliceTest; +import com.stampcrush.backend.application.visitor.coupon.VisitorCouponCommandService; +import com.stampcrush.backend.config.WebMvcConfig; +import com.stampcrush.backend.exception.CouponNotFoundException; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(value = VisitorCouponCommandApiController.class, + excludeFilters = + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfig.class)) +public class VisitorCouponCommandApiControllerTest extends ControllerSliceTest { + + @MockBean + private VisitorCouponCommandService visitorCouponCommandService; + + @Test + void 삭제_하려는_쿠폰_정보가_올바르지_않은_경우_상태코드가_404_이다() throws Exception { + // given + doThrow(CouponNotFoundException.class) + .when(visitorCouponCommandService) + .deleteCoupon(any(), any()); + + // when, then + mockMvc.perform( + delete("/api/coupons/{couponId}", 1L) + .contentType(APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + void 쿠폰을_정상적으로_삭제하면_상태코드가_204_이다() throws Exception { + // given + doNothing() + .when(visitorCouponCommandService) + .deleteCoupon(any(), any()); + + // when, then + mockMvc.perform( + delete("/api/coupons/{couponId}", 1L) + .contentType(APPLICATION_JSON)) + .andExpect(status().isNoContent()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/visitor/coupon/VisitorCouponFindApiControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/visitor/coupon/VisitorCouponFindApiControllerTest.java new file mode 100644 index 000000000..e87987240 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/visitor/coupon/VisitorCouponFindApiControllerTest.java @@ -0,0 +1,86 @@ +package com.stampcrush.backend.api.visitor.coupon; + +import com.stampcrush.backend.api.ControllerSliceTest; +import com.stampcrush.backend.application.visitor.coupon.VisitorCouponFindService; +import com.stampcrush.backend.application.visitor.coupon.dto.CustomerCouponFindResultDto; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.fixture.CafeFixture; +import com.stampcrush.backend.fixture.CouponFixture; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.List; +import java.util.Optional; + +import static com.stampcrush.backend.fixture.CustomerFixture.REGISTER_CUSTOMER_GITCHAN; +import static com.stampcrush.backend.fixture.CustomerFixture.REGISTER_CUSTOMER_GITCHAN_SAVED; +import static com.stampcrush.backend.helper.AuthHelper.CustomerAuthorization; +import static com.stampcrush.backend.helper.AuthHelper.createCustomerAuthorization; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(VisitorCouponFindApiController.class) +class VisitorCouponFindApiControllerTest extends ControllerSliceTest { + + @MockBean + private VisitorCouponFindService visitorCouponFindService; + + @Test + void 고객의_쿠폰_조회_요청_시_인증_헤더가_없으면_401_상태코드를_반환한다() throws Exception { + mockMvc.perform( + get("/api/coupons") + .contentType(APPLICATION_JSON) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + void 고객의_쿠폰_조회_요청_시_인증이_안되면_401_상태코드를_반환한다() throws Exception { + CustomerAuthorization customerAuthorization = createCustomerAuthorization(REGISTER_CUSTOMER_GITCHAN); + + when(registerCustomerRepository.findByLoginId(customerAuthorization.loginId())) + .thenReturn(Optional.empty()); + + mockMvc.perform( + get("/api/coupons") + .contentType(APPLICATION_JSON) + .header(AUTHORIZATION, customerAuthorization.basicAuthHeader()) + ) + .andExpect(status().isUnauthorized()); + } + + @Test + void 고객의_쿠폰_조회_요청_시_인증이_되면_200_상태코드와_응답을_반환한다() throws Exception { + RegisterCustomer customer = REGISTER_CUSTOMER_GITCHAN_SAVED; + + CustomerAuthorization customerAuthorization = createCustomerAuthorization(customer); + + when(registerCustomerRepository.findByLoginId(customerAuthorization.loginId())) + .thenReturn(Optional.of(customer)); + + when(visitorCouponFindService.findOneCouponForOneCafe(customer.getId())) + .thenReturn( + List.of( + CustomerCouponFindResultDto.of( + CafeFixture.GITCHAN_CAFE, + CouponFixture.GITCHAN_CAFE_COUPON, + true, + CouponFixture.GITCHAN_CAFE_COUPON_STAMP_COORDINATE + ) + ) + ); + + mockMvc.perform( + get("/api/coupons") + .contentType(APPLICATION_JSON) + .header(AUTHORIZATION, customerAuthorization.basicAuthHeader()) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("coupons").isNotEmpty()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/visitor/favorites/VisitorFavoritesCommandApiControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/visitor/favorites/VisitorFavoritesCommandApiControllerTest.java new file mode 100644 index 000000000..77b9f7bba --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/visitor/favorites/VisitorFavoritesCommandApiControllerTest.java @@ -0,0 +1,42 @@ +package com.stampcrush.backend.api.visitor.favorites; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stampcrush.backend.api.ControllerSliceTest; +import com.stampcrush.backend.api.visitor.favorites.request.FavoritesUpdateRequest; +import com.stampcrush.backend.application.visitor.favorites.VisitorFavoritesCommandService; +import com.stampcrush.backend.config.WebMvcConfig; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.http.MediaType; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(value = VisitorFavoritesCommandApiController.class, + excludeFilters = + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfig.class)) +public class VisitorFavoritesCommandApiControllerTest extends ControllerSliceTest { + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private VisitorFavoritesCommandService visitorFavoritesCommandService; + + @Test + void 리워드를_사용할_때_isFavorites_가_null_이면_상태코드가_400_이다() throws Exception { + // given + FavoritesUpdateRequest request = new FavoritesUpdateRequest(null); + + // when, then + mockMvc.perform( + post("/api/cafes/1/favorites") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/visitor/profile/VisitorProfilesCommandApiControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/visitor/profile/VisitorProfilesCommandApiControllerTest.java new file mode 100644 index 000000000..0477d9c99 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/visitor/profile/VisitorProfilesCommandApiControllerTest.java @@ -0,0 +1,67 @@ +package com.stampcrush.backend.api.visitor.profile; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.stampcrush.backend.api.ControllerSliceTest; +import com.stampcrush.backend.api.visitor.profile.request.VisitorProfilesPhoneNumberUpdateRequest; +import com.stampcrush.backend.application.visitor.profile.VisitorProfilesCommandService; +import com.stampcrush.backend.config.WebMvcConfig; +import com.stampcrush.backend.exception.CustomerBadRequestException; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest( + value = VisitorProfilesCommandApiController.class, + excludeFilters = + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfig.class) +) +class VisitorProfilesCommandApiControllerTest extends ControllerSliceTest { + + @MockBean + private VisitorProfilesCommandService visitorProfilesCommandService; + + @Autowired + private ObjectMapper objectMapper; + + @Test + void 전화번호를_정상_저장하면_200_상태코드를_반환한다() throws Exception { + doNothing() + .when(visitorProfilesCommandService) + .registerPhoneNumber(anyLong(), anyString()); + + VisitorProfilesPhoneNumberUpdateRequest request = new VisitorProfilesPhoneNumberUpdateRequest("01012345678"); + + mockMvc.perform( + post("/api/profiles/phone-number") + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ) + .andExpect(status().isOk()); + } + + @Test + @Disabled + void 전화번호가_기존과_중복되면_BAD_REQUEST_상태코드를_반환한다() throws Exception { + doThrow(CustomerBadRequestException.class) + .when(visitorProfilesCommandService) + .registerPhoneNumber(null, null); + + mockMvc.perform( + post("/api/profiles/phone-number") + .contentType(APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/visitor/reward/VisitorRewardsFindControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/visitor/reward/VisitorRewardsFindControllerTest.java new file mode 100644 index 000000000..5be40df0d --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/visitor/reward/VisitorRewardsFindControllerTest.java @@ -0,0 +1,91 @@ +package com.stampcrush.backend.api.visitor.reward; + +import com.stampcrush.backend.api.ControllerSliceTest; +import com.stampcrush.backend.application.visitor.reward.VisitorRewardsFindService; +import com.stampcrush.backend.application.visitor.reward.dto.VisitorRewardsFindResultDto; +import com.stampcrush.backend.config.WebMvcConfig; +import com.stampcrush.backend.entity.reward.Reward; +import com.stampcrush.backend.fixture.RewardFixture; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.http.MediaType; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest( + value = VisitorRewardsFindController.class, + excludeFilters = + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + classes = WebMvcConfig.class + ) +) +class VisitorRewardsFindControllerTest extends ControllerSliceTest { + + @MockBean + private VisitorRewardsFindService visitorRewardsFindService; + + @Test + void 고객모드에서_사용_가능한_리워드를_조회한다() throws Exception { + // given, when + Reward reward = RewardFixture.REWARD_USED_FALSE; + + when(visitorRewardsFindService.findRewards(null, false)) + .thenReturn( + List.of( + VisitorRewardsFindResultDto.from(reward) + ) + ); + + // then + mockMvc.perform( + get("/api/rewards") + .param("used", "false") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("rewards").isNotEmpty()) + .andExpect(jsonPath("rewards[0].id").value(reward.getId())) + .andExpect(jsonPath("rewards[0].rewardName").value(reward.getName())) + .andExpect(jsonPath("rewards[0].cafeName").value(reward.getCafe().getName())) + .andExpect(jsonPath("rewards[0].createdAt").value(LocalDate.from(reward.getCreatedAt()).format(DateTimeFormatter.ofPattern("yyyy:MM:dd")))) + .andExpect(jsonPath("rewards[0].usedAt").value(LocalDate.from(reward.getUpdatedAt()).format(DateTimeFormatter.ofPattern("yyyy:MM:dd")))); + } + + @Test + void 고객모드에서_사용_완료한_리워드를_조회한다() throws Exception { + // given, when + Reward reward = RewardFixture.REWARD_USED_TRUE; + + when(visitorRewardsFindService.findRewards(null, true)) + .thenReturn( + List.of( + VisitorRewardsFindResultDto.from(reward) + ) + ); + + // then + mockMvc.perform( + get("/api/rewards") + .param("used", "true") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("rewards").isNotEmpty()) + .andExpect(jsonPath("rewards[0].id").value(reward.getId())) + .andExpect(jsonPath("rewards[0].rewardName").value(reward.getName())) + .andExpect(jsonPath("rewards[0].cafeName").value(reward.getCafe().getName())) + .andExpect(jsonPath("rewards[0].createdAt").value(LocalDate.from(reward.getCreatedAt()).format(DateTimeFormatter.ofPattern("yyyy:MM:dd")))) + .andExpect(jsonPath("rewards[0].usedAt").value(LocalDate.from(reward.getUpdatedAt()).format(DateTimeFormatter.ofPattern("yyyy:MM:dd")))); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/visitor/reward/response/VisitorRewardsFindResponseTest.java b/backend/src/test/java/com/stampcrush/backend/api/visitor/reward/response/VisitorRewardsFindResponseTest.java new file mode 100644 index 000000000..5098ca326 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/visitor/reward/response/VisitorRewardsFindResponseTest.java @@ -0,0 +1,44 @@ +package com.stampcrush.backend.api.visitor.reward.response; + +import com.stampcrush.backend.application.visitor.reward.dto.VisitorRewardsFindResultDto; +import com.stampcrush.backend.common.KorNamingConverter; +import com.stampcrush.backend.entity.reward.Reward; +import com.stampcrush.backend.fixture.CafeFixture; +import com.stampcrush.backend.fixture.CustomerFixture; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@KorNamingConverter +class VisitorRewardsFindResponseTest { + + @Test + void 사용_가능한_리워드는_usedAt과_createdAt이_같은_값이다() { + LocalDateTime now = LocalDateTime.now(); + + Reward reward = new Reward( + now, + now, + 1L, + "아메리카노", + false, + CustomerFixture.REGISTER_CUSTOMER_GITCHAN_SAVED, + CafeFixture.GITCHAN_CAFE + ); + + VisitorRewardsFindResultDto dto = VisitorRewardsFindResultDto.from(reward); + + VisitorRewardsFindResponse response = VisitorRewardsFindResponse.from(List.of(dto)); + VisitorRewardsFindResponse.VisitorRewardFindResponse rewardResponse = response.getRewards().get(0); + + assertAll( + () -> assertDoesNotThrow(() -> VisitorRewardsFindResponse.from(List.of(dto))), + () -> assertThat(rewardResponse.getUsedAt()).isEqualTo(rewardResponse.getCreatedAt()) + ); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/api/visitor/visithistory/VisitorVisitHistoryFindApiControllerTest.java b/backend/src/test/java/com/stampcrush/backend/api/visitor/visithistory/VisitorVisitHistoryFindApiControllerTest.java new file mode 100644 index 000000000..6d2013d26 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/api/visitor/visithistory/VisitorVisitHistoryFindApiControllerTest.java @@ -0,0 +1,41 @@ +package com.stampcrush.backend.api.visitor.visithistory; + +import com.stampcrush.backend.api.ControllerSliceTest; +import com.stampcrush.backend.application.visitor.visithistory.VisitorVisitHistoryFindService; +import com.stampcrush.backend.application.visitor.visithistory.dto.CustomerStampHistoryFindResultDto; +import com.stampcrush.backend.config.WebMvcConfig; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; + +@WebMvcTest(value = VisitorVisitHistoryFindApiController.class, + excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = WebMvcConfig.class)) +class VisitorVisitHistoryFindApiControllerTest extends ControllerSliceTest { + + @MockBean + private VisitorVisitHistoryFindService visitorVisitHistoryFindService; + + @Test + void 고객의_스탬프_적립_내역을_조회한다() throws Exception { + // given + CustomerStampHistoryFindResultDto expected1 = new CustomerStampHistoryFindResultDto(1L, "cafe1", 3, null); + CustomerStampHistoryFindResultDto expected2 = new CustomerStampHistoryFindResultDto(2L, "cafe2", 5, null); + + // when + given(visitorVisitHistoryFindService.findStampHistoriesByCustomer(anyLong())) + .willReturn(List.of(expected1, expected2)); + + // then + mockMvc.perform(MockMvcRequestBuilders.get("/api/stamp-histories")) + .andExpect(MockMvcResultMatchers.status().isOk()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/ServiceSliceTest.java b/backend/src/test/java/com/stampcrush/backend/application/ServiceSliceTest.java new file mode 100644 index 000000000..e2d546b6d --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/ServiceSliceTest.java @@ -0,0 +1,17 @@ +package com.stampcrush.backend.application; + +import com.stampcrush.backend.common.KorNamingConverter; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@KorNamingConverter +@ExtendWith(MockitoExtension.class) +public @interface ServiceSliceTest { +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCommandServiceTest.java b/backend/src/test/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCommandServiceTest.java new file mode 100644 index 000000000..c226c7b88 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCommandServiceTest.java @@ -0,0 +1,264 @@ +package com.stampcrush.backend.application.manager.cafe; + +import com.stampcrush.backend.application.ServiceSliceTest; +import com.stampcrush.backend.application.manager.cafe.dto.CafeCreateDto; +import com.stampcrush.backend.application.manager.cafe.dto.CafeUpdateDto; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.cafe.CafeCouponDesign; +import com.stampcrush.backend.entity.cafe.CafePolicy; +import com.stampcrush.backend.entity.cafe.CafeStampCoordinate; +import com.stampcrush.backend.entity.sample.SampleBackImage; +import com.stampcrush.backend.entity.sample.SampleFrontImage; +import com.stampcrush.backend.entity.sample.SampleStampCoordinate; +import com.stampcrush.backend.entity.sample.SampleStampImage; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.repository.cafe.CafeCouponDesignRepository; +import com.stampcrush.backend.repository.cafe.CafePolicyRepository; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.cafe.CafeStampCoordinateRepository; +import com.stampcrush.backend.repository.sample.SampleBackImageRepository; +import com.stampcrush.backend.repository.sample.SampleFrontImageRepository; +import com.stampcrush.backend.repository.sample.SampleStampCoordinateRepository; +import com.stampcrush.backend.repository.sample.SampleStampImageRepository; +import com.stampcrush.backend.repository.user.OwnerRepository; +import jakarta.persistence.EntityManager; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +// TODO: Mock 사용한 slice test로 변경해주세요! +@Transactional +@SpringBootTest +@ServiceSliceTest +public class ManagerCafeCommandServiceTest { + + @Autowired + private EntityManager em; + + @Autowired + private ManagerCafeCommandService managerCafeCommandService; + + @Autowired + private CafeRepository cafeRepository; + + @Autowired + private OwnerRepository ownerRepository; + + @Autowired + private SampleFrontImageRepository sampleFrontImageRepository; + + @Autowired + private SampleBackImageRepository sampleBackImageRepository; + + @Autowired + private SampleStampImageRepository sampleStampImageRepository; + + @Autowired + private SampleStampCoordinateRepository sampleStampCoordinateRepository; + + @Autowired + private CafePolicyRepository cafePolicyRepository; + + @Autowired + private CafeCouponDesignRepository cafeCouponDesignRepository; + + @Autowired + private CafeStampCoordinateRepository cafeStampCoordinateRepository; + + private Owner owner_1; + private Owner owner_2; + private SampleFrontImage sampleFrontImage; + private SampleBackImage sampleBackImage; + private SampleStampImage sampleStampImage; + + @BeforeEach + void setUp() { + owner_1 = ownerRepository.save(new Owner("lisa", "lisa@naver.com", "1234", "01011111111")); + owner_2 = ownerRepository.save(new Owner("hardy", "ehdgur4814@naver.com", "1234", "01011111111")); + sampleFrontImage = sampleFrontImageRepository.save(new SampleFrontImage("http://www.sampleFrontImage.com")); + sampleBackImage = sampleBackImageRepository.save(new SampleBackImage("http://www.sampleBackImage.com")); + sampleStampImage = sampleStampImageRepository.save(new SampleStampImage("http://www.sampleStampImage.com")); + for (int i = 1; i <= 10; i++) { + sampleStampCoordinateRepository.save(new SampleStampCoordinate(i, i, i, sampleBackImage)); + } + em.flush(); + em.clear(); + } + + @Test + void 카페를_등록한다() { + // given + CafeCreateDto cafeCreateDto = getCafeCreateDto(); + + // when + Long cafeId = managerCafeCommandService.createCafe(cafeCreateDto); + + // then + assertThat(cafeId).isNotNull(); + } + + @Test + @Disabled + void 카페를_등록할_때_쿠폰이미지는_기본이미지로_저장된다() { + // given + CafeCreateDto cafeCreateDto = getCafeCreateDto(); + + // when + Long cafeId = managerCafeCommandService.createCafe(cafeCreateDto); + Cafe cafe = cafeRepository.findById(cafeId).get(); + Optional cafeCouponDesign = cafeCouponDesignRepository.findByCafe(cafe) + .stream() + .filter(design -> design.getCafe().getId().equals(cafeId)) + .findAny(); + + // then + SoftAssertions softAssertions = new SoftAssertions(); + softAssertions.assertThat(cafeCouponDesign).isPresent(); + softAssertions.assertThat(cafeCouponDesign.get().getFrontImageUrl()).isEqualTo(sampleFrontImage.getImageUrl()); + softAssertions.assertThat(cafeCouponDesign.get().getBackImageUrl()).isEqualTo(sampleBackImage.getImageUrl()); + softAssertions.assertThat(cafeCouponDesign.get().getStampImageUrl()).isEqualTo(sampleStampImage.getImageUrl()); + softAssertions.assertAll(); + } + + @Test + @Disabled + void 카페를_등록할_때_스탬프_좌표는_10개가_저장된다() { + // given + Integer expectedStampCoordinatesCount = 10; + CafeCreateDto cafeCreateDto = getCafeCreateDto(); + + // when + Long cafeId = managerCafeCommandService.createCafe(cafeCreateDto); + CafeCouponDesign cafeCouponDesign = cafeCouponDesignRepository.findAll() + .stream() + .filter(design -> design.getCafe().getId().equals(cafeId)) + .findAny() + .get(); + List cafeStampCoordinates = cafeStampCoordinateRepository.findAll() + .stream() + .filter(stamp -> stamp.getCafeCouponDesign().equals(cafeCouponDesign)) + .toList(); + + // then + assertThat(cafeStampCoordinates.size()).isEqualTo(expectedStampCoordinatesCount); + } + + /** + * 기본정책 + * 1. 최대 스탬프 개수: 10 + * 2. 유효기간: 6 + */ + @Test + void 카페를_등록할_때_정책은_기본정책으로_저장된다() { + // given + Integer expectedMaxStampCount = 10; + Integer expectedExpirePeriod = 6; + CafeCreateDto cafeCreateDto = getCafeCreateDto(); + + // when + Long cafeId = managerCafeCommandService.createCafe(cafeCreateDto); + Optional cafePolicy = cafePolicyRepository.findAll() + .stream() + .filter(policy -> policy.getCafe().getId().equals(cafeId)) + .findAny(); + + // then + SoftAssertions softAssertions = new SoftAssertions(); + softAssertions.assertThat(cafePolicy).isPresent(); + softAssertions.assertThat(cafePolicy.get().getMaxStampCount()).isEqualTo(expectedMaxStampCount); + softAssertions.assertThat(cafePolicy.get().getExpirePeriod()).isEqualTo(expectedExpirePeriod); + softAssertions.assertThat(cafePolicy.get().getDeleted()).isFalse(); + softAssertions.assertAll(); + } + + private CafeCreateDto getCafeCreateDto() { + return new CafeCreateDto( + owner_1.getId(), + "윤생까페", + "잠실동12길", + "14층", + "11111111"); + } + + @Test + void 카페목록을_조회한다() { + // given + Integer expectedSize = 2; + createCafe(); + createCafe(); + // when + List cafes = cafeRepository.findAllByOwnerId(owner_2.getId()); + + // then + assertThat(cafes.size()).isEqualTo(expectedSize); + } + + @Test + void 카페를_소유하지_않은_사장이_카페목록을_조회하면_빈_배열을_반환한다() { + // given + Integer expectedSize = 0; + + // when + List cafes = cafeRepository.findAllByOwnerId(owner_2.getId()); + + // then + assertThat(cafes.size()).isEqualTo(expectedSize); + } + + @Test + void 카페정보를_수정한다() { + // given + Cafe hadiCafe = createCafe(); + + // when + CafeUpdateDto cafeUpdateDto = new CafeUpdateDto( + "반갑습니다", + LocalTime.of(12, 30), + LocalTime.of(18, 30), + "99999", + "image" + ); + hadiCafe.updateCafeAdditionalInformation( + cafeUpdateDto.getIntroduction(), + cafeUpdateDto.getOpenTime(), + cafeUpdateDto.getCloseTime(), + cafeUpdateDto.getTelephoneNumber(), + cafeUpdateDto.getCafeImageUrl() + ); + + // then + assertAll( + () -> assertThat(hadiCafe.getCafeImageUrl()).isEqualTo(cafeUpdateDto.getCafeImageUrl()), + () -> assertThat(hadiCafe.getTelephoneNumber()).isEqualTo(cafeUpdateDto.getTelephoneNumber()), + () -> assertThat(hadiCafe.getOpenTime()).isEqualTo(cafeUpdateDto.getOpenTime()), + () -> assertThat(hadiCafe.getCloseTime()).isEqualTo(cafeUpdateDto.getCloseTime()), + () -> assertThat(hadiCafe.getIntroduction()).isEqualTo(cafeUpdateDto.getIntroduction()) + ); + } + + private Cafe createCafe() { + return cafeRepository.save( + new Cafe( + "하디까페", + LocalTime.of(12, 30), + LocalTime.of(18, 30), + "0211111111", + "http://www.cafeImage.com", + "안녕하세요", + "잠실동12길", + "14층", + "11111111", + owner_2)); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCouponSettingCommandServiceTest.java b/backend/src/test/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCouponSettingCommandServiceTest.java new file mode 100644 index 000000000..1dd131a45 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCouponSettingCommandServiceTest.java @@ -0,0 +1,70 @@ +package com.stampcrush.backend.application.manager.cafe; + +import com.stampcrush.backend.application.ServiceSliceTest; +import com.stampcrush.backend.application.manager.cafe.dto.CafeCouponSettingDto; +import com.stampcrush.backend.exception.CafeNotFoundException; +import com.stampcrush.backend.repository.cafe.CafeCouponDesignRepository; +import com.stampcrush.backend.repository.cafe.CafePolicyRepository; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.cafe.CafeStampCoordinateRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.List; +import java.util.Optional; + +import static com.stampcrush.backend.fixture.CafeFixture.GITCHAN_CAFE; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.when; + +@ServiceSliceTest +class ManagerCafeCouponSettingCommandServiceTest { + + private static final CafeCouponSettingDto CAFE_COUPON_SETTING_DTO = CafeCouponSettingDto.of( + "frontImageUrl", + "backImageUrl", + "stampImageUrl", + List.of(List.of(1, 1, 1), List.of(1, 2, 2)), + 2, + "reward", + 6 + ); + + @InjectMocks + private ManagerCafeCouponSettingCommandService managerCafeCouponSettingCommandService; + + @Mock + private CafeRepository cafeRepository; + + @Mock + private CafeCouponDesignRepository cafeCouponDesignRepository; + + @Mock + private CafePolicyRepository cafePolicyRepository; + + @Mock + private CafeStampCoordinateRepository cafeStampCoordinateRepository; + + @Test + void 쿠폰_세팅을_업데이트할_때_카페_정보가_존재하지_않으면_예외가_발생한다() { + long cafeId = 1L; + + when(cafeRepository.findById(cafeId)) + .thenReturn(Optional.empty()); + + assertThatThrownBy(() -> managerCafeCouponSettingCommandService.updateCafeCouponSetting(cafeId, CAFE_COUPON_SETTING_DTO)) + .isInstanceOf(CafeNotFoundException.class); + } + + @Test + void 쿠폰_세팅을_업데이트할_수_있다() { + long cafeId = 1L; + + when(cafeRepository.findById(cafeId)) + .thenReturn(Optional.of(GITCHAN_CAFE)); + + assertDoesNotThrow(() -> managerCafeCouponSettingCommandService.updateCafeCouponSetting(cafeId, CAFE_COUPON_SETTING_DTO)); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCouponSettingFindServiceTest.java b/backend/src/test/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCouponSettingFindServiceTest.java new file mode 100644 index 000000000..3ae4cbabc --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/manager/cafe/ManagerCafeCouponSettingFindServiceTest.java @@ -0,0 +1,57 @@ +package com.stampcrush.backend.application.manager.cafe; + +import com.stampcrush.backend.application.ServiceSliceTest; +import com.stampcrush.backend.exception.CafeNotFoundException; +import com.stampcrush.backend.exception.CouponNotFoundException; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.coupon.CouponRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Optional; + +import static com.stampcrush.backend.fixture.CafeFixture.GITCHAN_CAFE; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ServiceSliceTest +public class ManagerCafeCouponSettingFindServiceTest { + + @InjectMocks + private ManagerCafeCouponSettingFindService managerCafeCouponSettingFindService; + + @Mock + private CafeRepository cafeRepository; + + @Mock + private CouponRepository couponRepository; + + @Test + void 쿠폰_세팅을_조회할_때_카페_정보가_존재하지_않으면_예외가_발생한다() { + // given + Long cafeId = 1L; + + when(cafeRepository.findById(cafeId)) + .thenReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> managerCafeCouponSettingFindService.findCouponSetting(cafeId, 1L)) + .isInstanceOf(CafeNotFoundException.class); + } + + @Test + void 쿠폰_세팅을_조회할_때_쿠폰_디자인이_존재하지_않으면_예외가_발생한다() { + // given + Long cafeId = 1L; + + when(cafeRepository.findById(cafeId)) + .thenReturn(Optional.of(GITCHAN_CAFE)); + when(couponRepository.findById(any())) + .thenReturn(Optional.empty()); + // when, then + assertThatThrownBy(() -> managerCafeCouponSettingFindService.findCouponSetting(cafeId, 1L)) + .isInstanceOf(CouponNotFoundException.class); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/manager/coupon/ManageCouponCommandServiceTest.java b/backend/src/test/java/com/stampcrush/backend/application/manager/coupon/ManageCouponCommandServiceTest.java new file mode 100644 index 000000000..beaa639a2 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/manager/coupon/ManageCouponCommandServiceTest.java @@ -0,0 +1,356 @@ +package com.stampcrush.backend.application.manager.coupon; + +import com.stampcrush.backend.application.ServiceSliceTest; +import com.stampcrush.backend.application.manager.coupon.dto.StampCreateDto; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.cafe.CafeCouponDesign; +import com.stampcrush.backend.entity.cafe.CafePolicy; +import com.stampcrush.backend.entity.coupon.Coupon; +import com.stampcrush.backend.entity.coupon.CouponPolicy; +import com.stampcrush.backend.entity.coupon.CouponStatus; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.TemporaryCustomer; +import com.stampcrush.backend.exception.CafeNotFoundException; +import com.stampcrush.backend.exception.CustomerNotFoundException; +import com.stampcrush.backend.repository.cafe.CafeCouponDesignRepository; +import com.stampcrush.backend.repository.cafe.CafePolicyRepository; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.coupon.CouponDesignRepository; +import com.stampcrush.backend.repository.coupon.CouponPolicyRepository; +import com.stampcrush.backend.repository.coupon.CouponRepository; +import com.stampcrush.backend.repository.coupon.StampRepository; +import com.stampcrush.backend.repository.reward.RewardRepository; +import com.stampcrush.backend.repository.user.CustomerRepository; +import com.stampcrush.backend.repository.user.OwnerRepository; +import com.stampcrush.backend.repository.visithistory.VisitHistoryRepository; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.time.LocalDate; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +@ServiceSliceTest +public class ManageCouponCommandServiceTest { + + @InjectMocks + private ManagerCouponCommandService managerCouponCommandService; + + @Mock + private CouponRepository couponRepository; + + @Mock + private CafeRepository cafeRepository; + + @Mock + private CustomerRepository customerRepository; + + @Mock + private CafeCouponDesignRepository cafeCouponDesignRepository; + + @Mock + private CafePolicyRepository cafePolicyRepository; + + @Mock + private CouponDesignRepository couponDesignRepository; + + @Mock + private CouponPolicyRepository couponPolicyRepository; + + @Mock + private OwnerRepository ownerRepository; + + @Mock + private RewardRepository rewardRepository; + + @Mock + private VisitHistoryRepository visitHistoryRepository; + + @Mock + private StampRepository stampRepository; + + private static Cafe cafe; + private static Customer customer; + private static CouponPolicy couponPolicy; + + @BeforeAll + static void setUp() { + cafe = new Cafe(1L, "name", "road", "detailAddress", "phone", null); + customer = new TemporaryCustomer(1L, "name", "phone"); + couponPolicy = new CouponPolicy(10, "reward", 6); + } + + @Test + void 적립중인_쿠폰이_있으면_새로운_쿠폰을_발급하고_기존_쿠폰은_만료된다() { + // given + Coupon currentCoupon = new Coupon(LocalDate.EPOCH, customer, cafe, null, couponPolicy); + given(customerRepository.findById(anyLong())) + .willReturn(Optional.of(customer)); + given(cafeRepository.findById(anyLong())) + .willReturn(Optional.of(cafe)); + given(cafePolicyRepository.findByCafe(any())) + .willReturn(Optional.of(new CafePolicy(10, "reward", 6, false, cafe))); + given(cafeCouponDesignRepository.findByCafe(any())) + .willReturn(Optional.of(new CafeCouponDesign("front", "back", "stamp", false, null))); + given(couponRepository.findByCafeAndCustomerAndStatus(any(), any(), any())) + .willReturn(List.of(currentCoupon)); + + // when + managerCouponCommandService.createCoupon(1L, 1L); + + // then + then(couponRepository).should(times(1)).save(any()); + then(couponDesignRepository).should(times(1)).save(any()); + then(couponPolicyRepository).should(times(1)).save(any()); + assertThat(currentCoupon.getStatus()).isEqualTo(CouponStatus.EXPIRED); + } + + @Test + void 적립중인_쿠폰이_없으면_새로운_쿠폰만_발급한다() { + // given + given(customerRepository.findById(anyLong())) + .willReturn(Optional.of(customer)); + given(cafeRepository.findById(anyLong())) + .willReturn(Optional.of(cafe)); + given(cafePolicyRepository.findByCafe(any())) + .willReturn(Optional.of(new CafePolicy(10, "reward", 6, false, cafe))); + given(cafeCouponDesignRepository.findByCafe(any())) + .willReturn(Optional.of(new CafeCouponDesign("front", "back", "stamp", false, null))); + given(couponRepository.findByCafeAndCustomerAndStatus(any(), any(), any())) + .willReturn(Collections.emptyList()); + + // when + managerCouponCommandService.createCoupon(1L, 1L); + + // then + then(couponRepository).should(times(1)).save(any()); + then(couponDesignRepository).should(times(1)).save(any()); + then(couponPolicyRepository).should(times(1)).save(any()); + } + + @Test + void 존재하지_않는_회원이_쿠폰을_발급받으려_하면_예외발생() { + // given, when + given(customerRepository.findById(anyLong())) + .willReturn(Optional.empty()); + + // then + assertThatThrownBy(() -> managerCouponCommandService.createCoupon(1L, 1L)) + .isInstanceOf(CustomerNotFoundException.class); + } + + @Test + void 존재하지_않는_카페가_쿠폰을_발급하려고_하면_예외발생() { + // given, when + given(customerRepository.findById(anyLong())) + .willReturn(Optional.of(customer)); + given(cafeRepository.findById(anyLong())) + .willReturn(Optional.empty()); + + // then + assertThatThrownBy(() -> managerCouponCommandService.createCoupon(1L, 1L)) + .isInstanceOf(CafeNotFoundException.class); + } + + @Test + void 쿠폰_발급_시_카페의_정책이_존재하지_않으면_예외발생() { + // given, when + given(customerRepository.findById(anyLong())) + .willReturn(Optional.of(customer)); + given(cafeRepository.findById(anyLong())) + .willReturn(Optional.of(cafe)); + given(cafePolicyRepository.findByCafe(any())) + .willReturn(Optional.empty()); + + // then + assertThatThrownBy(() -> managerCouponCommandService.createCoupon(1L, 1L)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 쿠폰_발급_시_카페의_쿠폰_디자인이_없으면_예외발생() { + // given, when + given(customerRepository.findById(anyLong())) + .willReturn(Optional.of(customer)); + given(cafeRepository.findById(anyLong())) + .willReturn(Optional.of(cafe)); + given(cafePolicyRepository.findByCafe(any())) + .willReturn(Optional.of(new CafePolicy(10, "reward", 6, false, cafe))); + given(cafeCouponDesignRepository.findByCafe(any())) + .willReturn(Optional.empty()); + + // then + assertThatThrownBy(() -> managerCouponCommandService.createCoupon(1L, 1L)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 추가_적립한_스탬프로_리워드를_받기_위한_스탬프_개수보다_적으면_스탬프만_적립한다() { + // given, when + int maxStampCount = 10; + Coupon currentCoupon = new Coupon(LocalDate.EPOCH, customer, cafe, null, couponPolicy); + 스탬프_적립을_위해_필요한_엔티티를_조회한다(maxStampCount, currentCoupon); + + StampCreateDto stampCreateDto = new StampCreateDto(1L, 1L, 1L, 1); + managerCouponCommandService.createStamp(stampCreateDto); + + // then + then(rewardRepository).should(times(0)).save(any()); + then(couponRepository).should(times(0)).save(any()); + then(visitHistoryRepository).should(times(1)).save(any()); + assertThat(currentCoupon.getStatus()).isEqualTo(CouponStatus.ACCUMULATING); + } + + @Test + void 추가_적립한_스탬프로_리워드를_받기_위한_스탬프_개수와_같으면_리워드를_생성한다() { + // given, when + int maxStampCount = 10; + Coupon currentCoupon = new Coupon(LocalDate.EPOCH, customer, cafe, null, couponPolicy); + 스탬프_적립을_위해_필요한_엔티티를_조회한다(maxStampCount, currentCoupon); + + StampCreateDto stampCreateDto = new StampCreateDto(1L, 1L, 1L, maxStampCount); + managerCouponCommandService.createStamp(stampCreateDto); + + // then + then(rewardRepository).should(times(1)).save(any()); + then(couponRepository).should(times(0)).save(any()); + then(visitHistoryRepository).should(times(1)).save(any()); + assertThat(currentCoupon.getStatus()).isEqualTo(CouponStatus.REWARDED); + } + + @Test + void 추가_적립한_스탬프로_리워드를_받기_위한_스탬프_개수를_초과하면_리워드를_생성하고_새로운_쿠폰에_나머지_스탬프가_찍힌다() { + // given, when + int maxStampCount = 10; + Coupon currentCoupon = new Coupon(LocalDate.EPOCH, customer, cafe, null, couponPolicy); + 스탬프_적립을_위해_필요한_엔티티를_조회한다(maxStampCount, currentCoupon); + + StampCreateDto stampCreateDto = new StampCreateDto(1L, 1L, 1L, maxStampCount * 2 + 2); + managerCouponCommandService.createStamp(stampCreateDto); + + // then + then(rewardRepository).should(times(2)).save(any()); + then(couponRepository).should(times(2)).save(any()); + then(visitHistoryRepository).should(times(1)).save(any()); + } + + @Test + void 추가_적립한_스탬프로_리워드를_받기_위한_스탬프_개수를_초과하면_리워드를_생성하고_새로운_쿠폰에_찍을_스탬프가_없다면_쿠폰을_발급하지_않는다() { + // given, when + int maxStampCount = 10; + Coupon currentCoupon = new Coupon(LocalDate.EPOCH, customer, cafe, null, couponPolicy); + 스탬프_적립을_위해_필요한_엔티티를_조회한다(maxStampCount, currentCoupon); + + StampCreateDto stampCreateDto = new StampCreateDto(1L, 1L, 1L, maxStampCount * 2); + managerCouponCommandService.createStamp(stampCreateDto); + + // then + then(rewardRepository).should(times(2)).save(any()); + then(couponRepository).should(times(1)).save(any()); + then(visitHistoryRepository).should(times(1)).save(any()); + } + + @Test + void 기존에_스탬프가_있는_쿠폰에_추가_적립한_스탬프로_리워드를_받기_위한_스탬프_개수보다_적으면_스탬프만_적립한다() { + // given + Coupon currentCoupon = new Coupon(LocalDate.EPOCH, customer, cafe, null, couponPolicy); + currentCoupon.accumulate(4); + int maxStampCount = 10; + 스탬프_적립을_위해_필요한_엔티티를_조회한다(maxStampCount, currentCoupon); + + // when + StampCreateDto stampCreateDto = new StampCreateDto(1L, 1L, 1L, 3); + managerCouponCommandService.createStamp(stampCreateDto); + + // then + then(rewardRepository).should(times(0)).save(any()); + then(couponRepository).should(times(0)).save(any()); + then(visitHistoryRepository).should(times(1)).save(any()); + assertThat(currentCoupon.getStatus()).isEqualTo(CouponStatus.ACCUMULATING); + assertThat(currentCoupon.getStampCount()).isEqualTo(7); + } + + @Test + void 기존에_스탬프가_있는_쿠폰에_추가_적립한_스탬프로_리워드를_받기_위한_스탬프_개수와_같으면_리워드를_생성한다() { + // givem + Coupon currentCoupon = new Coupon(LocalDate.EPOCH, customer, cafe, null, couponPolicy); + currentCoupon.accumulate(4); + int maxStampCount = 10; + 스탬프_적립을_위해_필요한_엔티티를_조회한다(maxStampCount, currentCoupon); + + // when + int earningStamp = 6; + StampCreateDto stampCreateDto = new StampCreateDto(1L, 1L, 1L, earningStamp); + managerCouponCommandService.createStamp(stampCreateDto); + + // then + then(rewardRepository).should(times(1)).save(any()); + then(couponRepository).should(times(0)).save(any()); + then(visitHistoryRepository).should(times(1)).save(any()); + assertThat(currentCoupon.getStatus()).isEqualTo(CouponStatus.REWARDED); + assertThat(currentCoupon.getStampCount()).isEqualTo(10); + } + + @Test + void 기존에_스탬프가_있는_쿠폰에_추가_적립한_스탬프로_리워드를_받기_위한_스탬프_개수를_초과하면_리워드를_생성하고_새로운_쿠폰에_나머지_스탬프가_찍힌다() { + // given + Coupon currentCoupon = new Coupon(LocalDate.EPOCH, customer, cafe, null, couponPolicy); + currentCoupon.accumulate(4); + int maxStampCount = 10; + 스탬프_적립을_위해_필요한_엔티티를_조회한다(maxStampCount, currentCoupon); + + // when + int earningStamp = 17; + StampCreateDto stampCreateDto = new StampCreateDto(1L, 1L, 1L, earningStamp); + managerCouponCommandService.createStamp(stampCreateDto); + + // then + then(rewardRepository).should(times(2)).save(any()); + then(couponRepository).should(times(2)).save(any()); + then(visitHistoryRepository).should(times(1)).save(any()); + } + + @Test + void 기존에_스탬프가_있는_쿠폰에_추가_적립한_스탬프로_리워드를_받기_위한_스탬프_개수를_초과하면_리워드를_생성하고_새로운_쿠폰에_찍을_스탬프가_없다면_쿠폰을_발급하지_않는다() { + // given, when + Coupon currentCoupon = new Coupon(LocalDate.EPOCH, customer, cafe, null, couponPolicy); + currentCoupon.accumulate(4); + int maxStampCount = 10; + 스탬프_적립을_위해_필요한_엔티티를_조회한다(maxStampCount, currentCoupon); + + StampCreateDto stampCreateDto = new StampCreateDto(1L, 1L, 1L, maxStampCount * 2 - 4); + managerCouponCommandService.createStamp(stampCreateDto); + + // then + then(rewardRepository).should(times(2)).save(any()); + then(couponRepository).should(times(1)).save(any()); + then(visitHistoryRepository).should(times(1)).save(any()); + } + + private void 스탬프_적립을_위해_필요한_엔티티를_조회한다(int maxStampCount, Coupon coupon) { + given(ownerRepository.findById(any())) + .willReturn(Optional.of(new Owner("owner", "id", "pw", "phone"))); + given(customerRepository.findById(any())) + .willReturn(Optional.of(customer)); + given(cafeRepository.findAllByOwner(any())) + .willReturn(List.of(cafe)); + given(cafePolicyRepository.findByCafe(any())) + .willReturn(Optional.of(new CafePolicy(maxStampCount, "reward", 6, false, cafe))); + given(cafeCouponDesignRepository.findByCafe(any())) + .willReturn(Optional.of(new CafeCouponDesign("front", "back", "stamp", false, null))); + given(couponRepository.findById(any())) + .willReturn(Optional.of(coupon)); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/manager/coupon/ManagerCouponFindServiceTest.java b/backend/src/test/java/com/stampcrush/backend/application/manager/coupon/ManagerCouponFindServiceTest.java new file mode 100644 index 000000000..b2f6fae80 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/manager/coupon/ManagerCouponFindServiceTest.java @@ -0,0 +1,245 @@ +package com.stampcrush.backend.application.manager.coupon; + +import com.stampcrush.backend.application.ServiceSliceTest; +import com.stampcrush.backend.application.manager.coupon.dto.CafeCustomerFindResultDto; +import com.stampcrush.backend.application.manager.coupon.dto.CustomerAccumulatingCouponFindResultDto; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.cafe.CafePolicy; +import com.stampcrush.backend.entity.coupon.Coupon; +import com.stampcrush.backend.entity.coupon.CouponPolicy; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.user.TemporaryCustomer; +import com.stampcrush.backend.entity.visithistory.VisitHistory; +import com.stampcrush.backend.repository.cafe.CafePolicyRepository; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.coupon.CouponRepository; +import com.stampcrush.backend.repository.reward.RewardRepository; +import com.stampcrush.backend.repository.user.CustomerRepository; +import com.stampcrush.backend.repository.visithistory.VisitHistoryRepository; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; + +@ServiceSliceTest +public class ManagerCouponFindServiceTest { + + private static Cafe cafe; + private static Customer customer1; + private static Customer customer2; + private static CouponPolicy couponPolicy1; + private static CouponPolicy couponPolicy2; + + @InjectMocks + private ManagerCouponFindService managerCouponFindService; + + @Mock + private CouponRepository couponRepository; + + @Mock + private CafeRepository cafeRepository; + + @Mock + private CustomerRepository customerRepository; + + @Mock + private CafePolicyRepository cafePolicyRepository; + + @Mock + private VisitHistoryRepository visitHistoryRepository; + + @Mock + private RewardRepository rewardRepository; + + @BeforeAll + static void setUp() { + cafe = new Cafe(1L, "name", "road", "detailAddress", "phone", null); + customer1 = new TemporaryCustomer(1L, "customer1", "phone"); + customer2 = new TemporaryCustomer(2L, "customer2", "phone"); + couponPolicy1 = new CouponPolicy(10, "reward", 6); + couponPolicy2 = new CouponPolicy(15, "reward", 6); + } + + @Test + void 카페의_고객_중_적립중인_쿠폰이_있으면_maxStampCount는_해당_쿠폰에_맞는_maxStampCount다() { + // given + LocalDateTime coupon1CreatedAt = LocalDateTime.now(); + LocalDateTime coupon1UpdatedAt = LocalDateTime.now(); + Coupon coupon1 = new Coupon(coupon1CreatedAt, coupon1UpdatedAt, LocalDate.EPOCH, customer1, cafe, null, couponPolicy1); + VisitHistory visitHistory1 = new VisitHistory(coupon1CreatedAt, null, cafe, customer1, 3); + + LocalDateTime coupon2CreatedAt = LocalDateTime.now(); + LocalDateTime coupon2UpdatedAt = LocalDateTime.now(); + Coupon coupon2 = new Coupon(coupon2CreatedAt, coupon2UpdatedAt, LocalDate.EPOCH, customer2, cafe, null, couponPolicy2); + VisitHistory visitHistory2 = new VisitHistory(coupon2CreatedAt, null, cafe, customer2, 5); + + given(cafeRepository.findById(anyLong())) + .willReturn(Optional.of(cafe)); + given(couponRepository.findByCafe(any())) + .willReturn(List.of(coupon1, coupon2)); + given(visitHistoryRepository.findByCafeAndCustomer(cafe, customer1)) + .willReturn(List.of(visitHistory1)); + given(visitHistoryRepository.findByCafeAndCustomer(cafe, customer2)) + .willReturn(List.of(visitHistory2)); + + // when + CustomerCouponStatistics customer1Statics = new CustomerCouponStatistics(0, 0, 10); + CustomerCouponStatistics customer2Statics = new CustomerCouponStatistics(0, 0, 15); + + CafeCustomerFindResultDto customer1Result = CafeCustomerFindResultDto.of(customer1, customer1Statics, 1, coupon1CreatedAt); + CafeCustomerFindResultDto customer2Result = CafeCustomerFindResultDto.of(customer2, customer2Statics, 1, coupon2CreatedAt); + List couponsByCafe = managerCouponFindService.findCouponsByCafe(anyLong()); + + // then + assertThat(couponsByCafe).containsExactlyInAnyOrder(customer1Result, customer2Result); + } + + @Test + void 카페의_고객_중_적립중인_쿠폰이_없으면_maxStampCount는_0이다() { + // given, when + LocalDateTime coupon1CreatedAt = LocalDateTime.now(); + LocalDateTime coupon1UpdatedAt = LocalDateTime.now(); + Coupon coupon1 = new Coupon(coupon1CreatedAt, coupon1UpdatedAt, LocalDate.EPOCH, customer1, cafe, null, couponPolicy1); + coupon1.expire(); + VisitHistory visitHistory = new VisitHistory(coupon1CreatedAt, null, cafe, customer1, 3); + + given(cafeRepository.findById(anyLong())) + .willReturn(Optional.of(cafe)); + given(couponRepository.findByCafe(any())) + .willReturn(List.of(coupon1)); + given(visitHistoryRepository.findByCafeAndCustomer(cafe, customer1)) + .willReturn(List.of(visitHistory)); + + CustomerCouponStatistics customer1Statistics = new CustomerCouponStatistics(0, 0, 0); + CafeCustomerFindResultDto customer1Result = CafeCustomerFindResultDto.of(customer1, customer1Statistics, 1, coupon1CreatedAt); + List couponsByCafe = managerCouponFindService.findCouponsByCafe(anyLong()); + + // then + assertThat(couponsByCafe).containsExactlyInAnyOrder(customer1Result); + } + + @Test + void 쿠폰_정책이_쿠폰의_카페의_정책과_내용이_같지_않으면_예전_정책의_쿠폰이다() { + // given + LocalDateTime coupon1CreatedAt = LocalDateTime.now(); + LocalDateTime coupon1UpdatedAt = LocalDateTime.now(); + Coupon coupon = new Coupon(coupon1CreatedAt, coupon1UpdatedAt, LocalDate.EPOCH, customer1, cafe, null, couponPolicy1); + given(cafeRepository.findById(anyLong())) + .willReturn(Optional.of(cafe)); + given(customerRepository.findById(anyLong())) + .willReturn(Optional.of(customer1)); + given(couponRepository.findByCafeAndCustomerAndStatus(any(), any(), any())) + .willReturn(List.of(coupon)); + + cafe.getPolicies().clear(); + cafe.getPolicies().add(new CafePolicy(10, "americano", 6, false, cafe)); + + // when + List findResult = managerCouponFindService.findAccumulatingCoupon(1L, 1L); + + // then + CustomerAccumulatingCouponFindResultDto expected = new CustomerAccumulatingCouponFindResultDto(1L, + customer1.getId(), + customer1.getNickname(), + 0, + LocalDateTime.now(), + true, + 10); + assertThat(findResult).usingRecursiveFieldByFieldElementComparatorIgnoringFields("id", "expireDate") + .containsExactlyInAnyOrder(expected); + } + + @Test + void 쿠폰_정책이_쿠폰의_카페의_정책과_내용이_같으면_현재_정책의_쿠폰이다() { + // given + LocalDateTime coupon1CreatedAt = LocalDateTime.now(); + LocalDateTime coupon1UpdatedAt = LocalDateTime.now(); + Coupon coupon = new Coupon(coupon1CreatedAt, coupon1UpdatedAt, LocalDate.EPOCH, customer1, cafe, null, couponPolicy1); + + given(cafeRepository.findById(anyLong())) + .willReturn(Optional.of(cafe)); + given(customerRepository.findById(anyLong())) + .willReturn(Optional.of(customer1)); + given(couponRepository.findByCafeAndCustomerAndStatus(any(), any(), any())) + .willReturn(List.of(coupon)); + + cafe.getPolicies().clear(); + cafe.getPolicies().add(new CafePolicy(couponPolicy1.getMaxStampCount(), couponPolicy1.getRewardName(), couponPolicy1.getExpiredPeriod(), false, cafe)); + + + // when + List findResult = managerCouponFindService.findAccumulatingCoupon(1L, 1L); + + // then + CustomerAccumulatingCouponFindResultDto expected = new CustomerAccumulatingCouponFindResultDto(1L, + customer1.getId(), + customer1.getNickname(), + 0, + LocalDateTime.now(), + false, + 10); + + assertThat(findResult).usingRecursiveFieldByFieldElementComparatorIgnoringFields("id", "expireDate") + .containsExactlyInAnyOrder(expected); + } + + @Test + void 카페의_고객_목록_조회_시_방문횟수와_첫_방문일을_계산한다() { + // given + + int rewardCount = 0; + + LocalDateTime coupon1CreatedAt = LocalDateTime.now(); + LocalDateTime coupon1UpdatedAt = LocalDateTime.now(); + + int customer1EarningStampCount1 = 3; + int customer1EarningStampCount2 = 2; + + Coupon coupon1 = new Coupon(coupon1CreatedAt, coupon1UpdatedAt, LocalDate.EPOCH, customer1, cafe, null, couponPolicy1); + coupon1.accumulate(customer1EarningStampCount1); + VisitHistory customer1VisitHistory1 = new VisitHistory(coupon1CreatedAt, null, cafe, customer1, customer1EarningStampCount1); + coupon1.accumulate(customer1EarningStampCount2); + VisitHistory customer1VisitHistory2 = new VisitHistory(coupon1UpdatedAt, null, cafe, customer1, customer1EarningStampCount2); + + LocalDateTime coupon2CreatedAt = LocalDateTime.now(); + LocalDateTime coupon2UpdatedAt = LocalDateTime.now(); + + int customer2EarningStampCount = 5; + + Coupon coupon2 = new Coupon(coupon2CreatedAt, coupon2UpdatedAt, LocalDate.EPOCH, customer2, cafe, null, couponPolicy2); + coupon2.accumulate(customer2EarningStampCount); + VisitHistory customer2visitHistory1 = new VisitHistory(coupon2CreatedAt, null, cafe, customer2, customer2EarningStampCount); + + given(cafeRepository.findById(anyLong())) + .willReturn(Optional.of(cafe)); + given(couponRepository.findByCafe(any())) + .willReturn(List.of(coupon1, coupon2)); + given(visitHistoryRepository.findByCafeAndCustomer(cafe, customer1)) + .willReturn(List.of(customer1VisitHistory1, customer1VisitHistory2)); + given(visitHistoryRepository.findByCafeAndCustomer(cafe, customer2)) + .willReturn(List.of(customer2visitHistory1)); + + // when + CustomerCouponStatistics customer1Statics = new CustomerCouponStatistics(customer1EarningStampCount1 + customer1EarningStampCount2 + , rewardCount, couponPolicy1.getMaxStampCount()); + CustomerCouponStatistics customer2Statics = new CustomerCouponStatistics(customer2EarningStampCount, rewardCount, + couponPolicy2.getMaxStampCount()); + + CafeCustomerFindResultDto customer1Result = CafeCustomerFindResultDto.of(customer1, customer1Statics, 2, coupon1CreatedAt); + CafeCustomerFindResultDto customer2Result = CafeCustomerFindResultDto.of(customer2, customer2Statics, 1, coupon2CreatedAt); + List couponsByCafe = managerCouponFindService.findCouponsByCafe(anyLong()); + + // then + assertThat(couponsByCafe).containsExactlyInAnyOrder(customer1Result, customer2Result); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/manager/customer/ManagerCustomerCommandServiceTest.java b/backend/src/test/java/com/stampcrush/backend/application/manager/customer/ManagerCustomerCommandServiceTest.java new file mode 100644 index 000000000..7f59c3184 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/manager/customer/ManagerCustomerCommandServiceTest.java @@ -0,0 +1,51 @@ +package com.stampcrush.backend.application.manager.customer; + +import com.stampcrush.backend.application.ServiceSliceTest; +import com.stampcrush.backend.entity.user.TemporaryCustomer; +import com.stampcrush.backend.exception.CustomerBadRequestException; +import com.stampcrush.backend.repository.user.CustomerRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ServiceSliceTest +class ManagerCustomerCommandServiceTest { + + @InjectMocks + private ManagerCustomerCommandService managerCustomerCommandService; + + @Mock + private CustomerRepository customerRepository; + + @Test + void 임시_고객을_생성한다() { + // given + TemporaryCustomer temporaryCustomer = TemporaryCustomer.from("01012345678"); + when(customerRepository.findByPhoneNumber(temporaryCustomer.getPhoneNumber())).thenReturn(Collections.emptyList()); + when(customerRepository.save(any(TemporaryCustomer.class))).thenReturn(temporaryCustomer); + + // when + Long customerId = managerCustomerCommandService.createTemporaryCustomer(temporaryCustomer.getPhoneNumber()); + + // then + verify(customerRepository, times(1)).save(any(TemporaryCustomer.class)); + } + + @Test + void 존재하는_회원의_번호로_고객을_생성하려면_에러를_발생한다() { + // given + TemporaryCustomer temporaryCustomer = TemporaryCustomer.from("01012345678"); + when(customerRepository.findByPhoneNumber(temporaryCustomer.getPhoneNumber())).thenReturn(List.of(TemporaryCustomer.from("01012345678å"))); + + // when, then + assertThatThrownBy(() -> managerCustomerCommandService.createTemporaryCustomer(temporaryCustomer.getPhoneNumber())) + .isInstanceOf(CustomerBadRequestException.class); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/manager/customer/ManagerCustomerFindServiceTest.java b/backend/src/test/java/com/stampcrush/backend/application/manager/customer/ManagerCustomerFindServiceTest.java new file mode 100644 index 000000000..7b4b0ed16 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/manager/customer/ManagerCustomerFindServiceTest.java @@ -0,0 +1,65 @@ +package com.stampcrush.backend.application.manager.customer; + +import com.stampcrush.backend.application.ServiceSliceTest; +import com.stampcrush.backend.application.manager.customer.dto.CustomerFindDto; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.entity.user.TemporaryCustomer; +import com.stampcrush.backend.repository.user.CustomerRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ServiceSliceTest +class ManagerCustomerFindServiceTest { + + @InjectMocks + private ManagerCustomerFindService managerCustomerFindService; + + @Mock + private CustomerRepository customerRepository; + + @Test + void 전화번호로_가입_고객을_조회한다() { + // given + Customer customer = new RegisterCustomer("제나", "01012345678", "jena", "1234"); + when(customerRepository.findByPhoneNumber(customer.getPhoneNumber())).thenReturn(Collections.singletonList(customer)); + + // when + List findCustomer = managerCustomerFindService.findCustomer("01012345678"); + + // then + assertThat(findCustomer).containsExactly(CustomerFindDto.from(customer)); + } + + @Test + void 전화번호로_임시_고객을_조회한다() { + // given + Customer customer = TemporaryCustomer.from("01012345678"); + when(customerRepository.findByPhoneNumber(customer.getPhoneNumber())).thenReturn(Collections.singletonList(customer)); + + // when + List findCustomer = managerCustomerFindService.findCustomer("01012345678"); + + // then + assertThat(findCustomer).containsExactly(CustomerFindDto.from(customer)); + } + + @Test + void 고객이_존재하지_않는_경우_빈_배열을_반환한다() { + // given + String notExistPhoneNumber = "01000000000"; + when(customerRepository.findByPhoneNumber(notExistPhoneNumber)).thenReturn(Collections.emptyList()); + + // when + List findCustomer = managerCustomerFindService.findCustomer(notExistPhoneNumber); + + assertThat(findCustomer.size()).isEqualTo(0); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/manager/reward/ManagerRewardCommandServiceTest.java b/backend/src/test/java/com/stampcrush/backend/application/manager/reward/ManagerRewardCommandServiceTest.java new file mode 100644 index 000000000..5a1e2d230 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/manager/reward/ManagerRewardCommandServiceTest.java @@ -0,0 +1,186 @@ +package com.stampcrush.backend.application.manager.reward; + +import com.stampcrush.backend.application.ServiceSliceTest; +import com.stampcrush.backend.application.manager.reward.dto.RewardFindDto; +import com.stampcrush.backend.application.manager.reward.dto.RewardFindResultDto; +import com.stampcrush.backend.application.manager.reward.dto.RewardUsedUpdateDto; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.reward.Reward; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.entity.user.TemporaryCustomer; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.reward.RewardRepository; +import com.stampcrush.backend.repository.user.OwnerRepository; +import com.stampcrush.backend.repository.user.RegisterCustomerRepository; +import com.stampcrush.backend.repository.user.TemporaryCustomerRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@Transactional +@SpringBootTest +@ServiceSliceTest +class ManagerRewardCommandServiceTest { + + @Autowired + private ManagerRewardCommandService managerRewardCommandService; + + @Autowired + private ManagerRewardFindService managerRewardFindService; + + @Autowired + private RewardRepository rewardRepository; + + @Autowired + private OwnerRepository ownerRepository; + + @Autowired + private CafeRepository cafeRepository; + + @Autowired + private RegisterCustomerRepository registerCustomerRepository; + + @Autowired + private TemporaryCustomerRepository temporaryCustomerRepository; + + private Owner owner_1; + private Owner owner_2; + private Cafe cafe_1; + private Cafe cafe_2; + private Customer registerCustomer_1; + private Customer registerCustomer_2; + private Customer temporaryCustomer; + private Reward unusedReward; + + @BeforeEach + void setUp() { + owner_1 = ownerRepository.save(new Owner("lisa", "lisa@naver.com", "1234", "01011111111")); + owner_2 = ownerRepository.save(new Owner("tommy", "tommy@naver.com", "5678", "01099999999")); + + cafe_1 = cafeRepository.save(new Cafe("stamp-crush", LocalTime.of(18, 0), LocalTime.of(23, 59), "0211111111", "imageUrl", "안녕하세요", "잠실도로명", "14층", "11-11111", owner_1)); + cafe_2 = cafeRepository.save(new Cafe("wrongCafe", LocalTime.of(18, 0), LocalTime.of(23, 59), "0211111111", "imageUrl", "안녕하세요", "잠실도로", "1층", "11-11111", owner_2)); + + registerCustomer_1 = registerCustomerRepository.save(new RegisterCustomer("registered", "01022222222", "ehdgur@naver.com", "1111")); + registerCustomer_2 = registerCustomerRepository.save(new RegisterCustomer("registered2", "01044444444", "dsadsa@naver.com", "2345")); + temporaryCustomer = temporaryCustomerRepository.save(TemporaryCustomer.from("01033333333")); + + unusedReward = rewardRepository.save(new Reward("Americano", registerCustomer_1, cafe_1)); + } + + @Test + void 리워드를_사용한다() { + // given + RewardUsedUpdateDto rewardUsedUpdateDto = new RewardUsedUpdateDto(unusedReward.getId(), registerCustomer_1.getId(), cafe_1.getId(), true); + + // when + managerRewardCommandService.useReward(rewardUsedUpdateDto); + + // then + assertThat(unusedReward.getUsed()).isTrue(); + } + + @Test + void 존재하지_않는_리워드를_사용하려하면_예외를_던진다() { + // given + RewardUsedUpdateDto rewardUsedUpdateDto = new RewardUsedUpdateDto(unusedReward.getId(), Long.MAX_VALUE, cafe_1.getId(), true); + + // when, then + assertThatThrownBy(() -> managerRewardCommandService.useReward(rewardUsedUpdateDto)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 존재하지_않는_고객에_대한_리워드를_사용하려하면_예외를_던진다() { + // given + RewardUsedUpdateDto rewardUsedUpdateDto = new RewardUsedUpdateDto(Long.MAX_VALUE, registerCustomer_1.getId(), cafe_1.getId(), true); + + // when, then + assertThatThrownBy(() -> managerRewardCommandService.useReward(rewardUsedUpdateDto)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 존재하지_않는_카페에서_리워드를_사용하려하면_예외를_던진다() { + // given + RewardUsedUpdateDto rewardUsedUpdateDto = new RewardUsedUpdateDto(unusedReward.getId(), registerCustomer_1.getId(), Long.MAX_VALUE, true); + + // when, then + assertThatThrownBy(() -> managerRewardCommandService.useReward(rewardUsedUpdateDto)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 임시회원이_리워드를_사용하려하면_예외를_던진다() { + //given + RewardUsedUpdateDto rewardUsedUpdateDto = new RewardUsedUpdateDto(unusedReward.getId(), temporaryCustomer.getId(), cafe_1.getId(), true); + + // when, then + assertThatThrownBy(() -> managerRewardCommandService.useReward(rewardUsedUpdateDto)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 이미_사용된_리워드를_사용하려하면_예외를_던진다() { + //given + RewardUsedUpdateDto rewardUsedUpdateDto = new RewardUsedUpdateDto(unusedReward.getId(), registerCustomer_1.getId(), cafe_1.getId(), true); + managerRewardCommandService.useReward(rewardUsedUpdateDto); + + // when, then + assertThatThrownBy(() -> managerRewardCommandService.useReward(rewardUsedUpdateDto)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 다른_카페에의해_생성된_리워드를_사용하려하면_예외를_던진다() { + //given + RewardUsedUpdateDto rewardUsedUpdateDto = new RewardUsedUpdateDto(unusedReward.getId(), registerCustomer_1.getId(), cafe_2.getId(), true); + + // when, then + assertThatThrownBy(() -> managerRewardCommandService.useReward(rewardUsedUpdateDto)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 소유자가_아닌_고객이_리워드를_사용하려하면_예외를_던진다() { + //given + RewardUsedUpdateDto rewardUsedUpdateDto = new RewardUsedUpdateDto(unusedReward.getId(), registerCustomer_2.getId(), cafe_1.getId(), true); + + // when, then + assertThatThrownBy(() -> managerRewardCommandService.useReward(rewardUsedUpdateDto)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 사용된_리워드_목록을_조회한다() { + // given + RewardFindDto rewardFindDto = new RewardFindDto(registerCustomer_1.getId(), cafe_1.getId(), true); + + // when + List rewards = managerRewardFindService.findRewards(rewardFindDto); + + // then + assertThat(rewards).hasSize(0); + } + + @Test + void 미사용_리워드_목록을_조회한다() { + // given + RewardFindDto rewardFindDto = new RewardFindDto(registerCustomer_1.getId(), cafe_1.getId(), false); + + // when + List rewards = managerRewardFindService.findRewards(rewardFindDto); + + // then + assertThat(rewards).hasSize(1); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/manager/sample/ManagerSampleCouponFindServiceTest.java b/backend/src/test/java/com/stampcrush/backend/application/manager/sample/ManagerSampleCouponFindServiceTest.java new file mode 100644 index 000000000..44ad98e1c --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/manager/sample/ManagerSampleCouponFindServiceTest.java @@ -0,0 +1,72 @@ +package com.stampcrush.backend.application.manager.sample; + +import com.stampcrush.backend.application.ServiceSliceTest; +import com.stampcrush.backend.application.manager.sample.dto.SampleCouponsFindResultDto; +import com.stampcrush.backend.entity.sample.SampleBackImage; +import com.stampcrush.backend.entity.sample.SampleFrontImage; +import com.stampcrush.backend.entity.sample.SampleStampCoordinate; +import com.stampcrush.backend.entity.sample.SampleStampImage; +import com.stampcrush.backend.fixture.SampleCouponFixture; +import com.stampcrush.backend.repository.sample.SampleBackImageRepository; +import com.stampcrush.backend.repository.sample.SampleFrontImageRepository; +import com.stampcrush.backend.repository.sample.SampleStampCoordinateRepository; +import com.stampcrush.backend.repository.sample.SampleStampImageRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.when; + +@ServiceSliceTest +class ManagerSampleCouponFindServiceTest { + + @InjectMocks + private ManagerSampleCouponFindService managerSampleCouponFindService; + + @Mock + private SampleFrontImageRepository sampleFrontImageRepository; + + @Mock + private SampleBackImageRepository sampleBackImageRepository; + + @Mock + private SampleStampCoordinateRepository sampleStampCoordinateRepository; + + @Mock + private SampleStampImageRepository sampleStampImageRepository; + + @Test + void 샘플_쿠폰을_조회한다() { + List sampleFrontImage = List.of(SampleCouponFixture.SAMPLE_FRONT_IMAGE); + List sampleBackImage = List.of(SampleCouponFixture.SAMPLE_BACK_IMAGE); + List sampleStampImage = List.of(SampleCouponFixture.SAMPLE_STAMP_IMAGE); + List sampleStampCoordinates = SampleCouponFixture.SAMPLE_COORDINATES_SIZE_EIGHT; + + when(sampleFrontImageRepository.findAll()) + .thenReturn(sampleFrontImage); + when(sampleBackImageRepository.findAll()) + .thenReturn(sampleBackImage); + when(sampleStampImageRepository.findAll()) + .thenReturn(sampleStampImage); + when(sampleStampCoordinateRepository.findSampleStampCoordinateBySampleBackImage(SampleCouponFixture.SAMPLE_BACK_IMAGE)) + .thenReturn(sampleStampCoordinates); + + int maxStampCount = 8; + + assertAll( + () -> assertThat(managerSampleCouponFindService.findSampleCouponsByMaxStampCount(maxStampCount)).isNotNull(), + () -> assertThat(managerSampleCouponFindService.findSampleCouponsByMaxStampCount(maxStampCount)) + .usingRecursiveComparison() + .isEqualTo(SampleCouponsFindResultDto.of( + sampleFrontImage, + sampleBackImage, + sampleStampCoordinates, + sampleStampImage + )) + ); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/visitor/cafe/VisitorCafeFindServiceTest.java b/backend/src/test/java/com/stampcrush/backend/application/visitor/cafe/VisitorCafeFindServiceTest.java new file mode 100644 index 000000000..9740dd105 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/visitor/cafe/VisitorCafeFindServiceTest.java @@ -0,0 +1,52 @@ +package com.stampcrush.backend.application.visitor.cafe; + +import com.stampcrush.backend.application.ServiceSliceTest; +import com.stampcrush.backend.application.visitor.cafe.dto.CafeInfoFindByCustomerResultDto; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.exception.CafeNotFoundException; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Optional; + +import static com.stampcrush.backend.fixture.CafeFixture.GITCHAN_CAFE; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.Mockito.when; + +@ServiceSliceTest +class VisitorCafeFindServiceTest { + + @InjectMocks + private VisitorCafeFindService visitorCafeFindService; + + @Mock + private CafeRepository cafeRepository; + + @Test + void 고객이_카페_정보를_조회한다() { + // given + Long cafeId = 1L; + Cafe cafe = GITCHAN_CAFE; + when(cafeRepository.findById(cafeId)).thenReturn(Optional.of(cafe)); + + // when + CafeInfoFindByCustomerResultDto findCafe = visitorCafeFindService.findCafeById(cafeId); + + // then + assertThat(findCafe).usingRecursiveComparison().isEqualTo(cafe); + } + + @Test + void 존재하지_않는_카페_정보_조회_요청_시_에러_발생한다() { + // given + Long notExistcafeId = 1L; + when(cafeRepository.findById(notExistcafeId)).thenReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> visitorCafeFindService.findCafeById(notExistcafeId)) + .isInstanceOf(CafeNotFoundException.class); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/visitor/coupon/VisitorCouponCommandServiceTest.java b/backend/src/test/java/com/stampcrush/backend/application/visitor/coupon/VisitorCouponCommandServiceTest.java new file mode 100644 index 000000000..e83e210d0 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/visitor/coupon/VisitorCouponCommandServiceTest.java @@ -0,0 +1,35 @@ +package com.stampcrush.backend.application.visitor.coupon; + +import com.stampcrush.backend.application.ServiceSliceTest; +import com.stampcrush.backend.exception.CouponNotFoundException; +import com.stampcrush.backend.repository.coupon.CouponRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +@ServiceSliceTest +public class VisitorCouponCommandServiceTest { + + @InjectMocks + private VisitorCouponCommandService visitorCouponCommandService; + + @Mock + private CouponRepository couponRepository; + + @Test + void 삭제하려는_쿠폰을_찾지_못할_시_예외를_던진다() { + // given + when(couponRepository.findByIdAndCustomerId(anyLong(), anyLong())) + .thenReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> visitorCouponCommandService.deleteCoupon(1L, 1L)) + .isInstanceOf(CouponNotFoundException.class); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/visitor/coupon/VisitorCouponFindServiceTest.java b/backend/src/test/java/com/stampcrush/backend/application/visitor/coupon/VisitorCouponFindServiceTest.java new file mode 100644 index 000000000..5b38221c5 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/visitor/coupon/VisitorCouponFindServiceTest.java @@ -0,0 +1,100 @@ +package com.stampcrush.backend.application.visitor.coupon; + +import com.stampcrush.backend.application.ServiceSliceTest; +import com.stampcrush.backend.application.visitor.coupon.dto.CustomerCouponFindResultDto; +import com.stampcrush.backend.application.visitor.favorites.VisitorFavoritesFindService; +import com.stampcrush.backend.entity.coupon.Coupon; +import com.stampcrush.backend.entity.coupon.CouponStatus; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.exception.CustomerNotFoundException; +import com.stampcrush.backend.fixture.CustomerFixture; +import com.stampcrush.backend.repository.coupon.CouponRepository; +import com.stampcrush.backend.repository.coupon.CouponStampCoordinateRepository; +import com.stampcrush.backend.repository.user.CustomerRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static com.stampcrush.backend.fixture.CouponFixture.GITCHAN_CAFE_COUPON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.when; + +@ServiceSliceTest +class VisitorCouponFindServiceTest { + + @InjectMocks + private VisitorCouponFindService visitorCouponFindService; + + @Mock + private VisitorFavoritesFindService visitorFavoritesFindService; + + @Mock + private CustomerRepository customerRepository; + + @Mock + private CouponRepository couponRepository; + + @Mock + private CouponStampCoordinateRepository couponStampCoordinateRepository; + + @Test + void 하나의_카페당_하나의_쿠폰을_조회할_때_고객정보가_없으면_예외처리한다() { + long customerId = 1L; + + when(customerRepository.findById(customerId)) + .thenReturn(Optional.empty()); + + assertThatThrownBy(() -> visitorCouponFindService.findOneCouponForOneCafe(customerId)) + .isInstanceOf(CustomerNotFoundException.class); + } + + @Test + void 하나의_카페당_하나의_쿠폰을_조회할_때_ACCUMULATING_상태의_쿠폰이_없으면_아무것도_조회하지_않는다() { + long customerId = 1L; + RegisterCustomer customer = CustomerFixture.REGISTER_CUSTOMER_GITCHAN; + + when(customerRepository.findById(customerId)) + .thenReturn(Optional.of(customer)); + + when(couponRepository.findFilteredAndSortedCoupons(customer, CouponStatus.ACCUMULATING)) + .thenReturn(new ArrayList<>()); + + assertThat(visitorCouponFindService.findOneCouponForOneCafe(customerId)).isEmpty(); + } + + @Test + void 하나의_카페당_하나의_쿠폰을_조회할_수_있다() { + long customerId = 1L; + RegisterCustomer customer = CustomerFixture.REGISTER_CUSTOMER_GITCHAN; + Coupon gitchanCoupon = GITCHAN_CAFE_COUPON; + + when(customerRepository.findById(customerId)) + .thenReturn(Optional.of(customer)); + + when(couponRepository.findFilteredAndSortedCoupons(customer, CouponStatus.ACCUMULATING)) + .thenReturn(List.of(gitchanCoupon)); + + when(visitorFavoritesFindService.findIsFavorites(gitchanCoupon.getCafe(), customer)) + .thenReturn(true); + + assertAll( + () -> assertThat(visitorCouponFindService.findOneCouponForOneCafe(customerId)).isNotEmpty(), + () -> assertThat(visitorCouponFindService.findOneCouponForOneCafe(customerId)) + .usingRecursiveComparison() + .isEqualTo( + List.of( + CustomerCouponFindResultDto.of( + gitchanCoupon.getCafe(), + gitchanCoupon, + true, + gitchanCoupon.getCouponDesign().getCouponStampCoordinates() + ))) + ); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/visitor/favorites/VisitorFavoritesCommandServiceTest.java b/backend/src/test/java/com/stampcrush/backend/application/visitor/favorites/VisitorFavoritesCommandServiceTest.java new file mode 100644 index 000000000..a0b39ca63 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/visitor/favorites/VisitorFavoritesCommandServiceTest.java @@ -0,0 +1,98 @@ +package com.stampcrush.backend.application.visitor.favorites; + +import com.stampcrush.backend.application.ServiceSliceTest; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.favorites.Favorites; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.exception.CafeNotFoundException; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.favorites.FavoritesRepository; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +@ServiceSliceTest +public class VisitorFavoritesCommandServiceTest { + + @InjectMocks + private VisitorFavoritesCommandService visitorFavoritesCommandService; + + @Mock + private CafeRepository cafeRepository; + + @Mock + private FavoritesRepository favoritesRepository; + + @Test + void 존재하지_않는_카페를_즐겨찾기에_등록_또는_해제_하려하면_예외를_던진다() { + // given + RegisterCustomer customer = new RegisterCustomer("hardy", "01000000000", "ehdgur4814", "1234"); + Long cafeId = 1L; + Boolean isFavorites = Boolean.TRUE; + + when(cafeRepository.findById(anyLong())) + .thenReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> visitorFavoritesCommandService.changeFavorites(customer.getId(), cafeId, isFavorites)) + .isInstanceOf(CafeNotFoundException.class); + } + + @Test + @Disabled + // TODO: 하디는 해결하시오 + void 카페를_즐겨찾기_목록에_이미_존재하지_않을_경우_새로_만들어_저장한다() { + // given + RegisterCustomer customer = new RegisterCustomer("hardy", "01000000000", "ehdgur4814", "1234"); + Long cafeId = 1L; + Boolean isFavorites = Boolean.TRUE; + Cafe cafe = new Cafe("하디카페", "동작구", "이수동", "1111", new Owner("하디", "hardy@", "1234", "010111111111")); + + when(cafeRepository.findById(anyLong())) + .thenReturn(Optional.of(cafe)); + when(favoritesRepository.findByCafeAndCustomer(cafe, customer)) + .thenReturn(Optional.empty()); + + // when + visitorFavoritesCommandService.changeFavorites(customer.getId(), cafeId, isFavorites); + + // then + verify(cafeRepository, times(1)).findById(anyLong()); + verify(favoritesRepository, times(1)).findByCafeAndCustomer(cafe, customer); + verify(favoritesRepository, times(1)).save(any(Favorites.class)); + } + + @Test + @Disabled + // TODO: 하디는 해결하시오 + void 카페를_즐겨찾기_목록에_이미_존재할_경우_새로_저장하지_않고_변경한다() { + // given + RegisterCustomer customer = new RegisterCustomer("hardy", "01000000000", "ehdgur4814", "1234"); + Long cafeId = 1L; + Boolean isFavorites = Boolean.TRUE; + Cafe cafe = new Cafe("하디카페", "동작구", "이수동", "1111", new Owner("하디", "hardy@", "1234", "010111111111")); + Favorites favorites = new Favorites(cafe, customer, isFavorites); + + when(cafeRepository.findById(anyLong())) + .thenReturn(Optional.of(cafe)); + when(favoritesRepository.findByCafeAndCustomer(cafe, customer)) + .thenReturn(Optional.of(favorites)); + + // when + visitorFavoritesCommandService.changeFavorites(customer.getId(), cafeId, isFavorites); + + // then + verify(cafeRepository, times(1)).findById(anyLong()); + verify(favoritesRepository, times(1)).findByCafeAndCustomer(cafe, customer); + verify(favoritesRepository, times(0)).save(any(Favorites.class)); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/visitor/profile/VisitorProfilesCommandServiceTest.java b/backend/src/test/java/com/stampcrush/backend/application/visitor/profile/VisitorProfilesCommandServiceTest.java new file mode 100644 index 000000000..d741877b7 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/visitor/profile/VisitorProfilesCommandServiceTest.java @@ -0,0 +1,32 @@ +package com.stampcrush.backend.application.visitor.profile; + +import com.stampcrush.backend.application.ServiceSliceTest; +import com.stampcrush.backend.fixture.CustomerFixture; +import com.stampcrush.backend.repository.user.CustomerRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +@ServiceSliceTest +class VisitorProfilesCommandServiceTest { + + @InjectMocks + private VisitorProfilesCommandService visitorProfilesCommandService; + + @Mock + private CustomerRepository customerRepository; + + @Test + void 고객의_전화번호를_저장한다() { + given(customerRepository.findById(any())) + .willReturn(Optional.of(CustomerFixture.REGISTER_CUSTOMER_GITCHAN)); + + assertDoesNotThrow(() -> visitorProfilesCommandService.registerPhoneNumber(any(), "01012345678")); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/visitor/profile/VisitorProfilesCommandServiceTest2.java b/backend/src/test/java/com/stampcrush/backend/application/visitor/profile/VisitorProfilesCommandServiceTest2.java new file mode 100644 index 000000000..7dd4f193b --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/visitor/profile/VisitorProfilesCommandServiceTest2.java @@ -0,0 +1,38 @@ +package com.stampcrush.backend.application.visitor.profile; + +import com.stampcrush.backend.auth.OAuthProvider; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.repository.user.CustomerRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class VisitorProfilesCommandServiceTest2 { + + @Autowired + private VisitorProfilesCommandService visitorProfilesCommandService; + + @Autowired + private CustomerRepository customerRepository; + + @Test + void 전화번호_등록한다() { + RegisterCustomer gitchan = customerRepository.save(RegisterCustomer.builder() + .nickname("깃짱") + .email("gitchan@naver.com") + .oAuthId(1L) + .oAuthProvider(OAuthProvider.KAKAO) + .build() + ); + + String phoneNumber = "01012345678"; + visitorProfilesCommandService.registerPhoneNumber(gitchan.getId(), phoneNumber); + + Customer findGitchan = customerRepository.findById(gitchan.getId()).get(); + assertThat(findGitchan.getPhoneNumber()).isEqualTo(phoneNumber); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/visitor/reward/VisitorRewardsFindServiceTest.java b/backend/src/test/java/com/stampcrush/backend/application/visitor/reward/VisitorRewardsFindServiceTest.java new file mode 100644 index 000000000..8d1fc7cae --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/visitor/reward/VisitorRewardsFindServiceTest.java @@ -0,0 +1,82 @@ +package com.stampcrush.backend.application.visitor.reward; + +import com.stampcrush.backend.application.ServiceSliceTest; +import com.stampcrush.backend.application.visitor.reward.dto.VisitorRewardsFindResultDto; +import com.stampcrush.backend.entity.reward.Reward; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.exception.CustomerNotFoundException; +import com.stampcrush.backend.fixture.CustomerFixture; +import com.stampcrush.backend.fixture.RewardFixture; +import com.stampcrush.backend.repository.reward.RewardRepository; +import com.stampcrush.backend.repository.user.CustomerRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; + +@ServiceSliceTest +class VisitorRewardsFindServiceTest { + + @InjectMocks + private VisitorRewardsFindService visitorRewardsFindService; + + @Mock + private RewardRepository rewardRepository; + + @Mock + private CustomerRepository customerRepository; + + @Test + void 리워드를_조회할_때_해당_고객이_존재하지_않으면_예외처리한다() { + given(customerRepository.findById(anyLong())) + .willReturn(Optional.empty()); + + assertThatThrownBy(() -> visitorRewardsFindService.findRewards(1L, true)) + .isInstanceOf(CustomerNotFoundException.class); + } + + @Test + void 리워드를_조회한다() { + // given + RegisterCustomer customer = CustomerFixture.REGISTER_CUSTOMER_GITCHAN_SAVED; + Reward availableReward = RewardFixture.REWARD_USED_TRUE; + Reward usedReward = RewardFixture.REWARD_USED_FALSE; + + given(customerRepository.findById(anyLong())) + .willReturn(Optional.of(customer)); + given(rewardRepository.findAllByCustomerAndUsed(any(RegisterCustomer.class), eq(true))) + .willReturn(List.of(availableReward)); + given(rewardRepository.findAllByCustomerAndUsed(any(RegisterCustomer.class), eq(false))) + .willReturn(List.of(usedReward)); + + // when + List availableRewards = visitorRewardsFindService.findRewards(1L, false); + List usedRewards = visitorRewardsFindService.findRewards(1L, true); + + // then + assertAll( + () -> assertThat(availableRewards) + .usingRecursiveComparison() + .isEqualTo( + List.of( + VisitorRewardsFindResultDto.from(availableReward) + ) + ), + () -> assertThat(usedRewards) + .usingRecursiveComparison() + .isEqualTo( + List.of( + VisitorRewardsFindResultDto.from(usedReward) + ) + ) + ); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/application/visitor/visithistory/VisitorVisitHistoryFindServiceTest.java b/backend/src/test/java/com/stampcrush/backend/application/visitor/visithistory/VisitorVisitHistoryFindServiceTest.java new file mode 100644 index 000000000..265e97f88 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/application/visitor/visithistory/VisitorVisitHistoryFindServiceTest.java @@ -0,0 +1,39 @@ +package com.stampcrush.backend.application.visitor.visithistory; + +import com.stampcrush.backend.application.ServiceSliceTest; +import com.stampcrush.backend.exception.CustomerNotFoundException; +import com.stampcrush.backend.repository.user.CustomerRepository; +import com.stampcrush.backend.repository.visithistory.VisitHistoryRepository; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; + +@ServiceSliceTest +class VisitorVisitHistoryFindServiceTest { + + @InjectMocks + private VisitorVisitHistoryFindService visitorVisitHistoryFindService; + + @Mock + private VisitHistoryRepository visitHistoryRepository; + + @Mock + private CustomerRepository customerRepository; + + @Test + void 존재하지_않는_고객이_스탬프_적립_내역_조회하면_예외발생() { + // given, when + given(customerRepository.findById(anyLong())) + .willReturn(Optional.empty()); + + // then + assertThatThrownBy(() -> visitorVisitHistoryFindService.findStampHistoriesByCustomer(1L)) + .isInstanceOf(CustomerNotFoundException.class); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/common/DataCleaner.java b/backend/src/test/java/com/stampcrush/backend/common/DataCleaner.java new file mode 100644 index 000000000..1302c3d95 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/common/DataCleaner.java @@ -0,0 +1,48 @@ +package com.stampcrush.backend.common; + +import jakarta.annotation.PostConstruct; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Profile("test") +@Component +public class DataCleaner { + + private static final String FOREIGN_KEY_CHECK_FORMAT = "SET FOREIGN_KEY_CHECKS %d"; + private static final String TRUNCATE_FORMAT = "TRUNCATE TABLE %s"; + + private final List tableNames = new ArrayList<>(); + + @PersistenceContext + private EntityManager entityManager; + + @SuppressWarnings("unchecked") + @PostConstruct + public void findDatabaseTableNames() { + List tableInfos = entityManager.createNativeQuery("SHOW TABLES").getResultList(); + for (Object[] tableInfo : tableInfos) { + String tableName = (String) tableInfo[0]; + tableNames.add(tableName); + } + } + + @Transactional + public void clear() { + entityManager.clear(); + truncate(); + } + + private void truncate() { + entityManager.createNativeQuery(String.format(FOREIGN_KEY_CHECK_FORMAT, 0)).executeUpdate(); + for (String tableName : tableNames) { + entityManager.createNativeQuery(String.format(TRUNCATE_FORMAT, tableName)).executeUpdate(); + } + entityManager.createNativeQuery(String.format(FOREIGN_KEY_CHECK_FORMAT, 1)).executeUpdate(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/common/DataClearExtension.java b/backend/src/test/java/com/stampcrush/backend/common/DataClearExtension.java new file mode 100644 index 000000000..f63557654 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/common/DataClearExtension.java @@ -0,0 +1,19 @@ +package com.stampcrush.backend.common; + +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +public class DataClearExtension implements BeforeEachCallback { + + @Override + public void beforeEach(ExtensionContext context) { + DataCleaner dataCleaner = getDataCleaner(context); + dataCleaner.clear(); + } + + private DataCleaner getDataCleaner(ExtensionContext extensionContext) { + return SpringExtension.getApplicationContext(extensionContext) + .getBean(DataCleaner.class); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/common/KorNamingConverter.java b/backend/src/test/java/com/stampcrush/backend/common/KorNamingConverter.java new file mode 100644 index 000000000..f38366395 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/common/KorNamingConverter.java @@ -0,0 +1,16 @@ +package com.stampcrush.backend.common; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +public @interface KorNamingConverter { +} diff --git a/backend/src/test/java/com/stampcrush/backend/entity/cafe/CafePolicyTest.java b/backend/src/test/java/com/stampcrush/backend/entity/cafe/CafePolicyTest.java new file mode 100644 index 000000000..99be51669 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/entity/cafe/CafePolicyTest.java @@ -0,0 +1,19 @@ +package com.stampcrush.backend.entity.cafe; + +import com.stampcrush.backend.common.KorNamingConverter; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +@KorNamingConverter +class CafePolicyTest { + + @Test + void 보상_스탬프_개수를_토대로_스탬프에_대해_몇_개의_보상이_발급되는지_계산한다() { + // given, when + CafePolicy cafePolicy = new CafePolicy(10, "reward", 6, false, null); + int rewardCouponCount = cafePolicy.calculateRewardCouponCount(76); + + // then + Assertions.assertThat(rewardCouponCount).isEqualTo(7); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/entity/coupon/CouponTest.java b/backend/src/test/java/com/stampcrush/backend/entity/coupon/CouponTest.java new file mode 100644 index 000000000..7ed53f6d5 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/entity/coupon/CouponTest.java @@ -0,0 +1,206 @@ +package com.stampcrush.backend.entity.coupon; + +import com.stampcrush.backend.common.KorNamingConverter; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.TemporaryCustomer; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@KorNamingConverter +class CouponTest { + + @Test + void 쿠폰이_현재_USING_상태인지_확인한다() { + // given, when + Coupon coupon = new Coupon(LocalDate.EPOCH, TemporaryCustomer.from("01012345678"), new Cafe( + "하디까페", + LocalTime.of(12, 30), + LocalTime.of(18, 30), + "0211111111", + "http://www.cafeImage.com", + "안녕하세요", + "잠실동12길", + "14층", + "11111111", + new Owner("이름", "아이디", "비번", "01012345678")), new CouponDesign(), new CouponPolicy()); + + // then + assertAll( + () -> assertThat(coupon.isAccumulating()).isTrue(), + () -> assertThat(coupon.isRewarded()).isFalse() + ); + } + + @Test + void 쿠폰이_현재_REWARD_상태인지_확인한다() { + // given + Coupon coupon = new Coupon(LocalDate.EPOCH, TemporaryCustomer.from("01012345678"), new Cafe( + "하디까페", + LocalTime.of(12, 30), + LocalTime.of(18, 30), + "0211111111", + "http://www.cafeImage.com", + "안녕하세요", + "잠실동12길", + "14층", + "11111111", + new Owner("이름", "아이디", "비번", "01012345678")), new CouponDesign(), new CouponPolicy()); + + // when + coupon.reward(); + + // then + assertAll( + () -> assertThat(coupon.isAccumulating()).isFalse(), + () -> assertThat(coupon.isRewarded()).isTrue() + ); + } + + @Test + void 스탬프를_적립한다() { + // given + Coupon coupon = new Coupon(LocalDate.EPOCH, TemporaryCustomer.from("01012345678"), new Cafe( + "하디까페", + LocalTime.of(12, 30), + LocalTime.of(18, 30), + "0211111111", + "http://www.cafeImage.com", + "안녕하세요", + "잠실동12길", + "14층", + "11111111", + new Owner("이름", "아이디", "비번", "01012345678")), new CouponDesign(), new CouponPolicy(2, "짱", 10) + ); + + // when + coupon.accumulate(1); + + // then + assertThat(coupon.getStatus()).isSameAs(CouponStatus.ACCUMULATING); + assertThat(coupon.getStampCount()).isEqualTo(1); + } + + @Test + void 스탬프를_적립하고_최대_스탬프_개수와_같아지면_쿠폰_상태가_REWARDED가_된다() { + // given + Coupon coupon = new Coupon(LocalDate.EPOCH, TemporaryCustomer.from("01012345678"), new Cafe( + "하디까페", + LocalTime.of(12, 30), + LocalTime.of(18, 30), + "0211111111", + "http://www.cafeImage.com", + "안녕하세요", + "잠실동12길", + "14층", + "11111111", + new Owner("이름", "아이디", "비번", "01012345678")), new CouponDesign(), new CouponPolicy(2, "짱", 10) + ); + + // when + coupon.accumulate(2); + + // then + assertThat(coupon.getStatus()).isSameAs(CouponStatus.REWARDED); + assertThat(coupon.getStampCount()).isEqualTo(2); + } + + @Test + void 스탬프를_적립하고_최대_스탬프_개수와_같아지면_쿠폰_상태가_REWARDED가_된다1() { + // given + Coupon coupon = new Coupon(LocalDate.EPOCH, TemporaryCustomer.from("01012345678"), new Cafe( + "하디까페", + LocalTime.of(12, 30), + LocalTime.of(18, 30), + "0211111111", + "http://www.cafeImage.com", + "안녕하세요", + "잠실동12길", + "14층", + "11111111", + new Owner("이름", "아이디", "비번", "01012345678")), new CouponDesign(), new CouponPolicy(2, "짱", 10) + ); + + // when + coupon.accumulate(2); + + // then + assertThat(coupon.getStatus()).isSameAs(CouponStatus.REWARDED); + assertThat(coupon.getStampCount()).isEqualTo(2); + } + + @Test + void 보상까지_남은_스탬프_개수를_계산한다() { + // given + Coupon coupon = new Coupon(LocalDate.EPOCH, TemporaryCustomer.from("01012345678"), new Cafe( + "하디까페", + LocalTime.of(12, 30), + LocalTime.of(18, 30), + "0211111111", + "http://www.cafeImage.com", + "안녕하세요", + "잠실동12길", + "14층", + "11111111", + new Owner("이름", "아이디", "비번", "01012345678")), new CouponDesign(), new CouponPolicy(10, "짱", 10) + ); + + // when + coupon.accumulate(3); + int restStampCount = coupon.calculateRestStampCountForReward(); + + // then + assertThat(restStampCount).isEqualTo(7); + } + + @Test + void 스탬프_적립_시_찍힌_스탬프_합이_보상_개수보다_적으면_true다() { + // given + Coupon coupon = new Coupon(LocalDate.EPOCH, TemporaryCustomer.from("01012345678"), new Cafe( + "하디까페", + LocalTime.of(12, 30), + LocalTime.of(18, 30), + "0211111111", + "http://www.cafeImage.com", + "안녕하세요", + "잠실동12길", + "14층", + "11111111", + new Owner("이름", "아이디", "비번", "01012345678")), new CouponDesign(), new CouponPolicy(10, "짱", 10) + ); + + // when + coupon.accumulate(7); + + // then + assertThat(coupon.isLessThanMaxStampAfterAccumulateStamp(2)).isTrue(); + } + + @Test + void 스탬프_적립_시_찍힌_스탬프_합이_보상_개수와_같으면_true다() { + // given + Coupon coupon = new Coupon(LocalDate.EPOCH, TemporaryCustomer.from("01012345678"), new Cafe( + "하디까페", + LocalTime.of(12, 30), + LocalTime.of(18, 30), + "0211111111", + "http://www.cafeImage.com", + "안녕하세요", + "잠실동12길", + "14층", + "11111111", + new Owner("이름", "아이디", "비번", "01012345678")), new CouponDesign(), new CouponPolicy(10, "짱", 10) + ); + + // when + coupon.accumulate(7); + + // then + assertThat(coupon.isSameMaxStampAfterAccumulateStamp(3)).isTrue(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/entity/reward/RewardTest.java b/backend/src/test/java/com/stampcrush/backend/entity/reward/RewardTest.java new file mode 100644 index 000000000..0840c6773 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/entity/reward/RewardTest.java @@ -0,0 +1,91 @@ +package com.stampcrush.backend.entity.reward; + +import com.stampcrush.backend.common.KorNamingConverter; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.entity.user.TemporaryCustomer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.time.LocalTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@KorNamingConverter +@DataJpaTest +public class RewardTest { + + private Cafe cafe_1; + private Cafe cafe_2; + private Customer registerCustomer_1; + private Customer registerCustomer_2; + private Customer temporaryCustomer; + + @BeforeEach + void setUp() { + Owner owner_1 = new Owner("lisa", "lisa@naver.com", "1234", "01011111111"); + Owner owner_2 = new Owner("tommy", "tommy@naver.com", "5678", "01099999999"); + cafe_1 = new Cafe("stamp-crush", LocalTime.of(18, 0), LocalTime.of(23, 59), "0211111111", "imageUrl", "안녕하세요", "잠실도로명", "14층", "11-11111", owner_1); + cafe_2 = new Cafe("wrongCafe", LocalTime.of(18, 0), LocalTime.of(23, 59), "0211111111", "imageUrl", "안녕하세요", "잠실도로", "1층", "11-11111", owner_2); + registerCustomer_1 = new RegisterCustomer("registered", "01022222222", "ehdgur@naver.com", "1111"); + registerCustomer_2 = new RegisterCustomer("registered2", "01044444444", "dsadsa@naver.com", "2345"); + temporaryCustomer = TemporaryCustomer.from("01033333333"); + } + + @Test + void 가입회원이_리워드를_사용한다() { + // given + Reward reward = new Reward("Americano", registerCustomer_1, cafe_1); + + // when + reward.useReward(registerCustomer_1, cafe_1); + + // then + assertThat(reward.getUsed()).isTrue(); + } + + @Test + void 임시회원이_리워드를_사용하려하면_예외를_던진다() { + //given + Reward reward = new Reward("Americano", temporaryCustomer, cafe_1); + + // when, then + assertThatThrownBy(() -> reward.useReward(temporaryCustomer, cafe_1)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 이미_사용된_리워드를_사용려하면_예외를_던진다() { + // given + Reward reward = new Reward("Americano", registerCustomer_1, cafe_1); + reward.useReward(registerCustomer_1, cafe_1); + + // when, then + assertThatThrownBy(() -> reward.useReward(registerCustomer_1, cafe_1)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 다른_카페에의해_생성된_리워드를_사용하려하면_예외를_던진다() { + // given + Reward reward = new Reward("Americano", registerCustomer_1, cafe_1); + + // when, then + assertThatThrownBy(() -> reward.useReward(registerCustomer_1, cafe_2)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 소유자가_아닌_고객이_리워드를_사용하려하면_예외를_던진다() { + // given + Reward reward = new Reward("Americano", registerCustomer_1, cafe_1); + + // when, then + assertThatThrownBy(() -> reward.useReward(registerCustomer_2, cafe_1)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/entity/user/TemporaryCustomerTest.java b/backend/src/test/java/com/stampcrush/backend/entity/user/TemporaryCustomerTest.java new file mode 100644 index 000000000..22dd38a9c --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/entity/user/TemporaryCustomerTest.java @@ -0,0 +1,20 @@ +package com.stampcrush.backend.entity.user; + +import com.stampcrush.backend.common.KorNamingConverter; +import com.stampcrush.backend.exception.CustomerBadRequestException; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@KorNamingConverter +class TemporaryCustomerTest { + + @Test + void 전화번호_길이가_4자_이하일때_닉네임_생성_안되어_에러_발생() { + // given + String invalidPhoneNumber = "010"; + + // when, then + assertThatThrownBy(() -> TemporaryCustomer.from(invalidPhoneNumber)).isInstanceOf(CustomerBadRequestException.class); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/fixture/CafeFixture.java b/backend/src/test/java/com/stampcrush/backend/fixture/CafeFixture.java new file mode 100644 index 000000000..388e14def --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/fixture/CafeFixture.java @@ -0,0 +1,40 @@ +package com.stampcrush.backend.fixture; + +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.user.Owner; + +import java.time.LocalTime; + +public final class CafeFixture { + + public static final Cafe GITCHAN_CAFE = new Cafe( + "깃짱카페", + LocalTime.NOON, + LocalTime.MIDNIGHT, + "01012345678", + "cafeImageUrl", + "introduction", + "roadAddress", + "detailAddress", + "buisnessRegistrationNumber", + OwnerFixture.GITCHAN + ); + + public static Cafe cafeOfSavedOwner(Owner savedOwner) { + return new Cafe( + "깃짱카페", + LocalTime.NOON, + LocalTime.MIDNIGHT, + "01012345678", + "#", + "안녕하세요", + "서울시 올림픽로 어쩌고", + "루터회관", + "10-222-333", + savedOwner + ); + } + + private CafeFixture() { + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/fixture/CouponDesignFixture.java b/backend/src/test/java/com/stampcrush/backend/fixture/CouponDesignFixture.java new file mode 100644 index 000000000..8b8b5984b --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/fixture/CouponDesignFixture.java @@ -0,0 +1,29 @@ +package com.stampcrush.backend.fixture; + +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.cafe.CafeCouponDesign; +import com.stampcrush.backend.entity.coupon.CouponDesign; + +public final class CouponDesignFixture { + + public static final CouponDesign COUPON_DESIGN_1 = new CouponDesign("front", "back", "stamp"); + public static final CouponDesign COUPON_DESIGN_2 = new CouponDesign("front", "back", "stamp"); + public static final CouponDesign COUPON_DESIGN_3 = new CouponDesign("front", "back", "stamp"); + public static final CouponDesign COUPON_DESIGN_4 = new CouponDesign("front", "back", "stamp"); + public static final CouponDesign COUPON_DESIGN_5 = new CouponDesign("front", "back", "stamp"); + public static final CouponDesign COUPON_DESIGN_6 = new CouponDesign("front", "back", "stamp"); + public static final CouponDesign COUPON_DESIGN_7 = new CouponDesign("front", "back", "stamp"); + + public static CafeCouponDesign cafeCouponDesignOfSavedCafe(Cafe savedCafe) { + return new CafeCouponDesign( + "#", + "#", + "#", + false, + savedCafe + ); + } + + private CouponDesignFixture() { + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/fixture/CouponFixture.java b/backend/src/test/java/com/stampcrush/backend/fixture/CouponFixture.java new file mode 100644 index 000000000..244ad0f52 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/fixture/CouponFixture.java @@ -0,0 +1,25 @@ +package com.stampcrush.backend.fixture; + +import com.stampcrush.backend.entity.coupon.Coupon; +import com.stampcrush.backend.entity.coupon.CouponStampCoordinate; + +import java.time.LocalDate; +import java.util.List; + +public final class CouponFixture { + + public static final Coupon GITCHAN_CAFE_COUPON = new Coupon( + LocalDate.EPOCH, + CustomerFixture.REGISTER_CUSTOMER_GITCHAN_SAVED, + CafeFixture.GITCHAN_CAFE, + CouponDesignFixture.COUPON_DESIGN_1, + CouponPolicyFixture.COUPON_POLICY_1 + ); + + public static final List GITCHAN_CAFE_COUPON_STAMP_COORDINATE = List.of( + new CouponStampCoordinate(1, 1, 1, CouponDesignFixture.COUPON_DESIGN_1) + ); + + private CouponFixture() { + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/fixture/CouponPolicyFixture.java b/backend/src/test/java/com/stampcrush/backend/fixture/CouponPolicyFixture.java new file mode 100644 index 000000000..62c6bc45e --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/fixture/CouponPolicyFixture.java @@ -0,0 +1,24 @@ +package com.stampcrush.backend.fixture; + +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.cafe.CafePolicy; +import com.stampcrush.backend.entity.coupon.CouponPolicy; + +public final class CouponPolicyFixture { + + public static final CouponPolicy COUPON_POLICY_1 = new CouponPolicy(10, "아메리카노", 8); + public static final CouponPolicy COUPON_POLICY_2 = new CouponPolicy(10, "아메리카노", 8); + + public static CafePolicy cafePolicyOfSavedCafe(Cafe savedCafe) { + return new CafePolicy( + 2, + "아메리카노", + 12, + false, + savedCafe + ); + } + + private CouponPolicyFixture() { + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/fixture/CustomerFixture.java b/backend/src/test/java/com/stampcrush/backend/fixture/CustomerFixture.java new file mode 100644 index 000000000..207315b62 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/fixture/CustomerFixture.java @@ -0,0 +1,20 @@ +package com.stampcrush.backend.fixture; + +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.entity.user.TemporaryCustomer; + +public final class CustomerFixture { + + public static final TemporaryCustomer TEMPORARY_CUSTOMER_1 = TemporaryCustomer.from("깃짱 번호"); + public static final TemporaryCustomer TEMPORARY_CUSTOMER_2 = TemporaryCustomer.from("깃짱 번호"); + public static final TemporaryCustomer TEMPORARY_CUSTOMER_3 = TemporaryCustomer.from("깃짱 번호"); + public static final RegisterCustomer REGISTER_CUSTOMER_1 = new RegisterCustomer("깃짱 닉네임", "깃짱 번호", "깃짱 아이디", "깃짱 비번"); + public static final RegisterCustomer REGISTER_CUSTOMER_2 = new RegisterCustomer("깃짱 닉네임", "깃짱 번호", "깃짱 아이디", "깃짱 비번"); + public static final RegisterCustomer REGISTER_CUSTOMER_GITCHAN = new RegisterCustomer("깃짱", "01012345678", "gitchan", "password"); + public static final RegisterCustomer REGISTER_CUSTOMER_GITCHAN_SAVED = new RegisterCustomer(1L, "깃짱", "01012345678", "gitchan", "password"); + public static final RegisterCustomer REGISTER_CUSTOMER_JENA = new RegisterCustomer("jena", "01012345678", "jena", "1234"); + public static final RegisterCustomer REGISTER_CUSTOMER_YOUNGHO = new RegisterCustomer("name", "phone", "id", "pw"); + + private CustomerFixture() { + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/fixture/OwnerFixture.java b/backend/src/test/java/com/stampcrush/backend/fixture/OwnerFixture.java new file mode 100644 index 000000000..4cd1495b4 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/fixture/OwnerFixture.java @@ -0,0 +1,14 @@ +package com.stampcrush.backend.fixture; + +import com.stampcrush.backend.entity.user.Owner; + +public final class OwnerFixture { + public static final Owner OWNER1 = new Owner(1L, "이름", "아이디", "비번", "번호"); + public static final Owner OWNER2 = new Owner(2L, "이름", "아이디", "비번", "번호"); + public static final Owner OWNER3 = new Owner(3L, "jena", "jenaId", "jnpw1234", "01098765432"); + public static final Owner GITCHAN = new Owner(4L, "깃짱", "gitchan", "password", "01011112222"); + public static final Owner JENA = new Owner(5L, "제나", "jena", "password", "0101010"); + + private OwnerFixture() { + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/fixture/RewardFixture.java b/backend/src/test/java/com/stampcrush/backend/fixture/RewardFixture.java new file mode 100644 index 000000000..3828f65a8 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/fixture/RewardFixture.java @@ -0,0 +1,31 @@ +package com.stampcrush.backend.fixture; + +import com.stampcrush.backend.entity.reward.Reward; + +import java.time.LocalDateTime; + +public final class RewardFixture { + + public static final Reward REWARD_USED_FALSE = new Reward( + LocalDateTime.MIN, + LocalDateTime.MAX, + 1L, + "깃짱네 아메리카노", + false, + CustomerFixture.REGISTER_CUSTOMER_GITCHAN, + CafeFixture.GITCHAN_CAFE + ); + + public static final Reward REWARD_USED_TRUE = new Reward( + LocalDateTime.MIN, + LocalDateTime.MAX, + 1L, + "깃짱네 아메리카노", + true, + CustomerFixture.REGISTER_CUSTOMER_GITCHAN, + CafeFixture.GITCHAN_CAFE + ); + + private RewardFixture() { + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/fixture/SampleCouponFixture.java b/backend/src/test/java/com/stampcrush/backend/fixture/SampleCouponFixture.java new file mode 100644 index 000000000..cb970b82d --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/fixture/SampleCouponFixture.java @@ -0,0 +1,32 @@ +package com.stampcrush.backend.fixture; + +import com.stampcrush.backend.entity.sample.SampleBackImage; +import com.stampcrush.backend.entity.sample.SampleFrontImage; +import com.stampcrush.backend.entity.sample.SampleStampCoordinate; +import com.stampcrush.backend.entity.sample.SampleStampImage; + +import java.util.List; + +public final class SampleCouponFixture { + + public static final SampleFrontImage SAMPLE_FRONT_IMAGE = new SampleFrontImage("frontImageUrl"); + public static final SampleFrontImage SAMPLE_FRONT_IMAGE_SAVED = new SampleFrontImage(1L, "frontImageUrl"); + public static final SampleBackImage SAMPLE_BACK_IMAGE = new SampleBackImage("backImageUrl"); + public static final SampleBackImage SAMPLE_BACK_IMAGE_SAVED = new SampleBackImage(1L, "backImageUrl"); + public static final SampleStampImage SAMPLE_STAMP_IMAGE = new SampleStampImage("sampleStampImageUrl"); + public static final SampleStampImage SAMPLE_STAMP_IMAGE_SAVED = new SampleStampImage(1L, "sampleStampImageUrl"); + + public static final List SAMPLE_COORDINATES_SIZE_EIGHT = List.of( + new SampleStampCoordinate(1, 1, 2, SAMPLE_BACK_IMAGE_SAVED), + new SampleStampCoordinate(2, 2, 2, SAMPLE_BACK_IMAGE_SAVED), + new SampleStampCoordinate(3, 3, 2, SAMPLE_BACK_IMAGE_SAVED), + new SampleStampCoordinate(4, 4, 2, SAMPLE_BACK_IMAGE_SAVED), + new SampleStampCoordinate(5, 1, 1, SAMPLE_BACK_IMAGE_SAVED), + new SampleStampCoordinate(6, 2, 1, SAMPLE_BACK_IMAGE_SAVED), + new SampleStampCoordinate(7, 3, 1, SAMPLE_BACK_IMAGE_SAVED), + new SampleStampCoordinate(8, 4, 1, SAMPLE_BACK_IMAGE_SAVED) + ); + + private SampleCouponFixture() { + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/helper/AuthHelper.java b/backend/src/test/java/com/stampcrush/backend/helper/AuthHelper.java new file mode 100644 index 000000000..ba9e8cefa --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/helper/AuthHelper.java @@ -0,0 +1,68 @@ +package com.stampcrush.backend.helper; + +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; + +import java.util.Base64; + +public final class AuthHelper { + + public static OwnerAuthorization createOwnerAuthorization(Owner owner) { + String loginId = owner.getLoginId(); + String encryptedPassword = owner.getEncryptedPassword(); + String basicAuthHeader = generateBasicAuthHeader(loginId, encryptedPassword); + + return new OwnerAuthorization(owner, basicAuthHeader); + } + + public static final class OwnerAuthorization { + + private final Owner owner; + private final String basicAuthHeader; + + private OwnerAuthorization(Owner owner, String basicAuthHeader) { + this.owner = owner; + this.basicAuthHeader = basicAuthHeader; + } + + public Owner getOwner() { + return owner; + } + + public String getBasicAuthHeader() { + return basicAuthHeader; + } + } + + public static CustomerAuthorization createCustomerAuthorization(RegisterCustomer customer) { + String loginId = customer.getLoginId(); + String encryptedPassword = customer.getEncryptedPassword(); + String basicAuthHeader = generateBasicAuthHeader(loginId, encryptedPassword); + + return new CustomerAuthorization(loginId, basicAuthHeader); + } + + public static final class CustomerAuthorization { + + private final String loginId; + + private final String basicAuthHeader; + + private CustomerAuthorization(String loginId, String basicAuthHeader) { + this.loginId = loginId; + this.basicAuthHeader = basicAuthHeader; + } + + public String loginId() { + return loginId; + } + + public String basicAuthHeader() { + return basicAuthHeader; + } + } + + private static String generateBasicAuthHeader(String loginId, String encryptedPassword) { + return "Basic " + Base64.getEncoder().encodeToString((loginId + ":" + encryptedPassword).getBytes()); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/repository/cafe/CafeCouponDesignRepositoryTest.java b/backend/src/test/java/com/stampcrush/backend/repository/cafe/CafeCouponDesignRepositoryTest.java new file mode 100644 index 000000000..5b7a1411b --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/repository/cafe/CafeCouponDesignRepositoryTest.java @@ -0,0 +1,70 @@ +package com.stampcrush.backend.repository.cafe; + +import com.stampcrush.backend.common.KorNamingConverter; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.cafe.CafeCouponDesign; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.fixture.OwnerFixture; +import com.stampcrush.backend.repository.user.OwnerRepository; +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@KorNamingConverter +@DataJpaTest +class CafeCouponDesignRepositoryTest { + + @Autowired + private CafeCouponDesignRepository cafeCouponDesignRepository; + + @Autowired + private CafeRepository cafeRepository; + + @Autowired + private OwnerRepository ownerRepository; + + @Test + void 특정_카페의_디자인_중_삭제되지_않은_데이터만_조회한다() { + // given, when + Cafe savedCafe = createCafe(OwnerFixture.GITCHAN); + + CafeCouponDesign deletedCafeCouponDesign = saveCafeCouponDesign(savedCafe, true); + CafeCouponDesign notDeletedCafeCouponDesign = saveCafeCouponDesign(savedCafe, false); + + Optional filteredCafeCouponDesign = cafeCouponDesignRepository.findByCafe(savedCafe); + + // then + assertAll( + () -> assertThat(filteredCafeCouponDesign).isNotEmpty(), + () -> assertThat(filteredCafeCouponDesign.get()).isEqualTo(notDeletedCafeCouponDesign), + () -> assertThat(filteredCafeCouponDesign.get()).isNotEqualTo(deletedCafeCouponDesign) + ); + } + + private Cafe createCafe(Owner owner) { + Owner savedOwner = ownerRepository.save(owner); + return cafeRepository.save( + new Cafe( + "깃짱카페", + "서초구", + "어쩌고", + "0101010101", + savedOwner + ) + ); + } + + private CafeCouponDesign saveCafeCouponDesign(Cafe savedCafe, boolean deleted) { + return cafeCouponDesignRepository.save( + new CafeCouponDesign( + "#", "#", "#", deleted, savedCafe + ) + ); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/repository/cafe/CafePolicyRepositoryTest.java b/backend/src/test/java/com/stampcrush/backend/repository/cafe/CafePolicyRepositoryTest.java new file mode 100644 index 000000000..73e790592 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/repository/cafe/CafePolicyRepositoryTest.java @@ -0,0 +1,153 @@ +package com.stampcrush.backend.repository.cafe; + +import com.stampcrush.backend.common.KorNamingConverter; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.cafe.CafePolicy; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.repository.user.OwnerRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +import java.time.LocalTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@KorNamingConverter +@EnableJpaAuditing +@DataJpaTest +class CafePolicyRepositoryTest { + + @Autowired + private CafePolicyRepository cafePolicyRepository; + + @Autowired + private CafeRepository cafeRepository; + + @Autowired + private OwnerRepository ownerRepository; + + @Test + void 특정_카페의_정책_중_삭제되지_않은_데이터만_조회한다() { + // given, when + Cafe savedCafe = cafeRepository.save( + new Cafe( + "깃짱카페", + LocalTime.NOON, + LocalTime.MIDNIGHT, + "01012345678", + "#", + "안녕하세요", + "서울시 올림픽로 어쩌고", + "루터회관", + "10-222-333", + ownerRepository.save(new Owner("이름", "아이디", "pw", "phone")) + ) + ); + + CafePolicy deletedCafePolicy = cafePolicyRepository.save( + new CafePolicy( + 2, + "마카롱", + 12, + true, + savedCafe + ) + ); + + CafePolicy notDeletedCafePolicy = cafePolicyRepository.save( + new CafePolicy( + 2, + "아메리카노", + 12, + false, + savedCafe + ) + ); + + Optional filteredCafePolicy = cafePolicyRepository.findByCafe(savedCafe); + + // then + assertAll( + () -> assertThat(filteredCafePolicy).isNotEmpty(), + () -> assertThat(filteredCafePolicy.get()).isEqualTo(notDeletedCafePolicy), + () -> assertThat(filteredCafePolicy.get()).isNotEqualTo(deletedCafePolicy) + ); + } + + @Test + void 특정_시간보다_이후에_생성된_카페_리워드_정책이_있으면_해당_정책들을_반환한다() { + Cafe savedCafe = cafeRepository.save( + new Cafe( + "깃짱카페", + LocalTime.NOON, + LocalTime.MIDNIGHT, + "01012345678", + "#", + "안녕하세요", + "서울시 올림픽로 어쩌고", + "루터회관", + "10-222-333", + ownerRepository.save(new Owner("이름", "아이디", "pw", "phone")) + ) + ); + + CafePolicy oldCafePolicy = cafePolicyRepository.save( + new CafePolicy( + 2, + "마카롱", + 12, + true, + savedCafe + ) + ); + + CafePolicy newCafePolicy = cafePolicyRepository.save( + new CafePolicy( + 2, + "아메리카노", + 12, + false, + savedCafe + ) + ); + + List cafePolicies = cafePolicyRepository.findByCafeAndCreatedAtGreaterThan(savedCafe, oldCafePolicy.getCreatedAt()); + assertThat(cafePolicies).isNotEmpty(); + } + + @Test + void 특정_시간보다_이후에_생성된_카페_리워드_정책이_없으면_빈_리스트_반환() { + Cafe savedCafe = cafeRepository.save( + new Cafe( + "깃짱카페", + LocalTime.NOON, + LocalTime.MIDNIGHT, + "01012345678", + "#", + "안녕하세요", + "서울시 올림픽로 어쩌고", + "루터회관", + "10-222-333", + ownerRepository.save(new Owner("이름", "아이디", "pw", "phone")) + ) + ); + + CafePolicy currentPolicy = cafePolicyRepository.save( + new CafePolicy( + 2, + "마카롱", + 12, + true, + savedCafe + ) + ); + + List cafePolicies = cafePolicyRepository.findByCafeAndCreatedAtGreaterThan(savedCafe, currentPolicy.getCreatedAt()); + assertThat(cafePolicies).isEmpty(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/repository/coupon/CouponRepositoryTest.java b/backend/src/test/java/com/stampcrush/backend/repository/coupon/CouponRepositoryTest.java new file mode 100644 index 000000000..bbc8e6001 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/repository/coupon/CouponRepositoryTest.java @@ -0,0 +1,218 @@ +package com.stampcrush.backend.repository.coupon; + +import com.stampcrush.backend.common.KorNamingConverter; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.coupon.*; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.entity.user.TemporaryCustomer; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.user.OwnerRepository; +import com.stampcrush.backend.repository.user.RegisterCustomerRepository; +import com.stampcrush.backend.repository.user.TemporaryCustomerRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@KorNamingConverter +@DataJpaTest +class CouponRepositoryTest { + + @Autowired + private CouponRepository couponRepository; + + @Autowired + private TemporaryCustomerRepository temporaryCustomerRepository; + + @Autowired + private RegisterCustomerRepository registerCustomerRepository; + + @Autowired + private CafeRepository cafeRepository; + + @Autowired + private CouponDesignRepository couponDesignRepository; + + @Autowired + private CouponPolicyRepository couponPolicyRepository; + + @Autowired + private OwnerRepository ownerRepository; + + private TemporaryCustomer tmpCustomer1; + private TemporaryCustomer tmpCustomer2; + private TemporaryCustomer tmpCustomer3; + + private RegisterCustomer registerCustomer1; + private RegisterCustomer registerCustomer2; + + private Cafe cafe1; + private Cafe cafe2; + + private CouponDesign couponDesign1; + private CouponDesign couponDesign2; + private CouponDesign couponDesign3; + private CouponDesign couponDesign4; + private CouponDesign couponDesign5; + + private CouponPolicy couponPolicy1; + private CouponPolicy couponPolicy2; + private CouponPolicy couponPolicy3; + private CouponPolicy couponPolicy4; + private CouponPolicy couponPolicy5; + + // coupon1, REWARD상태, cafe1 -> stamp2개 + // coupon2, USING상태, cafe1 -> stamp1개 + // coupon3, USING상태, cafe2 -> stamp1개 + // coupon4, USING상태, cafe2 -> stamp0개 + // coupon5, USING상태, cafe2 -> stamp0개 + private Coupon coupon1; + private Coupon coupon2; + private Coupon coupon3; + private Coupon coupon4; + private Coupon coupon5; + + @BeforeEach + void setUp() { + tmpCustomer1 = temporaryCustomerRepository.save(TemporaryCustomer.from("깃짱 번호1")); + tmpCustomer2 = temporaryCustomerRepository.save(TemporaryCustomer.from("깃짱 번호2")); + tmpCustomer3 = temporaryCustomerRepository.save(TemporaryCustomer.from("깃짱 번호3")); + + registerCustomer1 = registerCustomerRepository.save(new RegisterCustomer("깃짱 닉네임", "깃짱 번호4", "깃짱 아이디", "깃짱 비번")); + registerCustomer2 = registerCustomerRepository.save(new RegisterCustomer("깃짱 닉네임", "깃짱 번호5", "깃짱 아이디", "깃짱 비번")); + + cafe1 = cafeRepository.save(new Cafe( + "하디까페", + LocalTime.of(12, 30), + LocalTime.of(18, 30), + "0211111111", + "http://www.cafeImage.com", + "안녕하세요", + "잠실동12길", + "14층", + "11111111", + ownerRepository.save(new Owner("이름", "아이디", "비번", "번호")))); + cafe2 = cafeRepository.save(new Cafe( + "하디까페", + LocalTime.of(12, 30), + LocalTime.of(18, 30), + "0211111111", + "http://www.cafeImage.com", + "안녕하세요", + "잠실동12길", + "14층", + "11111111", + ownerRepository.save(new Owner("이름", "아이디", "비번", "번호")))); + + couponDesign1 = couponDesignRepository.save(new CouponDesign("front", "back", "stamp")); + couponDesign2 = couponDesignRepository.save(new CouponDesign("front", "back", "stamp")); + couponDesign3 = couponDesignRepository.save(new CouponDesign("front", "back", "stamp")); + couponDesign4 = couponDesignRepository.save(new CouponDesign("front", "back", "stamp")); + couponDesign5 = couponDesignRepository.save(new CouponDesign("front", "back", "stamp")); + + couponPolicy1 = couponPolicyRepository.save(new CouponPolicy(10, "아메리카노", 8)); + couponPolicy2 = couponPolicyRepository.save(new CouponPolicy(10, "아메리카노", 8)); + couponPolicy3 = couponPolicyRepository.save(new CouponPolicy(10, "아메리카노", 8)); + couponPolicy4 = couponPolicyRepository.save(new CouponPolicy(10, "아메리카노", 8)); + couponPolicy5 = couponPolicyRepository.save(new CouponPolicy(10, "아메리카노", 8)); + + coupon1 = new Coupon(LocalDateTime.now(), LocalDateTime.now(), LocalDate.EPOCH, tmpCustomer1, cafe1, couponDesign1, couponPolicy1); + Stamp stamp1 = new Stamp(); + Stamp stamp2 = new Stamp(); + stamp1.registerCoupon(coupon1); + stamp2.registerCoupon(coupon1); + Coupon save = couponRepository.save(coupon1); + save.reward(); + + coupon2 = new Coupon(LocalDate.EPOCH, registerCustomer1, cafe1, couponDesign2, couponPolicy2); + Stamp stamp3 = new Stamp(); + stamp3.registerCoupon(coupon2); + couponRepository.save(coupon2); + + coupon3 = new Coupon(LocalDate.EPOCH, tmpCustomer2, cafe2, couponDesign3, couponPolicy3); + Stamp stamp4 = new Stamp(); + stamp4.registerCoupon(coupon3); + couponRepository.save(coupon3); + + coupon4 = new Coupon(LocalDate.EPOCH, tmpCustomer3, cafe2, couponDesign4, couponPolicy4); + couponRepository.save(coupon4); + + coupon5 = new Coupon(LocalDate.EPOCH, registerCustomer2, cafe2, couponDesign5, couponPolicy5); + couponRepository.save(coupon5); + } + + @Test + void 카페에_맞는_쿠폰_정보를_조회한다() { + // when + List couponsCafe1 = couponRepository.findByCafe(cafe1); + List couponsCafe2 = couponRepository.findByCafe(cafe2); + + //then + assertAll( + () -> assertThat(couponsCafe1.size()).isEqualTo(2), + () -> assertThat(couponsCafe1).containsExactlyInAnyOrder(coupon1, coupon2), + () -> assertThat(couponsCafe1.get(0).getStamps().size()).isEqualTo(2), + () -> assertThat(couponsCafe1).doesNotContain(coupon3, coupon4, coupon5), + () -> assertThat(couponsCafe2.size()).isEqualTo(3), + () -> assertThat(couponsCafe2).containsExactlyInAnyOrder(coupon3, coupon4, coupon5), + () -> assertThat(couponsCafe2).doesNotContain(coupon1, coupon2) + ); + } + + @Test + void 쿠폰X_카페에_스탬프를_적립하고_있는_고객의_쿠폰을_조회한다() { + // then + List customerCoupon = couponRepository.findByCafeAndCustomerAndStatus(cafe1, tmpCustomer1, CouponStatus.ACCUMULATING); + + assertThat(customerCoupon).isEmpty(); + } + + @Test + void 쿠폰O_카페에_스탬프를_적립하고_있는_고객의_쿠폰을_조회한다() { + // when + List customerCoupon = couponRepository.findByCafeAndCustomerAndStatus(cafe2, tmpCustomer3, CouponStatus.ACCUMULATING); + // then + assertThat(customerCoupon).containsExactly(coupon4); + } + + @Test + void 쿠폰의_유효기간_만료일을_계산한다() { + // given, when + LocalDateTime expiredDate = coupon1.calculateExpireDate(); + LocalDateTime expected = coupon1.getCreatedAt().plusMonths(couponPolicy1.getExpiredPeriod()); + + // then + assertThat(expiredDate).isEqualTo(expected); + } + + @Test + @Disabled + // TODO: 영호씨 이거 갑자기 깨지는데 확인좀 해주세요. + void 쿠폰들의_createdAt을_비교한다() { + // given, when + LocalDateTime visitTime = coupon1.compareCreatedAtAndReturnEarlier(coupon5.getCreatedAt()); + + // then + assertThat(visitTime).isEqualTo(coupon1.getCreatedAt()); + } + + @Test + void 쿠폰의_아이디와_고객의_아이디로_쿠폰을_조회한다() { + // given, when + Optional coupon = couponRepository.findByIdAndCustomerId(coupon1.getId(), tmpCustomer1.getId()); + + // then + assertThat(coupon).isPresent(); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/repository/coupon/CouponRepositoryTest2.java b/backend/src/test/java/com/stampcrush/backend/repository/coupon/CouponRepositoryTest2.java new file mode 100644 index 000000000..975f0b120 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/repository/coupon/CouponRepositoryTest2.java @@ -0,0 +1,222 @@ +package com.stampcrush.backend.repository.coupon; + +import com.stampcrush.backend.common.KorNamingConverter; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.coupon.*; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.fixture.CouponDesignFixture; +import com.stampcrush.backend.fixture.CouponPolicyFixture; +import com.stampcrush.backend.fixture.CustomerFixture; +import com.stampcrush.backend.fixture.OwnerFixture; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.user.CustomerRepository; +import com.stampcrush.backend.repository.user.OwnerRepository; +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@KorNamingConverter +@DataJpaTest +class CouponRepositoryTest2 { + + @Autowired + private EntityManager em; + + @Autowired + private CouponRepository couponRepository; + + @Autowired + private OwnerRepository ownerRepository; + + @Autowired + private CafeRepository cafeRepository; + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private CouponDesignRepository couponDesignRepository; + + @Autowired + private CouponPolicyRepository couponPolicyRepository; + + @Autowired + private CouponStampCoordinateRepository couponStampCoordinateRepository; + + @Test + @Disabled + void 쿠폰_디자인에_읽기_전용인_좌표를_조회한다() { + // given, when + Cafe gitchanCafe = createCafe(OwnerFixture.GITCHAN); + + CouponDesign couponDesign = CouponDesignFixture.COUPON_DESIGN_1; + CouponStampCoordinate coordinates = new CouponStampCoordinate(1, 1, 1, couponDesign); + addCouponStampCoordinate(List.of(coordinates), couponDesign); + + RegisterCustomer savedCustomer = customerRepository.save(CustomerFixture.REGISTER_CUSTOMER_GITCHAN); + Coupon gitchanCafeCoupon = saveCoupon(gitchanCafe, savedCustomer, couponDesignRepository.save(couponDesign), couponPolicyRepository.save(CouponPolicyFixture.COUPON_POLICY_1)); + + em.flush(); + em.clear(); + + Coupon findCoupon = couponRepository.findById(gitchanCafeCoupon.getId()).get(); + List couponStampCoordinates = findCoupon.getCouponDesign().getCouponStampCoordinates(); + + // then + assertAll( + () -> assertThat(couponStampCoordinates).isNotEmpty(), + () -> assertThat(couponStampCoordinates).containsExactlyInAnyOrder(coordinates) + ); + } + + private void addCouponStampCoordinate(List coordinates, CouponDesign couponDesign) { + for (CouponStampCoordinate coordinate : coordinates) { + couponDesign.addCouponStampCoordinate(couponStampCoordinateRepository.save(coordinate)); + } + } + + @Test + void 쿠폰이_참조하는_카페를_찾을_수_있다() { + // given, when + Cafe gitchanCafe = createCafe(OwnerFixture.GITCHAN); + RegisterCustomer savedCustomer = customerRepository.save(CustomerFixture.REGISTER_CUSTOMER_GITCHAN); + Coupon gitchanCafeCoupon = saveCoupon(gitchanCafe, savedCustomer, couponDesignRepository.save(CouponDesignFixture.COUPON_DESIGN_1), couponPolicyRepository.save(CouponPolicyFixture.COUPON_POLICY_1)); + Cafe cafe = gitchanCafeCoupon.getCafe(); + + // then + assertAll( + () -> assertThat(cafe).isNotNull(), + () -> assertThat(cafe.getId()).isEqualTo(gitchanCafe.getId()), + () -> assertThat(cafe.getName()).isEqualTo(gitchanCafe.getName()) + ); + } + + @Test + void 쿠폰이_참조하는_카페정책을_찾을_수_있다() { + // given, when + Cafe gitchanCafe = createCafe(OwnerFixture.GITCHAN); + RegisterCustomer savedCustomer = customerRepository.save(CustomerFixture.REGISTER_CUSTOMER_GITCHAN); + Coupon gitchanCafeCoupon = saveCoupon(gitchanCafe, savedCustomer, couponDesignRepository.save(CouponDesignFixture.COUPON_DESIGN_1), couponPolicyRepository.save(CouponPolicyFixture.COUPON_POLICY_1)); + CouponPolicy couponPolicy = gitchanCafeCoupon.getCouponPolicy(); + + // then + assertAll( + () -> assertThat(couponPolicy).isNotNull(), + () -> assertThat(couponPolicy).isEqualTo(gitchanCafeCoupon.getCouponPolicy()) + ); + } + + @Test + void 쿠폰이_참조하는_쿠폰디자인을_찾을_수_있다() { + Cafe gitchanCafe = createCafe(OwnerFixture.GITCHAN); + RegisterCustomer savedCustomer = customerRepository.save(CustomerFixture.REGISTER_CUSTOMER_GITCHAN); + Coupon gitchanCafeCoupon = saveCoupon(gitchanCafe, savedCustomer, couponDesignRepository.save(CouponDesignFixture.COUPON_DESIGN_1), couponPolicyRepository.save(CouponPolicyFixture.COUPON_POLICY_1)); + CouponDesign couponDesign = gitchanCafeCoupon.getCouponDesign(); + + // then + assertAll( + () -> assertThat(couponDesign).isNotNull(), + () -> assertThat(couponDesign).isEqualTo(gitchanCafeCoupon.getCouponDesign()) + ); + } + + @Test + void 쿠폰에_적립된_스탬프의_개수를_찾을_수_있다() { + // given, when + Cafe gitchanCafe = createCafe(OwnerFixture.GITCHAN); + RegisterCustomer savedCustomer = customerRepository.save(CustomerFixture.REGISTER_CUSTOMER_GITCHAN); + Coupon gitchanCafeCoupon = saveCoupon(gitchanCafe, savedCustomer, couponDesignRepository.save(CouponDesignFixture.COUPON_DESIGN_1), couponPolicyRepository.save(CouponPolicyFixture.COUPON_POLICY_1)); + + int stampCount = 4; + for (int i = 0; i < stampCount; i++) { + gitchanCafeCoupon.accumulate(1); + } + + em.flush(); + em.clear(); + + Coupon findCoupon = couponRepository.findById(gitchanCafeCoupon.getId()).get(); + + // then + assertThat(findCoupon.getStampCount()).isEqualTo(stampCount); + } + + @Test + void 쿠폰을_고객으로_필터링하고_그중_ACCUMULATING_상태인_쿠폰만_필터링해_조회한다() { + // given + Cafe gitchanCafe = createCafe(OwnerFixture.GITCHAN); + Cafe jenaCafe = createCafe(OwnerFixture.JENA); + + RegisterCustomer savedCustomer = customerRepository.save(CustomerFixture.REGISTER_CUSTOMER_GITCHAN); + + Coupon gitchanCafeCoupon = saveCoupon(gitchanCafe, savedCustomer, couponDesignRepository.save(CouponDesignFixture.COUPON_DESIGN_1), couponPolicyRepository.save(CouponPolicyFixture.COUPON_POLICY_1)); + Coupon jenaCafeCoupon = saveCoupon(jenaCafe, savedCustomer, couponDesignRepository.save(CouponDesignFixture.COUPON_DESIGN_2), couponPolicyRepository.save(CouponPolicyFixture.COUPON_POLICY_2)); + + // when + List findCoupon = couponRepository.findFilteredAndSortedCoupons(savedCustomer, CouponStatus.ACCUMULATING); + + // then + assertThat(findCoupon).containsExactlyInAnyOrder(gitchanCafeCoupon, jenaCafeCoupon); + } + + @Test + void 쿠폰을_고객으로_필터링하고_그중_ACCUMULATING_상태인_쿠폰만_필터링해_조회한다_2() { + // given + Cafe gitchanCafe = createCafe(OwnerFixture.GITCHAN); + Cafe jenaCafe = createCafe(OwnerFixture.JENA); + + RegisterCustomer savedCustomer = customerRepository.save(CustomerFixture.REGISTER_CUSTOMER_GITCHAN); + + CouponPolicy gitchanCafeCouponPolicy = CouponPolicyFixture.COUPON_POLICY_1; + + Coupon gitchanCafeCoupon = saveCoupon(gitchanCafe, savedCustomer, couponDesignRepository.save(CouponDesignFixture.COUPON_DESIGN_1), couponPolicyRepository.save(gitchanCafeCouponPolicy)); + Coupon jenaCafeCoupon = saveCoupon(jenaCafe, savedCustomer, couponDesignRepository.save(CouponDesignFixture.COUPON_DESIGN_2), couponPolicyRepository.save(CouponPolicyFixture.COUPON_POLICY_2)); + changeCafeCouponStatusToRewarded(gitchanCafeCoupon, gitchanCafeCouponPolicy.getMaxStampCount()); + + // when + List findCoupon = couponRepository.findFilteredAndSortedCoupons(savedCustomer, CouponStatus.ACCUMULATING); + + // then + assertThat(findCoupon).containsOnly(jenaCafeCoupon); + } + + private Cafe createCafe(Owner owner) { + Owner savedOwner = ownerRepository.save(owner); + return cafeRepository.save( + new Cafe( + "깃짱카페", + "서초구", + "어쩌고", + "0101010101", + savedOwner + ) + ); + } + + private Coupon saveCoupon(Cafe savedCafe, RegisterCustomer savedCustomer, CouponDesign savedCouponDesign, CouponPolicy savedCouponPolicy) { + return couponRepository.save( + new Coupon( + LocalDate.MAX, + savedCustomer, + savedCafe, + savedCouponDesign, + savedCouponPolicy + ) + ); + } + + private void changeCafeCouponStatusToRewarded(Coupon gitchanCafeCoupon, Integer maxStampCount) { + for (int i = 0; i < maxStampCount; i++) { + gitchanCafeCoupon.accumulate(1); + } + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/repository/coupon/CouponStampCoordinateRepositoryTest.java b/backend/src/test/java/com/stampcrush/backend/repository/coupon/CouponStampCoordinateRepositoryTest.java new file mode 100644 index 000000000..fe5a465a1 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/repository/coupon/CouponStampCoordinateRepositoryTest.java @@ -0,0 +1,43 @@ +package com.stampcrush.backend.repository.coupon; + +import com.stampcrush.backend.common.KorNamingConverter; +import com.stampcrush.backend.entity.coupon.CouponDesign; +import com.stampcrush.backend.entity.coupon.CouponStampCoordinate; +import com.stampcrush.backend.fixture.CouponDesignFixture; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@KorNamingConverter +@DataJpaTest +class CouponStampCoordinateRepositoryTest { + + @Autowired + private CouponStampCoordinateRepository couponStampCoordinateRepository; + + @Autowired + private CouponDesignRepository couponDesignRepository; + + @Test + void 쿠폰의_좌표를_쿠폰_디자인으로_필터링해서_조회한다() { + // given, when + CouponDesign couponDesign1 = couponDesignRepository.save(CouponDesignFixture.COUPON_DESIGN_1); + CouponDesign couponDesign2 = couponDesignRepository.save(CouponDesignFixture.COUPON_DESIGN_2); + + CouponStampCoordinate couponStampCoordinateOfCouponDesign1 = couponStampCoordinateRepository.save(new CouponStampCoordinate(1, 1, 1, couponDesign1)); + + List coordinatesOfCouponDesign1 = couponStampCoordinateRepository.findByCouponDesign(couponDesign1); + List coordinatesOfCouponDesign2 = couponStampCoordinateRepository.findByCouponDesign(couponDesign2); + + assertAll( + () -> assertThat(coordinatesOfCouponDesign1).isNotEmpty(), + () -> assertThat(coordinatesOfCouponDesign1).containsExactlyInAnyOrder(couponStampCoordinateOfCouponDesign1), + () -> assertThat(coordinatesOfCouponDesign2).isEmpty() + ); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/repository/reward/RewardRepositoryTest.java b/backend/src/test/java/com/stampcrush/backend/repository/reward/RewardRepositoryTest.java new file mode 100644 index 000000000..74847c06a --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/repository/reward/RewardRepositoryTest.java @@ -0,0 +1,96 @@ +package com.stampcrush.backend.repository.reward; + +import com.stampcrush.backend.common.KorNamingConverter; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.reward.Reward; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.fixture.CustomerFixture; +import com.stampcrush.backend.fixture.OwnerFixture; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.user.CustomerRepository; +import com.stampcrush.backend.repository.user.OwnerRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@KorNamingConverter +@DataJpaTest +class RewardRepositoryTest { + + @Autowired + private RewardRepository rewardRepository; + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private CafeRepository cafeRepository; + + @Autowired + private OwnerRepository ownerRepository; + + @Test + void 해당_고객의_사용_가능한_리워드를_조회한다() { + Cafe cafe = createCafe(OwnerFixture.GITCHAN); + Customer customer = customerRepository.save(CustomerFixture.REGISTER_CUSTOMER_GITCHAN); + + Reward reward = new Reward("깃짱카페의 아메리카노", customer, cafe); + Reward savedReward = rewardRepository.save(reward); + + List findRewards = rewardRepository.findAllByCustomerAndUsed(customer, false); + + assertThat(findRewards).containsOnly(savedReward); + } + + @Test + void 해당_고객의_사용_완료한_리워드를_조회한다() { + Cafe cafe = createCafe(OwnerFixture.GITCHAN); + Customer customer = customerRepository.save(CustomerFixture.REGISTER_CUSTOMER_GITCHAN); + + Reward reward = new Reward("깃짱카페의 아메리카노", customer, cafe); + Reward savedReward = rewardRepository.save(reward); + savedReward.useReward(customer, cafe); + + List findRewards = rewardRepository.findAllByCustomerAndUsed(customer, false); + + assertThat(findRewards).isEmpty(); + } + + @Test + void 해당_고객의_사용_가능한_리워드의_개수를_조회한다() { + // given + Cafe cafe = createCafe(OwnerFixture.GITCHAN); + Customer customer = customerRepository.save(CustomerFixture.REGISTER_CUSTOMER_GITCHAN); + + Reward reward1 = rewardRepository.save(new Reward("Reward1", customer, cafe)); + rewardRepository.save(new Reward("Reward2", customer, cafe)); + rewardRepository.save(new Reward("Reward3", customer, cafe)); + rewardRepository.save(new Reward("Reward4", customer, cafe)); + rewardRepository.save(new Reward("Reward5", customer, cafe)); + reward1.useReward(customer, cafe); + + // when + Long countOfUnusedReward = rewardRepository.countByCafeAndCustomerAndUsed(cafe, customer, Boolean.FALSE); + + // then + assertThat(countOfUnusedReward).isEqualTo(4); + } + + private Cafe createCafe(Owner owner) { + Owner savedOwner = ownerRepository.save(owner); + return cafeRepository.save( + new Cafe( + "깃짱카페", + "서초구", + "어쩌고", + "0101010101", + savedOwner + ) + ); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/repository/user/CustomerRepositoryTest.java b/backend/src/test/java/com/stampcrush/backend/repository/user/CustomerRepositoryTest.java new file mode 100644 index 000000000..c23f5fc96 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/repository/user/CustomerRepositoryTest.java @@ -0,0 +1,61 @@ +package com.stampcrush.backend.repository.user; + +import com.stampcrush.backend.common.KorNamingConverter; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.entity.user.TemporaryCustomer; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.dao.DataIntegrityViolationException; + +import java.util.List; + +import static com.stampcrush.backend.fixture.CustomerFixture.REGISTER_CUSTOMER_GITCHAN; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@KorNamingConverter +@DataJpaTest +class CustomerRepositoryTest { + + @Autowired + private CustomerRepository customerRepository; + + @Test + void 주어진_전화번호에_해당하는_고객을_조회한다() { + // given + String phoneNumber = "01012345678"; + Customer customer = TemporaryCustomer.from(phoneNumber); + Customer savedCustomer = customerRepository.save(customer); + + // when + List customers = customerRepository.findByPhoneNumber(phoneNumber); + + // then + assertThat(customers.get(0)).isEqualTo(savedCustomer); + } + + @Test + void 존재하지_않는_전화번호로_고객을_조회하면_빈_리스트를_반환한다() { + // given + String notExistPhoneNumber = "01012345678"; + + // when + List customers = customerRepository.findByPhoneNumber(notExistPhoneNumber); + + // then + assertThat(customers.isEmpty()).isTrue(); + } + + @Test + void 중복되는_전화번호를_등록하면_예외가_발생한다() { + RegisterCustomer customer = REGISTER_CUSTOMER_GITCHAN; + RegisterCustomer savedCustomer = customerRepository.save(customer); + String phoneNumber = savedCustomer.getPhoneNumber(); + + RegisterCustomer duplicatePhoneNumberCustomer = new RegisterCustomer("전화번호_중복쟁이", phoneNumber, "loginId", "password"); + assertThatThrownBy(() -> customerRepository.save(duplicatePhoneNumberCustomer)) + .isInstanceOf(DataIntegrityViolationException.class); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/repository/user/OwnerRepositoryTest.java b/backend/src/test/java/com/stampcrush/backend/repository/user/OwnerRepositoryTest.java new file mode 100644 index 000000000..4af2f4164 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/repository/user/OwnerRepositoryTest.java @@ -0,0 +1,51 @@ +package com.stampcrush.backend.repository.user; + +import com.stampcrush.backend.auth.OAuthProvider; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.fixture.OwnerFixture; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +class OwnerRepositoryTest { + + @Autowired + private OwnerRepository ownerRepository; + + @Test + void Owner를_닉네임으로_조회한다() { + // given + Owner owner = OwnerFixture.GITCHAN; + Owner savedOwner = ownerRepository.save(owner); + + // when + Owner findOwner = ownerRepository.findByNickname(savedOwner.getNickname()).get(); + + // then + assertThat(savedOwner).isEqualTo(findOwner); + } + + @Test + void Owner를_OAuthProvider와_OAuthId로_조회한다() { + // given + long oAuthId = 123L; + OAuthProvider oauthProvider = OAuthProvider.KAKAO; + + Owner owner = Owner.builder() + .nickname("제나") + .email("yenawee@naver.com") + .oAuthId(oAuthId) + .oAuthProvider(oauthProvider) + .build(); + Owner savedOwner = ownerRepository.save(owner); + + // when + Owner findOwner = ownerRepository.findByOAuthProviderAndOAuthId(oauthProvider, oAuthId).get(); + + // then + assertThat(savedOwner).isEqualTo(findOwner); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/repository/user/RegisterCustomerRepositoryTest.java b/backend/src/test/java/com/stampcrush/backend/repository/user/RegisterCustomerRepositoryTest.java new file mode 100644 index 000000000..a46fea933 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/repository/user/RegisterCustomerRepositoryTest.java @@ -0,0 +1,65 @@ +package com.stampcrush.backend.repository.user; + +import com.stampcrush.backend.auth.OAuthProvider; +import com.stampcrush.backend.common.KorNamingConverter; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@KorNamingConverter +@DataJpaTest +class RegisterCustomerRepositoryTest { + + @Autowired + private RegisterCustomerRepository registerCustomerRepository; + + @Test + void 주어진_LoginId에_해당하는_고객을_조회한다() { + // given + String loginId = "jenaId"; + RegisterCustomer registerCustomer = new RegisterCustomer("jena", "01012345678", loginId, "jenapw"); + RegisterCustomer savedCustomer = registerCustomerRepository.save(registerCustomer); + + // when + Optional findCustomer = registerCustomerRepository.findByLoginId(loginId); + + // then + assertThat(findCustomer.get()).isEqualTo(savedCustomer); + } + + @Test + void 존재하지_않는_LoginId로_고객을_조회하면_빈_Optional을_반환한다() { + // given + String notExistLoginId = "notExist"; + + // when + Optional findCustomer = registerCustomerRepository.findByLoginId(notExistLoginId); + + // then + assertThat(findCustomer).isEmpty(); + } + + @Test + void OAuthProvider와_OAuthId로_고객을_조회한다() { + long oAuthId = 123L; + OAuthProvider oauthProvider = OAuthProvider.KAKAO; + + RegisterCustomer customer = RegisterCustomer.builder() + .nickname("제나") + .email("yenawee@naver.com") + .oAuthId(oAuthId) + .oAuthProvider(oauthProvider) + .build(); + + Customer savedCustomer = registerCustomerRepository.save(customer); + RegisterCustomer findCustomer = registerCustomerRepository.findByOAuthProviderAndOAuthId(oauthProvider, oAuthId).get(); + + assertThat(savedCustomer).isEqualTo(findCustomer); + } +} diff --git a/backend/src/test/java/com/stampcrush/backend/repository/visithistory/VisitHistoryRepositoryTest.java b/backend/src/test/java/com/stampcrush/backend/repository/visithistory/VisitHistoryRepositoryTest.java new file mode 100644 index 000000000..3b2a36395 --- /dev/null +++ b/backend/src/test/java/com/stampcrush/backend/repository/visithistory/VisitHistoryRepositoryTest.java @@ -0,0 +1,119 @@ +package com.stampcrush.backend.repository.visithistory; + +import com.stampcrush.backend.common.KorNamingConverter; +import com.stampcrush.backend.entity.cafe.Cafe; +import com.stampcrush.backend.entity.user.Customer; +import com.stampcrush.backend.entity.user.Owner; +import com.stampcrush.backend.entity.user.RegisterCustomer; +import com.stampcrush.backend.entity.user.TemporaryCustomer; +import com.stampcrush.backend.entity.visithistory.VisitHistory; +import com.stampcrush.backend.repository.cafe.CafeRepository; +import com.stampcrush.backend.repository.user.OwnerRepository; +import com.stampcrush.backend.repository.user.RegisterCustomerRepository; +import com.stampcrush.backend.repository.user.TemporaryCustomerRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.time.LocalTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@KorNamingConverter +@DataJpaTest +class VisitHistoryRepositoryTest { + + @Autowired + private VisitHistoryRepository visitHistoryRepository; + + @Autowired + private RegisterCustomerRepository registerCustomerRepository; + + @Autowired + private TemporaryCustomerRepository temporaryCustomerRepository; + + @Autowired + private OwnerRepository ownerRepository; + + @Autowired + private CafeRepository cafeRepository; + + private TemporaryCustomer temporaryCustomer1; + private TemporaryCustomer temporaryCustomer2; + + @BeforeEach + void setUp() { + temporaryCustomer1 = temporaryCustomerRepository.save(TemporaryCustomer.from("1234")); + temporaryCustomer2 = temporaryCustomerRepository.save(TemporaryCustomer.from("5678")); + } + + @Test + void 고객별_스탬프_적립_내역을_조회한다() { + // given + VisitHistory visitHistory1 = new VisitHistory(null, temporaryCustomer1, 3); + VisitHistory visitHistory2 = new VisitHistory(null, temporaryCustomer1, 2); + VisitHistory visitHistory3 = new VisitHistory(null, temporaryCustomer2, 6); + + // when + visitHistoryRepository.save(visitHistory1); + visitHistoryRepository.save(visitHistory2); + visitHistoryRepository.save(visitHistory3); + List visitHistoriesByCustomer = visitHistoryRepository.findVisitHistoriesByCustomer(temporaryCustomer1); + + // then + assertThat(visitHistoriesByCustomer).hasSize(2); + } + + @Test + void 특정_카페에_대한_특정_고객의_방문_이력을_조회한다() { + // given + Customer customer1 = registerCustomerRepository.save(new RegisterCustomer("jena", "01012345678", "jenaId", "jenaPw")); + TemporaryCustomer customer2 = temporaryCustomerRepository.save(TemporaryCustomer.from("010000011111")); + + Owner cafe1Owner = ownerRepository.save(new Owner("owner1", "owner1Id", "owner1Pw", "01076532456")); + Cafe cafe1 = cafeRepository.save(new Cafe("우아한카페", + LocalTime.NOON, + LocalTime.MIDNIGHT, + "01012345678", + "cafeImageUrl", + "introduction", + "roadAddress", + "detailAddress", + "buisnessRegistrationNumber", + cafe1Owner + )); + + Owner cafe2Owner = ownerRepository.save(new Owner("owner2", "owner2Id", "owner2Pw", "01084735242")); + Cafe cafe2 = cafeRepository.save(new Cafe( + "안우아한카페", + LocalTime.NOON, + LocalTime.MIDNIGHT, + "01087352412", + "cafeImageUrl", + "introduction", + "roadAddress", + "detailAddress", + "buisnessRegistrationNumber", + cafe2Owner + )); + + VisitHistory cafe1Customer1History1 = visitHistoryRepository.save(new VisitHistory(cafe1, customer1, 3)); + VisitHistory cafe1Customer1History2 = visitHistoryRepository.save(new VisitHistory(cafe1, customer1, 5)); + VisitHistory cafe2Customer1History = visitHistoryRepository.save(new VisitHistory(cafe2, customer1, 1)); + VisitHistory cafe1Customer2History = visitHistoryRepository.save(new VisitHistory(cafe1, customer2, 2)); + VisitHistory cafe2Customer2History = visitHistoryRepository.save(new VisitHistory(cafe2, customer2, 7)); + + // when + List expected1 = List.of(cafe1Customer1History1, cafe1Customer1History2); + List expected2 = List.of(cafe2Customer2History); + + List customer1Cafe1VisitHistory = visitHistoryRepository.findByCafeAndCustomer(cafe1, customer1); + List customer2Cafe2VisitHistory = visitHistoryRepository.findByCafeAndCustomer(cafe2, customer2); + + // then + assertThat(customer1Cafe1VisitHistory).usingRecursiveComparison().isEqualTo(expected1); + assertThat(customer2Cafe2VisitHistory).usingRecursiveComparison().isEqualTo(expected2); + } +} diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml new file mode 100644 index 000000000..03c30d374 --- /dev/null +++ b/backend/src/test/resources/application.yml @@ -0,0 +1,3 @@ +spring: + profiles: + active: test diff --git a/frontend/.babelrc.json b/frontend/.babelrc.json new file mode 100644 index 000000000..b5cf683b7 --- /dev/null +++ b/frontend/.babelrc.json @@ -0,0 +1,16 @@ +{ + "sourceType": "unambiguous", + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "chrome": 100 + } + } + ], + "@babel/preset-typescript", + "@babel/preset-react" + ], + "plugins": [] +} \ No newline at end of file diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 000000000..97c21e368 --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,26 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:storybook/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": ["@typescript-eslint", "react", "react-hooks"], + "rules": { + "react/react-in-jsx-scope": "off", + "react/prop-types": "off", + "linebreak-style": ["error", "unix"], + "quotes": ["error", "single"], + "semi": ["error", "always"] + } +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 000000000..c10423514 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,5 @@ +node_modules +yarn.lock +dist/ +.DS_Store +.env \ No newline at end of file diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 000000000..1b4488675 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,6 @@ +{ + "endOfLine": "auto", + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100 +} diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts new file mode 100644 index 000000000..b33ff9d10 --- /dev/null +++ b/frontend/.storybook/main.ts @@ -0,0 +1,17 @@ +import type { StorybookConfig } from '@storybook/react-webpack5'; +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], + addons: [ + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/addon-interactions', + ], + framework: { + name: '@storybook/react-webpack5', + options: {}, + }, + docs: { + autodocs: 'tag', + }, +}; +export default config; diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx new file mode 100644 index 000000000..0ca6d5b72 --- /dev/null +++ b/frontend/.storybook/preview.tsx @@ -0,0 +1,26 @@ +import type { Preview } from '@storybook/react'; +import { ThemeProvider } from 'styled-components'; +import { theme } from '../src/style/theme'; +import GlobalStyle from '../src/style/GlobalStyle'; + +const preview: Preview = { + parameters: { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + }, + decorators: [ + (Story) => ( + + + + + ), + ], +}; + +export default preview; diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts new file mode 100644 index 000000000..17161e32e --- /dev/null +++ b/frontend/cypress.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "cypress"; + +export default defineConfig({ + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, + }, +}); diff --git a/frontend/cypress/support/e2e.ts b/frontend/cypress/support/e2e.ts new file mode 100644 index 000000000..f80f74f8e --- /dev/null +++ b/frontend/cypress/support/e2e.ts @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 000000000..c771b29ee --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,60 @@ +{ + "name": "frontend", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "@tanstack/react-query": "^4.29.25", + "@tanstack/react-query-devtools": "^4.29.25", + "react": "^18.2.0", + "react-daum-postcode": "^3.1.3", + "react-dom": "^18.2.0", + "react-icons": "^4.10.1", + "react-router-dom": "^6.14.1", + "styled-components": "^6.0.2" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "webpack-dev-server --config webpack.dev.js --open --hot", + "build": "webpack --config webpack.prod.js", + "start": "webpack --config webpack.dev.js", + "cypress:open": "cypress open", + "cypress:run": "cypress run --browser chrome", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "devDependencies": { + "@babel/preset-env": "^7.22.9", + "@babel/preset-react": "^7.22.5", + "@babel/preset-typescript": "^7.22.5", + "@storybook/addon-essentials": "^7.0.27", + "@storybook/addon-interactions": "^7.0.27", + "@storybook/addon-links": "^7.0.27", + "@storybook/blocks": "^7.0.27", + "@storybook/react": "^7.0.27", + "@storybook/react-webpack5": "^7.0.27", + "@storybook/testing-library": "^0.0.14-next.2", + "@tanstack/eslint-plugin-query": "^4.29.25", + "@types/react": "^18.2.14", + "@types/react-dom": "^18.2.6", + "@types/styled-components": "^5.1.26", + "@typescript-eslint/eslint-plugin": "^5.61.0", + "@typescript-eslint/parser": "^5.61.0", + "color-thief-react": "^2.1.0", + "cypress": "^12.17.1", + "dotenv": "^16.3.1", + "eslint": "^8.44.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-storybook": "^0.6.12", + "html-webpack-plugin": "^5.5.3", + "msw": "^1.2.2", + "prettier": "^2.8.8", + "storybook": "^7.0.27", + "ts-loader": "^9.4.4", + "typescript": "^5.1.6", + "webpack": "^5.88.1", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" + } +} diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 000000000..f4a268b06 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,11 @@ + + + + + + 스탬프 크러쉬 + + +
+ + diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js new file mode 100644 index 000000000..8ee70b3e4 --- /dev/null +++ b/frontend/public/mockServiceWorker.js @@ -0,0 +1,303 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker (1.2.2). + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70' +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: INTEGRITY_CHECKSUM, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: true, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + const accept = request.headers.get('accept') || '' + + // Bypass server-sent events. + if (accept.includes('text/event-stream')) { + return + } + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = Math.random().toString(16).slice(2) + + event.respondWith( + handleRequest(event, requestId).catch((error) => { + if (error.name === 'NetworkError') { + console.warn( + '[MSW] Successfully emulated a network error for the "%s %s" request.', + request.method, + request.url, + ) + return + } + + // At this point, any exception indicates an issue with the original request/response. + console.error( + `\ +[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, + request.method, + request.url, + `${error.name}: ${error.message}`, + ) + }), + ) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const clonedResponse = response.clone() + sendToClient(client, { + type: 'RESPONSE', + payload: { + requestId, + type: clonedResponse.type, + ok: clonedResponse.ok, + status: clonedResponse.status, + statusText: clonedResponse.statusText, + body: + clonedResponse.body === null ? null : await clonedResponse.text(), + headers: Object.fromEntries(clonedResponse.headers.entries()), + redirected: clonedResponse.redirected, + }, + }) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + const clonedRequest = request.clone() + + function passthrough() { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const headers = Object.fromEntries(clonedRequest.headers.entries()) + + // Remove MSW-specific request headers so the bypassed requests + // comply with the server's CORS preflight check. + // Operate with the headers as an object because request "Headers" + // are immutable. + delete headers['x-msw-bypass'] + + return fetch(clonedRequest, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Bypass requests with the explicit bypass header. + // Such requests can be issued by "ctx.fetch()". + if (request.headers.get('x-msw-bypass') === 'true') { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const clientMessage = await sendToClient(client, { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + mode: request.mode, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.text(), + bodyUsed: request.bodyUsed, + keepalive: request.keepalive, + }, + }) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'MOCK_NOT_FOUND': { + return passthrough() + } + + case 'NETWORK_ERROR': { + const { name, message } = clientMessage.data + const networkError = new Error(message) + networkError.name = name + + // Rejecting a "respondWith" promise emulates a network error. + throw networkError + } + } + + return passthrough() +} + +function sendToClient(client, message) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [channel.port2]) + }) +} + +function sleep(timeMs) { + return new Promise((resolve) => { + setTimeout(resolve, timeMs) + }) +} + +async function respondWithMock(response) { + await sleep(response.delay) + return new Response(response.body, response) +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 000000000..7db18d6fe --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,22 @@ +import GlobalStyle from './style/GlobalStyle'; +import { ThemeProvider } from 'styled-components'; +import Router from './Router'; +import { theme } from './style/theme'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; + +const queryClient = new QueryClient(); + +const App = () => { + return ( + + + + + + + + ); +}; + +export default App; diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx new file mode 100644 index 000000000..6b178390e --- /dev/null +++ b/frontend/src/Router.tsx @@ -0,0 +1,102 @@ +import { RouterProvider, createBrowserRouter, Outlet } from 'react-router-dom'; +import { ROUTER_PATH } from './constants'; + +import CustomerList from './pages/Admin/CustomerList'; +import ManageCafe from './pages/Admin/ManageCafe'; +import CouponList from './pages/CouponList'; +import EnterPhoneNumber from './pages/Admin/EnterPhoneNumber'; +import Login from './pages/Login'; +import SignUp from './pages/SignUp'; +import NotFound from './pages/NotFound'; +import RegisterCafe from './pages/Admin/RegisterCafe'; +import MyPage from './pages/MyPage'; +import Template from './components/Template'; +import CustomCouponDesign from './pages/Admin/CouponDesign/CustomCouponDesign'; +import ModifyCouponPolicy from './pages/Admin/ModifyCouponPolicy'; +import SelectCoupon from './pages/Admin/EarnStamp/SelectCoupon'; +import RewardPage from './pages/Admin/RewardPage'; +import EarnStamp from './pages/Admin/EarnStamp'; +import CustomerTemplate from './components/Template/CustomerTemplate'; +import RewardList from './pages/RewardList'; +import RewardHistoryPage from './pages/HistoryPage/RewardHistory'; +import StampHistoryPage from './pages/HistoryPage/StampHistory'; +import Auth from './pages/Auth'; +import AdminLogin from './pages/Admin/AdminLogin'; +import AdminAuth from './pages/Admin/AdminAuth'; +import TemplateCouponDesign from './pages/Admin/CouponDesign/TemplateCouponDesign'; +import InputPhoneNumber from './pages/InputPhoneNumber'; + +const AdminRoot = () => { + return ( + <> + + + ); +}; + +const CustomerRoot = () => { + return ( + + + + ); +}; + +const Router = () => { + const router = createBrowserRouter([ + // 사장 + { path: ROUTER_PATH.adminLogin, element: }, + { path: ROUTER_PATH.adminSignup, element: }, + { path: ROUTER_PATH.adminAuth, element: }, + { path: ROUTER_PATH.enterReward, element: }, + { path: ROUTER_PATH.enterStamp, element: }, + { + path: '/', + element: , + errorElement: , + children: [ + { path: ROUTER_PATH.customerList, element: }, + { + path: ROUTER_PATH.modifyCouponPolicy, + element: , + }, + { + path: ROUTER_PATH.modifyCouponPolicy + ROUTER_PATH.templateCouponDesign, + element: , + }, + { + path: ROUTER_PATH.modifyCouponPolicy + ROUTER_PATH.customCouponDesign, + element: , + }, + { path: ROUTER_PATH.manageCafe, element: }, + { path: ROUTER_PATH.registerCafe, element: }, + { path: ROUTER_PATH.selectCoupon, element: }, + { path: ROUTER_PATH.earnStamp, element: }, + { path: ROUTER_PATH.useReward, element: }, + ], + }, + // 고객 + { + path: '/', + element: , + errorElement: , + children: [ + { index: true, element: }, + { path: ROUTER_PATH.auth, element: }, + { path: ROUTER_PATH.login, element: }, + { path: ROUTER_PATH.signup, element: }, + { path: ROUTER_PATH.myPage, element: }, + { path: ROUTER_PATH.rewardList, element: }, + { path: ROUTER_PATH.rewardHistory, element: }, + { path: ROUTER_PATH.stampHistory, element: }, + { path: ROUTER_PATH.inputPhoneNumber, element: }, + ], + }, + ]); + + return ; +}; + +export default Router; diff --git a/frontend/src/api/delete.ts b/frontend/src/api/delete.ts new file mode 100644 index 000000000..8802a18f6 --- /dev/null +++ b/frontend/src/api/delete.ts @@ -0,0 +1,5 @@ +import { api, customerHeader } from '.'; + +export const deleteCoupon = async (couponId: number) => { + return await api.delete(`/coupons/${couponId}`, customerHeader()); +}; diff --git a/frontend/src/api/get.ts b/frontend/src/api/get.ts new file mode 100644 index 000000000..5466228ca --- /dev/null +++ b/frontend/src/api/get.ts @@ -0,0 +1,117 @@ +import { api, customerHeader, ownerHeader } from '.'; +import { PARAMS_ERROR_MESSAGE } from '../constants'; +import { CouponDesign } from '../types'; +import { + CafeIdParams, + CafeInfoRes, + CafeRes, + CouponRes, + CustomerIdParams, + CustomerPhoneNumberRes, + CustomersRes, + IssuedCouponsRes, + MaxStampCountParams, + OAuthJWTRes, + OAuthTokenParams, + MyRewardRes, + PhoneNumberParams, + QueryReq, + RewardRes, + SampleCouponRes, + StampHistoryRes, + UsedParams, + CustomerProfileRes, +} from '../types/api'; + +export const getCafe = async () => { + return await api.get('/admin/cafes', ownerHeader()); +}; + +export const getCustomer = async ({ params }: QueryReq) => { + if (!params) throw new Error(PARAMS_ERROR_MESSAGE); + return await api.get( + `/admin/customers?phone-number=${params.phoneNumber}`, + ownerHeader(), + ); +}; + +export const getCustomers = async ({ params }: QueryReq) => { + if (!params) throw new Error(PARAMS_ERROR_MESSAGE); + return await api.get(`/admin/cafes/${params.cafeId}/customers`, ownerHeader()); +}; + +export const getCoupon = async ({ params }: QueryReq) => { + if (!params) throw new Error(PARAMS_ERROR_MESSAGE); + return await api.get( + `/admin/customers/${params.customerId}/coupons?cafe-id=${params.cafeId}&active=true`, + ownerHeader(), + ); +}; + +export const getReward = async ({ params }: QueryReq) => { + if (!params) throw new Error(PARAMS_ERROR_MESSAGE); + return await api.get( + `/admin/customers/${params.customerId}/rewards?cafe-id=${params.cafeId}&used=${false}`, + ownerHeader(), + ); +}; + +export const getCouponSamples = async ({ params }: QueryReq) => { + if (!params) throw new Error(PARAMS_ERROR_MESSAGE); + return await api.get( + `/admin/coupon-samples?max-stamp-count=${params.maxStampCount}`, + ownerHeader(), + ); +}; + +export const getCoupons = async () => { + return await api.get('/coupons', customerHeader()); +}; + +export const getCafeInfo = async ({ params }: QueryReq) => { + if (!params) throw new Error(PARAMS_ERROR_MESSAGE); + return await api.get(`/cafes/${params.cafeId}`, customerHeader()); +}; + +export const getMyRewards = async ({ params }: QueryReq) => { + if (!params) throw new Error(PARAMS_ERROR_MESSAGE); + return await api.get(`/rewards?used=${params.used}`, customerHeader()); +}; + +export const getStampHistories = async () => { + return await api.get('/stamp-histories', customerHeader()); +}; + +export const getAdminOAuthToken = async ( + { params }: QueryReq, + init: RequestInit = {}, +) => { + if (!params) throw new Error(PARAMS_ERROR_MESSAGE); + return await api.get( + `/admin/login/${params.resourceServer}/token?code=${params.code}`, + init, + ); +}; + +export const getOAuthToken = async ( + { params }: QueryReq, + init: RequestInit = {}, +) => { + if (!params) throw new Error(PARAMS_ERROR_MESSAGE); + return await api.get( + `/login/${params.resourceServer}/token?code=${params.code}`, + init, + ); +}; + +export const getCustomerProfile = async () => { + return await api.get('/profiles', customerHeader()); +}; + +export const getCouponDesign = async ({ params }: QueryReq) => { + if (!params) throw new Error(PARAMS_ERROR_MESSAGE); + return await api.get( + `/admin/coupon-setting?cafe-id=${params.cafeId}`, + ownerHeader(), + ); +}; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 000000000..c520f3a53 --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,60 @@ +import { worker } from '../mocks/browser'; + +const request = async (path: string, init?: RequestInit) => { + let BASE_URL = process.env.REACT_APP_BASE_URL; + + if (process.env.NODE_ENV === 'development') { + worker.start({ onUnhandledRequest: 'bypass' }); + BASE_URL = ''; + } + + const response = await fetch(`${BASE_URL}${path}`, { + ...init, + headers: { + 'Content-Type': 'application/json', + ...init?.headers, + }, + }); + + if (!response.ok) throw new Error(response.status.toString()); + return response; +}; + +export const api = { + get: (path: string, init?: RequestInit) => + request(path, init).then((response) => response.json()), + + patch: (path: string, init?: RequestInit, payload?: T) => + request(path, { + headers: init?.headers, + method: 'PATCH', + body: JSON.stringify(payload), + }), + + post: (path: string, init?: RequestInit, payload?: T) => + request(path, { + headers: init?.headers, + method: 'POST', + body: JSON.stringify(payload), + }), + + delete: (path: string, init?: RequestInit) => + request(path, { + headers: init?.headers, + method: 'DELETE', + }), +}; + +export const customerHeader = () => ({ + headers: { + Authorization: `Bearer ${localStorage.getItem('login-token')}`, + 'Content-Type': 'application/json', + }, +}); + +export const ownerHeader = () => ({ + headers: { + Authorization: `Bearer ${localStorage.getItem('admin-login-token')}`, + 'Content-Type': 'application/json', + }, +}); diff --git a/frontend/src/api/patch.ts b/frontend/src/api/patch.ts new file mode 100644 index 000000000..ab584fcf3 --- /dev/null +++ b/frontend/src/api/patch.ts @@ -0,0 +1,27 @@ +import { api, ownerHeader } from '.'; +import { + CafeIdParams, + CafeInfoReqBody, + CustomerIdParams, + MutateReq, + RewardIdParams, + RewardReqBody, +} from '../types/api'; +import { PARAMS_ERROR_MESSAGE } from '../constants'; + +export const patchReward = async ({ + params, + body, +}: MutateReq) => { + if (!params) throw new Error(PARAMS_ERROR_MESSAGE); + return await api.patch( + `/admin/customers/${params.customerId}/rewards/${params.rewardId}`, + ownerHeader(), + body, + ); +}; + +export const patchCafeInfo = async ({ params, body }: MutateReq) => { + if (!params) throw new Error(PARAMS_ERROR_MESSAGE); + return await api.patch(`/admin/cafes/${params.cafeId}`, ownerHeader(), body); +}; diff --git a/frontend/src/api/post.ts b/frontend/src/api/post.ts new file mode 100644 index 000000000..1b0a40df2 --- /dev/null +++ b/frontend/src/api/post.ts @@ -0,0 +1,81 @@ +import { api, customerHeader, ownerHeader } from '.'; +import { + CouponSettingReqBody, + StampEarningReqBody, + CafeRegisterReqBody, + IssueCouponReqBody, + RegisterUserReqBody, + CafeIdParams, + MutateReq, + IsFavoritesReqBody, + CustomerIdParams, + CouponIdParams, +} from '../types/api'; + +export const postEarnStamp = async ({ + params, + body, +}: MutateReq) => { + if (!params) return; + return await api.post( + `/admin/customers/${params.customerId}/coupons/${params.couponId}/stamps`, + ownerHeader(), + body, + ); +}; + +export const postRegisterUser = async ({ body }: MutateReq) => { + return await api.post('/admin/temporary-customers', ownerHeader(), body); +}; + +export const postIssueCoupon = async ({ + params, + body, +}: MutateReq) => { + if (!params) return; + return await api + .post(`/admin/customers/${params.customerId}/coupons`, ownerHeader(), body) + .then((response) => response.json()); +}; + +export const postCouponSetting = async ({ + params, + body, +}: MutateReq) => { + if (!params) return; + return await api.post( + `/admin/coupon-setting?cafe-id=${params.cafeId}`, + ownerHeader(), + body, + ); +}; + +export const postRegisterCafe = async ({ body }: MutateReq) => { + return await api.post('/admin/cafes', ownerHeader(), body); +}; + +export const postIsFavorites = async ({ + params, + body, +}: MutateReq) => { + if (!params) return; + return await api.post( + `/cafes/${params.cafeId}/favorites`, + customerHeader(), + body, + ); +}; + +export const postUploadImage = async (file: File) => { + const formData = new FormData(); + formData.append('image', file); + + return await fetch(`${process.env.REACT_APP_BASE_URL}/admin/images`, { + method: 'POST', + body: formData, + }); +}; + +export const postCustomerPhoneNumber = async ({ body }: MutateReq) => { + return await api.post('/profiles/phone-number', customerHeader(), body); +}; diff --git a/frontend/src/assets/coupon_load_img_for_customer.png b/frontend/src/assets/coupon_load_img_for_customer.png new file mode 100644 index 000000000..72ad9e29f Binary files /dev/null and b/frontend/src/assets/coupon_load_img_for_customer.png differ diff --git a/frontend/src/assets/coupon_loading_img.png b/frontend/src/assets/coupon_loading_img.png new file mode 100644 index 000000000..047b9c547 Binary files /dev/null and b/frontend/src/assets/coupon_loading_img.png differ diff --git a/frontend/src/assets/coupon_preview.png b/frontend/src/assets/coupon_preview.png new file mode 100644 index 000000000..f04b91a14 Binary files /dev/null and b/frontend/src/assets/coupon_preview.png differ diff --git a/frontend/src/assets/default_cafe_bg.png b/frontend/src/assets/default_cafe_bg.png new file mode 100644 index 000000000..42b62d562 Binary files /dev/null and b/frontend/src/assets/default_cafe_bg.png differ diff --git a/frontend/src/assets/fonts/Yeongdeok_Blueroad.ttf b/frontend/src/assets/fonts/Yeongdeok_Blueroad.ttf new file mode 100644 index 000000000..bf0b699df Binary files /dev/null and b/frontend/src/assets/fonts/Yeongdeok_Blueroad.ttf differ diff --git a/frontend/src/assets/grover_logo.svg b/frontend/src/assets/grover_logo.svg new file mode 100644 index 000000000..563e510ba --- /dev/null +++ b/frontend/src/assets/grover_logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/index.ts b/frontend/src/assets/index.ts new file mode 100644 index 000000000..101acf9ce --- /dev/null +++ b/frontend/src/assets/index.ts @@ -0,0 +1,6 @@ +export { default as StampcrushLogo } from './stampcrush_logo.svg'; +export { default as StampcrushWhiteLogo } from './stampcrush_logo_white.svg'; +export { default as NaverLoginButton } from './naver_login_button.svg'; +export { default as KakaoLoginButton } from './kakao_login.svg'; +export { default as CustomerKakaoLoginButton } from './kakao_login_medium_narrow.svg'; +export { default as LoginLogo } from './login_logo.svg'; diff --git a/frontend/src/assets/kakao_login.svg b/frontend/src/assets/kakao_login.svg new file mode 100644 index 000000000..bc75ba596 --- /dev/null +++ b/frontend/src/assets/kakao_login.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/assets/kakao_login_medium_narrow.svg b/frontend/src/assets/kakao_login_medium_narrow.svg new file mode 100644 index 000000000..2a8745a6b --- /dev/null +++ b/frontend/src/assets/kakao_login_medium_narrow.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/assets/loading_spinner.gif b/frontend/src/assets/loading_spinner.gif new file mode 100644 index 000000000..75ab912d1 Binary files /dev/null and b/frontend/src/assets/loading_spinner.gif differ diff --git a/frontend/src/assets/login_logo.svg b/frontend/src/assets/login_logo.svg new file mode 100644 index 000000000..792596b0b --- /dev/null +++ b/frontend/src/assets/login_logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/naver_login_button.svg b/frontend/src/assets/naver_login_button.svg new file mode 100644 index 000000000..5e04119bc --- /dev/null +++ b/frontend/src/assets/naver_login_button.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/assets/stamp_load_img.png b/frontend/src/assets/stamp_load_img.png new file mode 100644 index 000000000..8a5ac4fab Binary files /dev/null and b/frontend/src/assets/stamp_load_img.png differ diff --git a/frontend/src/assets/stamp_preview.png b/frontend/src/assets/stamp_preview.png new file mode 100644 index 000000000..b5363a32d Binary files /dev/null and b/frontend/src/assets/stamp_preview.png differ diff --git a/frontend/src/assets/stampcrush_logo.svg b/frontend/src/assets/stampcrush_logo.svg new file mode 100644 index 000000000..9050af933 --- /dev/null +++ b/frontend/src/assets/stampcrush_logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/stampcrush_logo_white.svg b/frontend/src/assets/stampcrush_logo_white.svg new file mode 100644 index 000000000..1e5f42df4 --- /dev/null +++ b/frontend/src/assets/stampcrush_logo_white.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/Alert/index.tsx b/frontend/src/components/Alert/index.tsx new file mode 100644 index 000000000..18fee9477 --- /dev/null +++ b/frontend/src/components/Alert/index.tsx @@ -0,0 +1,36 @@ +import ReactDOM from 'react-dom'; +import { AlertContainer, BackDrop, OptionContainer, OptionWrapper, TextContainer } from './style'; +import { CiCircleAlert } from 'react-icons/ci'; + +export interface AlertProps { + text: string; + rightOption: string; + leftOption: string; + onClickRight: () => void; + onClickLeft: () => void; +} + +const Alert = ({ text, rightOption, leftOption, onClickLeft, onClickRight }: AlertProps) => { + return ReactDOM.createPortal( + <> + + + + + {text} + + + + {leftOption} + + + {rightOption} + + + + , + document.querySelector('body') as HTMLBodyElement, + ); +}; + +export default Alert; diff --git a/frontend/src/components/Alert/style.tsx b/frontend/src/components/Alert/style.tsx new file mode 100644 index 000000000..68452a0d2 --- /dev/null +++ b/frontend/src/components/Alert/style.tsx @@ -0,0 +1,65 @@ +import { styled } from 'styled-components'; + +export const AlertContainer = styled.div` + display: flex; + flex-direction: column; + max-width: 400px; + width: 70%; + position: absolute; + top: 50%; + left: 50%; + border-radius: 10px; + z-index: 100; + transform: translate(-50%, -50%); +`; + +export const BackDrop = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 120vh; + z-index: 10; + background-color: rgba(0, 0, 0, 0.3); +`; + +export const TextContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 30px; + gap: 20px; + background: #e8e8e8; + width: 100%; + height: 80%; + border-radius: 10px 10px 0 0; + text-align: center; + white-space: pre-line; + line-height: 24px; + + & > img { + width: 60px; + height: 60px; + color: #eee; + } +`; + +export const OptionContainer = styled.div` + display: flex; + border-radius: 0 0 10px 10px; + + background: white; + overflow: hidden; +`; + +export const OptionWrapper = styled.button<{ $option: string }>` + width: 50%; + height: 50px; + color: ${(props) => (props.$option === 'left' ? '#888' : '#424242')}; + + &:active { + background: #eeeeee; + opacity: 50%; + transform: scale(0.95); + } +`; diff --git a/frontend/src/components/Button/index.tsx b/frontend/src/components/Button/index.tsx new file mode 100644 index 000000000..b839db094 --- /dev/null +++ b/frontend/src/components/Button/index.tsx @@ -0,0 +1,13 @@ +import { ButtonHTMLAttributes } from 'react'; +import { BaseButton } from './style'; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: 'primary' | 'secondary'; + size?: 'medium' | 'large'; +} + +const Button = ({ variant = 'primary', size = 'medium', ...props }: ButtonProps) => { + return ; +}; + +export default Button; diff --git a/frontend/src/components/Button/style.tsx b/frontend/src/components/Button/style.tsx new file mode 100644 index 000000000..e45e87db9 --- /dev/null +++ b/frontend/src/components/Button/style.tsx @@ -0,0 +1,52 @@ +import { styled } from 'styled-components'; + +interface StyledButtonProps { + $variant: 'primary' | 'secondary'; + $size: 'medium' | 'large'; +} + +const TYPE: Record> = { + primary: { + color: 'white', + background: 'main', + border: '0 solid transparent', + }, + secondary: { + color: 'black', + background: 'white', + border: '2px solid #222', + }, +}; + +const SIZE: Record> = { + medium: { + padding: '8px 35px', + width: 'auto', + }, + large: { + padding: '8px 0px', + width: '100%', + }, +}; + +export const BaseButton = styled.button` + outline: none; + border-radius: 7px; + + transition: background-color 0.2s ease color 0.1s ease; + font-weight: 600; + line-height: 26px; + font-size: 15px; + + width: ${({ $size }) => SIZE[$size].width}; + padding: ${({ $size }) => SIZE[$size].padding}; + border: ${({ $variant }) => TYPE[$variant].border}; + color: ${({ theme, $variant }) => theme.colors[TYPE[$variant].color]}; + background-color: ${({ theme, $variant }) => theme.colors[TYPE[$variant].background]}; + + cursor: pointer; + &:active { + transform: scale(0.985); + opacity: 70%; + } +`; diff --git a/frontend/src/components/Header/SubHeader/index.tsx b/frontend/src/components/Header/SubHeader/index.tsx new file mode 100644 index 000000000..3f0716ec6 --- /dev/null +++ b/frontend/src/components/Header/SubHeader/index.tsx @@ -0,0 +1,27 @@ +import { ArrowIconWrapper, HeaderContainer } from './style'; +import { useNavigate } from 'react-router-dom'; +import { ROUTER_PATH } from '../../../constants'; +import { BiArrowBack } from 'react-icons/bi'; + +interface SubHeaderProps { + title: string; +} + +const SubHeader = ({ title }: SubHeaderProps) => { + const navigate = useNavigate(); + + const navigateMyPage = () => { + navigate(ROUTER_PATH.myPage); + }; + + return ( + + + + + {title} + + ); +}; + +export default SubHeader; diff --git a/frontend/src/components/Header/SubHeader/style.tsx b/frontend/src/components/Header/SubHeader/style.tsx new file mode 100644 index 000000000..ccef26b47 --- /dev/null +++ b/frontend/src/components/Header/SubHeader/style.tsx @@ -0,0 +1,21 @@ +import { styled } from 'styled-components'; + +export const ArrowIconWrapper = styled.button` + position: absolute; + left: 20px; + width: 24px; + height: 24px; + color: black; + background: transparent; +`; + +export const HeaderContainer = styled.title` + display: flex; + position: relative; + align-items: center; + justify-content: center; + font-size: 18px; + font-weight: 600; + height: 60px; + border-bottom: 1px solid #222; +`; diff --git a/frontend/src/components/Header/index.tsx b/frontend/src/components/Header/index.tsx new file mode 100644 index 000000000..86d3bc1e4 --- /dev/null +++ b/frontend/src/components/Header/index.tsx @@ -0,0 +1,24 @@ +import { HeaderContainer, LogoImg, LogoutButton } from './style'; +import AdminHeaderLogo from '../../assets/admin_header_logo.png'; +import { Link, useNavigate } from 'react-router-dom'; +import { ROUTER_PATH } from '../../constants'; + +const Header = () => { + const navigate = useNavigate(); + + const handleLogout = () => { + // TODO: log out 로직 + navigate(ROUTER_PATH.adminLogin); + }; + + return ( + + + + + 로그아웃 + + ); +}; + +export default Header; diff --git a/frontend/src/components/Header/style.tsx b/frontend/src/components/Header/style.tsx new file mode 100644 index 000000000..5eb676b6a --- /dev/null +++ b/frontend/src/components/Header/style.tsx @@ -0,0 +1,21 @@ +import { styled } from 'styled-components'; + +export const HeaderContainer = styled.header` + display: flex; + height: 59px; + justify-content: space-between; + align-items: center; + padding: 0 60px; + border-bottom: 1px solid #888; +`; + +export const LogoImg = styled.img` + height: 30px; +`; + +export const LogoutButton = styled.button` + border: none; + background: transparent; + color: black; + cursor: pointer; +`; diff --git a/frontend/src/components/Input/index.tsx b/frontend/src/components/Input/index.tsx new file mode 100644 index 000000000..1a7ae9e14 --- /dev/null +++ b/frontend/src/components/Input/index.tsx @@ -0,0 +1,28 @@ +import { HTMLInputTypeAttribute, forwardRef, InputHTMLAttributes } from 'react'; +import { BaseInput, InputContainer, Label, LabelWrapper, Required } from './style'; + +const REQUIRED = '*' as const; + +interface InputProps extends InputHTMLAttributes { + id: string; + label?: string; + type?: HTMLInputTypeAttribute; + width?: number; + placeholder?: string; + maxLength?: number; + required?: boolean; +} + +export const Input = forwardRef((props, ref) => { + return ( + + + + {props.required && {REQUIRED}} + + + + ); +}); + +Input.displayName = 'Input'; diff --git a/frontend/src/components/Input/style.tsx b/frontend/src/components/Input/style.tsx new file mode 100644 index 000000000..fa12f44cf --- /dev/null +++ b/frontend/src/components/Input/style.tsx @@ -0,0 +1,48 @@ +import { styled } from 'styled-components'; + +type StyledInputProps = { + $width?: number; + $center?: boolean; + disabled?: boolean; + $required?: boolean; +}; + +export const BaseInput = styled.input` + width: ${(props) => (props.$width ? `${props.$width}px` : '100%')}; + padding: 8px 4px; + + border: none; + text-align: ${(props) => (props.$center ? 'center' : 'initial')}; + font-size: 16px; + + background: transparent; + border-bottom: 1px solid ${({ theme }) => theme.colors.black}; + transition: 0.4s ease-in-out; + + &:focus { + border-bottom: 1px solid ${({ theme }) => theme.colors.main}; + outline: none; + transition: 0.4s ease-in-out; + } + &::placeholder { + color: #aaa; + } +`; + +export const Label = styled.label` + font-weight: 700; +`; + +export const Required = styled.span` + color: #ff0505; +`; + +export const LabelWrapper = styled.div` + display: flex; + flex-direction: row; +`; + +export const InputContainer = styled.div` + display: flex; + flex-direction: column; +`; diff --git a/frontend/src/components/LoadingSpinner/index.tsx b/frontend/src/components/LoadingSpinner/index.tsx new file mode 100644 index 000000000..6acb1fafc --- /dev/null +++ b/frontend/src/components/LoadingSpinner/index.tsx @@ -0,0 +1,12 @@ +import loadingSpinner from '../../assets/loading_spinner.gif'; +import { LoadingContainer } from './style'; + +const LoadingSpinner = () => { + return ( + + 로딩 중입니다. + + ); +}; + +export default LoadingSpinner; diff --git a/frontend/src/components/LoadingSpinner/style.tsx b/frontend/src/components/LoadingSpinner/style.tsx new file mode 100644 index 000000000..76cb2c990 --- /dev/null +++ b/frontend/src/components/LoadingSpinner/style.tsx @@ -0,0 +1,9 @@ +import styled from 'styled-components'; + +export const LoadingContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100vh; +`; diff --git a/frontend/src/components/Modal/index.tsx b/frontend/src/components/Modal/index.tsx new file mode 100644 index 000000000..32ffc4f73 --- /dev/null +++ b/frontend/src/components/Modal/index.tsx @@ -0,0 +1,26 @@ +import ReactDOM from 'react-dom'; +import { BaseModal, CloseButton, ModalBackdrop } from './style'; +import { ForwardedRef, PropsWithChildren, forwardRef } from 'react'; + +interface ModalProps { + closeModal: () => void; +} + +const Modal = forwardRef( + ({ closeModal, children }: PropsWithChildren, ref: ForwardedRef) => { + return ReactDOM.createPortal( + <> + + + {children} + + + , + document.querySelector('body') as HTMLBodyElement, + ); + }, +); + +Modal.displayName = 'Modal'; + +export default Modal; diff --git a/frontend/src/components/Modal/style.tsx b/frontend/src/components/Modal/style.tsx new file mode 100644 index 000000000..287c16f4c --- /dev/null +++ b/frontend/src/components/Modal/style.tsx @@ -0,0 +1,72 @@ +import styled from 'styled-components'; + +export const ModalBackdrop = styled.div` + position: absolute; + top: 0; + left: 0; + + width: 100vw; + height: 120vh; + + z-index: 0; + + background-color: rgba(0, 0, 0, 0.3); +`; + +export const BaseModal = styled.div` + position: fixed; + + top: 50%; + left: 50%; + width: 60vw; + height: auto; + + padding: 35px; + border-radius: 10px; + box-shadow: rgba(0, 0, 0, 0.1) 3px 3px 5px 0px; + + z-index: 1; + background-color: ${({ theme }) => theme.colors.white}; + + transform: translate(-50%, -50%); + + @media screen and (min-width: 768px) and (max-width: 1024px) { + width: 70vw; + } +`; + +export const CloseButton = styled.button` + position: absolute; + top: 16px; + right: 16px; + width: 25px; + height: 25px; + + border: 2px solid black; + border-radius: 50%; + + background: transparent; + cursor: pointer; + + &:before, + &:after { + content: ''; + position: absolute; + left: 50%; + top: 50%; + + width: 12px; + height: 2px; + + border-radius: 4px; + background: #000; + } + + &:before { + transform: translate(-50%, -50%) rotate(-45deg); + } + + &:after { + transform: translate(-50%, -50%) rotate(45deg); + } +`; diff --git a/frontend/src/components/ProgressBar/index.tsx b/frontend/src/components/ProgressBar/index.tsx new file mode 100644 index 000000000..ccc4da917 --- /dev/null +++ b/frontend/src/components/ProgressBar/index.tsx @@ -0,0 +1,17 @@ +import { Bar, Progress } from './style'; + +interface ProgressBarProps { + stampCount: number; + maxCount: number; + color?: string; +} + +const ProgressBar = ({ stampCount, maxCount, color = 'gray' }: ProgressBarProps) => { + return ( + + + + ); +}; + +export default ProgressBar; diff --git a/frontend/src/components/ProgressBar/style.tsx b/frontend/src/components/ProgressBar/style.tsx new file mode 100644 index 000000000..624021caf --- /dev/null +++ b/frontend/src/components/ProgressBar/style.tsx @@ -0,0 +1,18 @@ +import styled from 'styled-components'; + +export const Bar = styled.div` + width: 80%; + height: 12px; + background-color: #ddd; + border-radius: 8px; + box-shadow: 1px 1px 1px 1px #e0e0e0; +`; + +export const Progress = styled.div<{ $width: number; $color: string }>` + width: ${(props) => `${props.$width}%`}; + height: 12px; + background: ${(props) => `${props.$color}`}; + border-radius: 8px; + transition: all 0.4s; + box-shadow: 1px 1px 1px 1px #e0e0e0; +`; diff --git a/frontend/src/components/SearchBar/index.tsx b/frontend/src/components/SearchBar/index.tsx new file mode 100644 index 000000000..ccffd3fb5 --- /dev/null +++ b/frontend/src/components/SearchBar/index.tsx @@ -0,0 +1,30 @@ +import { BaseInput, Container, SearchButton } from './style'; +import { BsSearch } from 'react-icons/bs'; +import { ChangeEvent, FormEvent, InputHTMLAttributes } from 'react'; + +export interface SearchBarProps extends InputHTMLAttributes { + searchWord: string; + setSearchWord: (searchWord: string) => void; + onClick: () => void; +} + +const SearchBar = ({ searchWord, setSearchWord, onClick, ...props }: SearchBarProps) => { + const search = (e: ChangeEvent) => { + setSearchWord(e.target.value); + }; + + const handleForm = (e: FormEvent) => { + e.preventDefault(); + }; + + return ( + + + + + + + ); +}; + +export default SearchBar; diff --git a/frontend/src/components/SearchBar/style.ts b/frontend/src/components/SearchBar/style.ts new file mode 100644 index 000000000..06c357c98 --- /dev/null +++ b/frontend/src/components/SearchBar/style.ts @@ -0,0 +1,35 @@ +import { styled } from 'styled-components'; + +export const Container = styled.form` + position: relative; + width: 250px; +`; + +export const BaseInput = styled.input` + display: flex; + width: 250px; + height: 40px; + + background: white; + box-shadow: 0px 0px 15px #888; + border-radius: 30px; + outline: none; + padding: 0 15px; + + &:focus { + border: 2px solid ${({ theme }) => theme.colors.text}; + } +`; + +export const SearchButton = styled.button` + width: 45px; + height: 45px; + border-radius: 50%; + position: absolute; + right: -10px; + top: -2px; + box-shadow: 0px 0px 15px #888; + background: ${({ theme }) => theme.colors.text}; + + cursor: pointer; +`; diff --git a/frontend/src/components/SelectBox/index.tsx b/frontend/src/components/SelectBox/index.tsx new file mode 100644 index 000000000..ed291a1fa --- /dev/null +++ b/frontend/src/components/SelectBox/index.tsx @@ -0,0 +1,58 @@ +import { MouseEvent, useState, Dispatch, SetStateAction } from 'react'; +import { Option } from '../../types'; +import { BaseSelectBox, LabelContent, SelectBoxWrapper, SelectContent } from './style'; + +interface SelectBoxProps { + width?: number; + options: Option[]; + checkedOption: Option; + setCheckedOption: Dispatch>; +} + +const SelectBox = ({ options, checkedOption, setCheckedOption, width = 110 }: SelectBoxProps) => { + const [isExpanded, setIsExpanded] = useState(false); + + const getOption = (value: string) => { + return [...options.filter((option: Option) => option.value === value && option)][0]; + }; + + const toggleExpandSelectBox = (e: MouseEvent) => { + e.preventDefault(); + + setIsExpanded(!isExpanded); + + if (e.target instanceof HTMLLabelElement) { + e.target.textContent && setCheckedOption(getOption(e.target.textContent)); + } + + if (e.target instanceof HTMLInputElement) { + setCheckedOption(getOption(e.target.value)); + } + }; + + return ( + + + {options.map((option) => ( + <> + + {option.value} + + ))} + + + ); +}; + +export default SelectBox; diff --git a/frontend/src/components/SelectBox/style.tsx b/frontend/src/components/SelectBox/style.tsx new file mode 100644 index 000000000..de48105d4 --- /dev/null +++ b/frontend/src/components/SelectBox/style.tsx @@ -0,0 +1,114 @@ +import styled, { css } from 'styled-components'; + +export const BaseSelectBox = styled.span<{ + $minWidth: number; + $minHeight: number; + $expanded: boolean; +}>` + position: relative; + display: inline-block; + margin-right: 1px; + min-height: ${(props) => `${props.$minHeight}px`}; + max-height: ${(props) => `${props.$minHeight}px`}; + width: ${(props) => `${props.$minWidth}px`}; + + overflow: hidden; + + cursor: pointer; + text-align: left; + white-space: nowrap; + + outline: none; + border: 1px solid ${({ theme }) => theme.colors.main}; + border-radius: 4px; + background-color: white; + + transition: 0.4s all ease-in-out; + + input:checked + label { + display: block; + border-top: none; + position: absolute; + top: 0; + width: 100%; + + &:nth-child(2) { + margin-top: 0; + position: relative; + } + } + + &::after { + content: ''; + position: absolute; + right: 12px; + top: 10px; + + width: 6px; + height: 6px; + + border-top: 1px solid ${({ theme }) => theme.colors.gray}; + border-right: 1px solid ${({ theme }) => theme.colors.gray}; + transform: rotate(-225deg); + + transition: 0.4s all ease-in-out; + } + + ${(props) => + props.$expanded && + css` + border: 1px solid ${({ theme }) => theme.colors.main}; + background: #fff; + border-radius: 4px; + padding: 0; + box-shadow: rgba(0, 0, 0, 0.1) 3px 3px 5px 0px; + max-height: 400px; + z-index: 1; + + label { + border-top: 1px solid ${({ theme }) => theme.colors.gray}; + &:hover { + color: ${({ theme }) => theme.colors.main}; + } + } + input:checked + label { + color: ${({ theme }) => theme.colors.main}; + } + + &::after { + transform: rotate(-45deg); + top: 12px; + } + `} +`; + +export const SelectContent = styled.input` + width: 1px; + height: 1px; + display: inline-block; + position: absolute; + opacity: 0.01; +`; + +export const LabelContent = styled.label` + border-top: 1px solid ${({ theme }) => theme.colors.gray}; + display: block; + height: 33px; + line-height: 33px; + padding-left: 12px; + padding-right: 66px; + cursor: pointer; + position: relative; + transition: 0.4s color ease-in-out; + + &:nth-child(2) { + margin-top: 33px; + + border-top: 1px solid ${({ theme }) => theme.colors.gray}; + } +`; + +export const SelectBoxWrapper = styled.div` + display: block; + height: 33px; +`; diff --git a/frontend/src/components/SideBar/index.tsx b/frontend/src/components/SideBar/index.tsx new file mode 100644 index 000000000..f595b3119 --- /dev/null +++ b/frontend/src/components/SideBar/index.tsx @@ -0,0 +1,158 @@ +import { useLocation, useNavigate } from 'react-router-dom'; +import { StampcrushWhiteLogo } from '../../assets'; +import { Option } from '../../types'; +import { + Container, + EmptyContent, + LabelContent, + LogoHeader, + LogoImg, + LogoImgWrapper, + LogoutButton, + LogoutContainer, + SideBarContainer, + SideBarContent, +} from './style'; +import { useEffect, useState } from 'react'; +import { + PiUserListLight, + PiBuildingsLight, + PiStampLight, + PiBookOpenTextLight, + PiGiftLight, +} from 'react-icons/pi'; +import { IoIosLogOut } from 'react-icons/io'; +import { ROUTER_PATH } from '../../constants'; + +const SIDE_BAR_OPTIONS: Option[] = [ + { key: '', value: '' }, + { key: '내 고객 목록', value: ROUTER_PATH.customerList }, + { key: '내 카페 관리', value: ROUTER_PATH.manageCafe }, + { key: '쿠폰 제작 및 변경', value: ROUTER_PATH.modifyCouponPolicy }, + { key: '스탬프 적립', value: ROUTER_PATH.enterStamp }, + { key: '리워드 사용', value: ROUTER_PATH.enterReward }, + { key: '', value: '' }, +]; + +const SIDEBAR_ICONS = [ + <>, + , + , + , + , + , + <>, +]; + +const SideBar = () => { + const navigate = useNavigate(); + const current = useLocation().pathname; + const [isDesignCoupon, setIsDesignCoupon] = useState(false); + const [isEarnStamp, setIsEarnStamp] = useState(false); + const [isUseReward, setIsUseReward] = useState(false); + const [currentIndex, setCurrentIndex] = useState( + SIDE_BAR_OPTIONS.findIndex((option) => option.value === current) + 1, + ); + + const modifyPolicyCoupon = ROUTER_PATH.modifyCouponPolicy; + const designCouponRoutes = [ROUTER_PATH.templateCouponDesign, ROUTER_PATH.customCouponDesign]; + const enterStamp = ROUTER_PATH.enterStamp; + const stampRoutes = [ROUTER_PATH.selectCoupon, ROUTER_PATH.earnStamp]; + const enterReward = ROUTER_PATH.enterReward; + const rewardRoutes = [ROUTER_PATH.useReward]; + + useEffect(() => { + const foundIndex = SIDE_BAR_OPTIONS.findIndex(({ value }) => { + if (checkIncludeRoute(value, modifyPolicyCoupon, designCouponRoutes)) { + setIsDesignCoupon(true); + return true; + } + + if (checkIncludeRoute(value, enterStamp, stampRoutes)) { + setIsEarnStamp(true); + return true; + } + + if (checkIncludeRoute(value, enterReward, rewardRoutes)) { + setIsUseReward(true); + return true; + } + + return value === current; + }); + + setCurrentIndex(foundIndex + 1); + }, [current]); + + const handleLogout = () => { + localStorage.setItem('admin-login-token', ''); + + navigate(ROUTER_PATH.adminLogin); + }; + + const checkIncludeRoute = (value: string, route: string, routes: string[]) => { + if (value !== route) return false; + return routes.some((route) => current.includes(route)); + }; + + const routeSideBar = (index: number) => () => { + if (current === ROUTER_PATH.registerCafe) { + alert('카페 등록 후 사용하실 수 있는 서비스입니다. 😄'); + return; + } + if (index === 0 || index === SIDE_BAR_OPTIONS.length - 1) return; + + setCurrentIndex(index + 1); + navigate(SIDE_BAR_OPTIONS[index].value); + }; + + return ( + + + + + + + + {SIDE_BAR_OPTIONS.map(({ key, value }, index) => { + if (index === 0 || index === SIDE_BAR_OPTIONS.length - 1) return ; + return ( + + + {SIDEBAR_ICONS[index]} + {key} + + + ); + })} + + + + + 로그아웃 + + + + ); +}; + +export default SideBar; diff --git a/frontend/src/components/SideBar/style.tsx b/frontend/src/components/SideBar/style.tsx new file mode 100644 index 000000000..1ab3db043 --- /dev/null +++ b/frontend/src/components/SideBar/style.tsx @@ -0,0 +1,137 @@ +import styled from 'styled-components'; + +interface SideBarStyleProps { + $isSelected: boolean; +} + +interface SideBarContainerStyleProps { + $prevIndex: number; + $nextIndex: number; +} + +export const Container = styled.section` + display: flex; + flex-direction: column; + height: 100%; +`; + +export const LogoHeader = styled.header<{ $currentIndex: number }>` + background: ${({ theme }) => theme.colors.main}; + padding-top: 40px; + border-radius: ${({ $currentIndex }) => ($currentIndex === 2 ? '0 0 40px 0' : '0')}; +`; + +export const LogoImgWrapper = styled.button` + display: flex; + align-self: flex-start; + background: transparent; + width: 150px; + padding: 0 0 40px 40px; + + cursor: pointer; +`; + +export const LogoImg = styled.img` + width: 150px; +`; + +export const PageSideBarWrapper = styled.div` + padding: 0 0 0 30px; +`; + +export const SideBarContainer = styled.div` + display: flex; + flex-direction: column; + padding-left: 30px; + width: 240px; + height: 250px; + background: ${({ theme }) => `linear-gradient(to right, ${theme.colors.main} 20%, white 80%)`}; + + div:nth-child(${(props) => props.$prevIndex}) { + border-radius: 0 0 40px 0; + } + + div:nth-child(${(props) => props.$nextIndex}) { + border-radius: 0 40px 0 0; + } +`; + +export const LabelContent = styled.span` + display: flex; + align-items: center; + gap: 10px; + justify-content: flex-start; + width: 240px; + height: 50px; + font-size: 16px; + font-weight: ${(props) => (props.$isSelected ? '600' : '400')}; + color: ${({ theme, $isSelected }) => ($isSelected ? `${theme.colors.text}` : 'white')}; + + border-radius: 40px 0 0 40px; + padding-left: 20px; + + cursor: pointer; +`; + +export const SideBarContent = styled.div<{ $isSelected: boolean; $currentIndex: number }>` + background-color: ${({ $isSelected, theme }) => ($isSelected ? 'white' : theme.colors.main)}; + border-radius: 40px 0 0 40px; + + :hover { + opacity: 80%; + } +`; + +export const ImageWrapper = styled.div` + position: absolute; + bottom: 10px; + left: 20px; +`; + +export const LogoutContainer = styled.div<{ $currentIndex: number }>` + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + /* background: ${({ theme }) => theme.colors.main}; */ + background: ${({ theme }) => + `linear-gradient(to bottom,${theme.colors.main} 10%, ${theme.colors.main} 30%, ${theme.colors.point} 100%)`}; + border-radius: 40px; + border-radius: ${({ $currentIndex }) => ($currentIndex === 6 ? '0 40px 0 0' : '0')}; +`; + +export const LogoutButton = styled.button` + border-top: 0.5px solid rgba(255, 255, 255, 0.2); + display: flex; + align-items: center; + gap: 10px; + color: white; + padding: 20px 0 0 20px; + margin: 10px 30px 0 30px; + background: transparent; + + cursor: pointer; + + &:hover { + opacity: 80%; + } +`; + +export const CopyRight = styled.p` + color: white; + padding-left: 50px; +`; + +export const EmptyContent = styled.div` + width: 240px; + height: 50px; + background: ${({ theme }) => theme.colors.main}; +`; + +export const CharacterImage = styled.img` + position: absolute; + width: 250px; + height: 200px; + bottom: 0; + left: 0; +`; diff --git a/frontend/src/components/Stepper/index.tsx b/frontend/src/components/Stepper/index.tsx new file mode 100644 index 000000000..1cb9714ec --- /dev/null +++ b/frontend/src/components/Stepper/index.tsx @@ -0,0 +1,44 @@ +import { Dispatch, SetStateAction } from 'react'; +import { BaseStepperButton, BaseStepperInput, StepperWrapper } from './style'; + +interface StepperProps { + value: number; + minValue?: number; + maxValue?: number; + step?: number; + height?: number; + setValue: Dispatch>; +} + +const Stepper = ({ + value, + minValue = 0, + maxValue = 10, + step = 1, + height = 42, + setValue, +}: StepperProps) => { + const increaseNumber = () => { + if (value + step > maxValue) return; + setValue(value + step); + }; + + const decreaseNumber = () => { + if (value - step < minValue) return; + setValue(value - step); + }; + + return ( + + + - + + + + + + + + ); +}; + +export default Stepper; diff --git a/frontend/src/components/Stepper/style.tsx b/frontend/src/components/Stepper/style.tsx new file mode 100644 index 000000000..cf9e38c05 --- /dev/null +++ b/frontend/src/components/Stepper/style.tsx @@ -0,0 +1,43 @@ +import { styled } from 'styled-components'; + +interface StyledStepperProps { + $height: number; +} + +interface StepperButtonProps extends StyledStepperProps { + $position: 'left' | 'right'; +} + +export const StepperWrapper = styled.div` + display: flex; + height: 50px; +`; + +export const BaseStepperButton = styled.button` + display: block; + background-color: ${({ theme }) => theme.colors.main}; + border: 3px solid ${({ theme }) => theme.colors.main}; + font-size: 30px; + font-weight: 600; + color: ${({ theme }) => theme.colors.point}; + cursor: pointer; + &:hover { + opacity: 80%; + } + width: 45px; + border-radius: ${({ $position }) => ($position === 'left' ? '10px 0 0 10px' : '0 10px 10px 0')}; +`; + +export const BaseStepperInput = styled.input` + box-sizing: border-box; + height: 50px; + width: 50px; + display: block; + padding: 0; + border: none; + border-top: 3px solid ${({ theme }) => theme.colors.main}; + border-bottom: 3px solid ${({ theme }) => theme.colors.main}; + text-align: center; + font-size: 20px; + font-weight: 700; +`; diff --git a/frontend/src/components/TabBar/index.tsx b/frontend/src/components/TabBar/index.tsx new file mode 100644 index 000000000..04365949e --- /dev/null +++ b/frontend/src/components/TabBar/index.tsx @@ -0,0 +1,35 @@ +import { ChangeEvent } from 'react'; +import { Option } from '../../types'; +import { LabelContent, TabBarContainer, TabBarInput, TabBarItem, TabBarLabel } from './style'; + +interface TabBarProps { + name: string; + options: Option[]; + width: number; + height: number; + selectedValue: string; + onChange: (e: ChangeEvent) => void; +} + +const TabBar = ({ name, options, height, width, selectedValue, onChange }: TabBarProps) => { + return ( + + {options.map(({ key, value }) => ( + + + + {value} + + + + + ))} + + ); +}; + +export default TabBar; diff --git a/frontend/src/components/TabBar/style.tsx b/frontend/src/components/TabBar/style.tsx new file mode 100644 index 000000000..5a6576036 --- /dev/null +++ b/frontend/src/components/TabBar/style.tsx @@ -0,0 +1,46 @@ +import styled from 'styled-components'; + +interface StyledTabBarSelect { + $isSelect: boolean; +} + +interface StyledTabBarWidth { + $height: number; + $width: number; +} + +type StyledTabBarContentProps = StyledTabBarSelect & StyledTabBarWidth; + +export const TabBarContainer = styled.div` + display: flex; + position: relative; + width: ${(props) => `${props.$width}px`}; + height: ${(props) => `${props.$height}px`}; +`; + +export const TabBarLabel = styled.label` + display: flex; + align-items: center; + justify-content: center; + width: 100%; + cursor: pointer; +`; + +export const LabelContent = styled.span` + display: flex; + align-items: center; + justify-content: center; + transition: all 0.4s ease; + color: ${(props) => (props.$isSelect ? 'black' : '#eee')}; + width: ${(props) => `${props.$width}px`}; + height: ${(props) => `${props.$height}px`}; +`; + +export const TabBarItem = styled.div` + transition: all 0.4s ease; + border-bottom: 1px solid ${(props) => (props.$isSelect ? 'black' : '#eee')}; +`; + +export const TabBarInput = styled.input` + display: none; +`; diff --git a/frontend/src/components/Template/CustomerTemplate/index.tsx b/frontend/src/components/Template/CustomerTemplate/index.tsx new file mode 100644 index 000000000..392899afe --- /dev/null +++ b/frontend/src/components/Template/CustomerTemplate/index.tsx @@ -0,0 +1,12 @@ +import { PropsWithChildren } from 'react'; +import { BaseCustomerTemplate, ContentContainer } from './style'; + +const CustomerTemplate = ({ children }: PropsWithChildren) => { + return ( + + {children} + + ); +}; + +export default CustomerTemplate; diff --git a/frontend/src/components/Template/CustomerTemplate/style.tsx b/frontend/src/components/Template/CustomerTemplate/style.tsx new file mode 100644 index 000000000..41ffba695 --- /dev/null +++ b/frontend/src/components/Template/CustomerTemplate/style.tsx @@ -0,0 +1,23 @@ +import styled from 'styled-components'; + +export const BaseCustomerTemplate = styled.div` + display: flex; + width: 100vw; + height: 100vh; +`; + +export const ContentContainer = styled.main` + display: flex; + flex-direction: column; + width: 100%; + height: 100vh; + max-width: 450px; + margin: 0 auto; + border-radius: 8px; + + @media screen and (min-width: 450px) { + position: relative; + + margin: 0 auto; + } +`; diff --git a/frontend/src/components/Template/index.tsx b/frontend/src/components/Template/index.tsx new file mode 100644 index 000000000..3e899ac6f --- /dev/null +++ b/frontend/src/components/Template/index.tsx @@ -0,0 +1,27 @@ +import { PropsWithChildren } from 'react'; +import SideBar from '../SideBar'; +import { BaseTemplate, Footer, PageContainer, SideBarWrapper } from './style'; + +import { AiOutlineMail } from 'react-icons/ai'; + +const Template = ({ children }: PropsWithChildren) => { + return ( + <> + + + + + {children} + +
+ + CONTACT stampcrush@gmail.com +
+
+ COPYRIGHT © 2023 STAMPCRUSH ALL RIGHTS RESERVED +
+ + ); +}; + +export default Template; diff --git a/frontend/src/components/Template/style.tsx b/frontend/src/components/Template/style.tsx new file mode 100644 index 000000000..b0d4a4326 --- /dev/null +++ b/frontend/src/components/Template/style.tsx @@ -0,0 +1,44 @@ +import { styled } from 'styled-components'; + +export const BaseTemplate = styled.main` + display: flex; + margin: 0 auto; + width: 100vw; + height: 100vh; + background: ${({ theme }) => + `linear-gradient(to bottom, ${theme.colors.main} 60%, ${theme.colors.point} 100%)`}; +`; + +export const SideBarWrapper = styled.section` + background: white; +`; + +export const PageContainer = styled.div` + display: flex; + flex-direction: column; + width: 100%; + padding: 0 40px; + margin: 20px 20px 20px 0; + background: white; + border-radius: 20px; + box-shadow: 7px 5px 5px 3px rgba(0, 0, 0, 0.25); +`; + +export const Footer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; + width: 100vw; + height: 150px; + padding-top: 40px; + background: ${({ theme }) => theme.colors.point}; + + & > span { + display: flex; + align-items: center; + gap: 5px; + font-size: 14px; + color: #eee; + } +`; diff --git a/frontend/src/components/Text/index.tsx b/frontend/src/components/Text/index.tsx new file mode 100644 index 000000000..9a3108820 --- /dev/null +++ b/frontend/src/components/Text/index.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from 'react'; +import { BaseText } from './style'; + +interface TextProps { + variant?: 'default' | 'pageTitle' | 'subTitle'; + ariaLabel?: string; + children: ReactNode; +} + +const Text = ({ variant = 'default', ariaLabel = '텍스트', children }: TextProps) => { + return ( + + {children} + + ); +}; + +export default Text; diff --git a/frontend/src/components/Text/style.ts b/frontend/src/components/Text/style.ts new file mode 100644 index 000000000..dc21db60e --- /dev/null +++ b/frontend/src/components/Text/style.ts @@ -0,0 +1,27 @@ +import { styled } from 'styled-components'; + +interface StyledTextProps { + $variant: 'default' | 'pageTitle' | 'subTitle'; +} + +const TYPE: Record> = { + default: { + fontSize: '16px', + fontWeight: '400', + }, + pageTitle: { + fontSize: '34px', + fontWeight: '1000', + }, + subTitle: { + fontSize: '24px', + fontWeight: '500', + }, +}; + +export const BaseText = styled.h1` + font-size: ${({ $variant }) => TYPE[$variant].fontSize}; + font-weight: ${({ $variant }) => TYPE[$variant].fontWeight}; + white-space: pre-line; + color: #222; +`; diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts new file mode 100644 index 000000000..a01c7f7f0 --- /dev/null +++ b/frontend/src/constants/index.ts @@ -0,0 +1,140 @@ +import { StampCountOption, RouterPath } from '../types'; + +export const REGEX = { + number: /^[0-9]+$/, +} as const; + +export const TEMPLATE_MENU = { + FRONT_IMAGE: '쿠폰(앞)', + BACK_IMAGE: '쿠폰(뒤)', + STAMP: '스탬프', +}; + +export const PHONE_NUMBER_LENGTH = 13; + +export const TEMPLATE_OPTIONS = [ + { + key: 'coupon-front', + value: TEMPLATE_MENU.FRONT_IMAGE, + }, + { + key: 'coupon-back', + value: TEMPLATE_MENU.BACK_IMAGE, + }, + { + key: 'stamp', + value: TEMPLATE_MENU.STAMP, + }, +]; + +export const CUSTOMERS_ORDER_OPTIONS = [ + { + key: 'stampCount', + value: '스탬프순', + }, + { + key: 'rewardCount', + value: '리워드순', + }, + { + key: 'visitCount', + value: '방문횟수순', + }, +]; + +export const STAMP_COUNT_OPTIONS: StampCountOption[] = [ + { + key: 'eight', + value: '8개', + }, + { + key: 'ten', + value: '10개', + }, + { + key: 'twelve', + value: '12개', + }, +]; + +export const EXPIRE_DATE_NONE = '없음'; + +export const EXPIRE_DATE_MAX = 1200; + +export const STAMP_COUNT_CUSTOM_OPTIONS = [ + { + key: 'eight', + value: '8개', + }, + { + key: 'nine', + value: '9개', + }, + { + key: 'ten', + value: '10개', + }, + { + key: 'eleven', + value: '11개', + }, + { + key: 'twelve', + value: '12개', + }, +]; + +export const EXPIRE_DATE_OPTIONS = [ + { + key: 'six-month', + value: '6개월', + }, + { + key: 'twelve-month', + value: '12개월', + }, + { + key: 'infinity', + value: EXPIRE_DATE_NONE, + }, +]; + +export const ROUTER_PATH: Record = { + customerList: '/admin', + adminLogin: '/admin/login', + adminAuth: '/admin/login/auth/kakao', + auth: '/login/auth/kakao', + adminSignup: '/admin/sign-up', + enterReward: '/admin/enter-reward', + enterStamp: '/admin/enter-stamp', + manageCafe: '/admin/manage-cafe', + modifyCouponPolicy: '/admin/modify-coupon-policy', + registerCafe: '/admin/register-cafe', + earnStamp: '/admin/earn-stamp', + selectCoupon: '/admin/select-coupon', + templateCouponDesign: '/template-coupon-design', + customCouponDesign: '/custom-coupon-design', + useReward: '/admin/use-reward', + couponList: '/', + login: '/login', + signup: '/sign-up', + myPage: '/my-page', + rewardList: '/reward-list', + rewardHistory: '/reward-history', + stampHistory: '/stamp-history', + inputPhoneNumber: '/input-phone-number', +} as const; + +export const PARAMS_ERROR_MESSAGE = '[ERROR] params를 지정해주세요.'; + +export const DATE_PARSE_OPTION = { + hasYear: false, + hasMonth: true, + hasDay: true, +}; + +export const INVALID_CAFE_ID = -1; + +export const MEGA_BYTE = 1024 ** 2; + +export const IMAGE_MAX_SIZE = 5 * MEGA_BYTE; diff --git a/frontend/src/context/index.tsx b/frontend/src/context/index.tsx new file mode 100644 index 000000000..611a748c1 --- /dev/null +++ b/frontend/src/context/index.tsx @@ -0,0 +1,20 @@ +import { createContext, PropsWithChildren, useState } from 'react'; + +const INVALID_CAFE_ID = -1; + +interface CafeContextValue { + cafeId: number; + setAdminCafeId: (id: number) => void; +} + +const CafeContext = createContext(null); + +export const CafeIdProvider = ({ children }: PropsWithChildren) => { + const [cafeId, setCafeId] = useState(INVALID_CAFE_ID); + + const setAdminCafeId = (id: number) => { + setCafeId(id); + }; + + return {children}; +}; diff --git a/frontend/src/custom.d.ts b/frontend/src/custom.d.ts new file mode 100644 index 000000000..ef008fe67 --- /dev/null +++ b/frontend/src/custom.d.ts @@ -0,0 +1,4 @@ +declare module '*.png'; +declare module '*.svg'; +declare module '*.ttf'; +declare module '*.gif'; diff --git a/frontend/src/hooks/useCustomerProfile.ts b/frontend/src/hooks/useCustomerProfile.ts new file mode 100644 index 000000000..1868e77a3 --- /dev/null +++ b/frontend/src/hooks/useCustomerProfile.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query'; +import { getCustomerProfile } from '../api/get'; + +export const useCustomerProfile = () => { + const { data: customerProfile, status } = useQuery({ + queryKey: ['customerProfile'], + queryFn: async () => await getCustomerProfile(), + staleTime: Infinity, + }); + + return { customerProfile, status }; +}; diff --git a/frontend/src/hooks/useDialPad.ts b/frontend/src/hooks/useDialPad.ts new file mode 100644 index 000000000..6f2406076 --- /dev/null +++ b/frontend/src/hooks/useDialPad.ts @@ -0,0 +1,97 @@ +import { PHONE_NUMBER_LENGTH, REGEX, ROUTER_PATH } from '../constants'; +import { ChangeEvent, KeyboardEvent, useRef, useState } from 'react'; +import { DialKeyType } from '../pages/Admin/EnterPhoneNumber/Dialpad'; +import { useLocation, useNavigate } from 'react-router-dom'; + +const addHypen = (phoneNumber: string) => { + return phoneNumber.length === 8 + ? phoneNumber.substring(0, 8) + '-' + phoneNumber.substring(8, phoneNumber.length) + : phoneNumber; +}; + +const useDialPad = () => { + const [phoneNumber, setPhoneNumber] = useState('010-'); + const phoneNumberRef = useRef(null); + const navigate = useNavigate(); + const location = useLocation(); + + const removeNumber = () => { + if (phoneNumber.length < 5) { + return; + } + if (phoneNumber.length === 9) { + setPhoneNumber((prev) => prev.substring(0, 7)); + return; + } + setPhoneNumber((prev) => prev.substring(0, prev.length - 1)); + }; + + const enter = () => { + if (phoneNumber.length !== PHONE_NUMBER_LENGTH) { + alert('올바른 전화번호를 입력해주세요.'); + return; + } + + const replacedPhoneNumber = phoneNumber.replaceAll('-', ''); + + if (location.pathname === ROUTER_PATH.enterStamp) { + navigate(ROUTER_PATH.selectCoupon, { state: { phoneNumber: replacedPhoneNumber } }); + return; + } + + if (location.pathname === ROUTER_PATH.enterReward) { + navigate(ROUTER_PATH.useReward, { state: { phoneNumber: replacedPhoneNumber } }); + return; + } + }; + + const handlePhoneNumber = (e: ChangeEvent) => { + if (e.target.value.length > 4 && e.target.value.endsWith('-')) + e.target.value = e.target.value.substring(0, e.target.value.length - 1); + + if (!REGEX.number.test(e.target.value.replaceAll('-', ''))) return; + + setPhoneNumber(addHypen(e.target.value)); + }; + + const handleBackspace = (e: KeyboardEvent) => { + if (!(e.target instanceof HTMLInputElement)) return; + + if (e.target.value.length < 5 && e.key === 'Backspace') { + e.preventDefault(); + return; + } + + if (e.target.value.length === 9 && e.key === 'Backspace') { + setPhoneNumber(e.target.value.substring(0, 8)); + return; + } + }; + + const pressPad = (dialKey: DialKeyType) => () => { + if (phoneNumberRef.current) phoneNumberRef.current.focus(); + + if (dialKey === '←') { + removeNumber(); + return; + } + if (dialKey === '입력') { + enter(); + return; + } + + if (phoneNumber.length > 12) return; + + setPhoneNumber((prev) => addHypen(prev + dialKey)); + }; + + return { + phoneNumber, + phoneNumberRef, + handlePhoneNumber, + handleBackspace, + pressPad, + }; +}; + +export default useDialPad; diff --git a/frontend/src/hooks/useFindAddress.ts b/frontend/src/hooks/useFindAddress.ts new file mode 100644 index 000000000..93ff5fda9 --- /dev/null +++ b/frontend/src/hooks/useFindAddress.ts @@ -0,0 +1,33 @@ +import { Address, useDaumPostcodePopup } from 'react-daum-postcode'; +import { Dispatch, SetStateAction } from 'react'; + +const useFindAddress = (setRoadAddress: Dispatch>) => { + const findAddress = (data: Address) => { + let fullAddress = data.address; + let extraAddress = ''; + + if (data.addressType === 'R') { + if (data.bname !== '') { + extraAddress += data.bname; + } + if (data.buildingName !== '') { + extraAddress += extraAddress !== '' ? `, ${data.buildingName}` : data.buildingName; + } + fullAddress += extraAddress !== '' ? ` (${extraAddress})` : ''; + } + + setRoadAddress(fullAddress); + }; + + const openPostcodePopup = useDaumPostcodePopup(); + + const openAddressPopup = () => { + openPostcodePopup({ onComplete: findAddress }); + }; + + return { + openAddressPopup, + }; +}; + +export default useFindAddress; diff --git a/frontend/src/hooks/useModal.ts b/frontend/src/hooks/useModal.ts new file mode 100644 index 000000000..60dbd704f --- /dev/null +++ b/frontend/src/hooks/useModal.ts @@ -0,0 +1,23 @@ +import { useCallback, useState } from 'react'; + +const useModal = () => { + const [isOpen, setOpen] = useState(false); + + const openModal = useCallback(() => { + setOpen(true); + document.body.style.overflow = 'hidden'; + }, []); + + const closeModal = useCallback(() => { + setOpen(false); + document.body.style.overflow = 'unset'; + }, []); + + return { + isOpen, + openModal, + closeModal, + }; +}; + +export default useModal; diff --git a/frontend/src/hooks/useRedirectRegisterPage.ts b/frontend/src/hooks/useRedirectRegisterPage.ts new file mode 100644 index 000000000..3a81185ca --- /dev/null +++ b/frontend/src/hooks/useRedirectRegisterPage.ts @@ -0,0 +1,35 @@ +import { useQuery } from '@tanstack/react-query'; +import { getCafe } from '../api/get'; +import { useNavigate } from 'react-router-dom'; +import { INVALID_CAFE_ID } from '../constants'; + +export const useCafeQuery = () => { + const result = useQuery({ + queryKey: ['cafe'], + queryFn: async () => await getCafe(), + staleTime: Infinity, + }); + + return result; +}; + +export const useCafeId = () => { + const { status, data } = useCafeQuery(); + let cafeId = INVALID_CAFE_ID; + if (status === 'success' && data.cafes.length !== 0) { + cafeId = data.cafes[0].id; + } + return { status, cafeId }; +}; + +export const useRedirectRegisterPage = () => { + const navigate = useNavigate(); + const { status, cafeId } = useCafeId(); + + if (cafeId === INVALID_CAFE_ID && status === 'success') { + alert('카페 등록 후 사용해주세요.'); + navigate('/admin/register-cafe'); + } + + return cafeId; +}; diff --git a/frontend/src/hooks/useUploadImage.ts b/frontend/src/hooks/useUploadImage.ts new file mode 100644 index 000000000..b5e1e2ad4 --- /dev/null +++ b/frontend/src/hooks/useUploadImage.ts @@ -0,0 +1,32 @@ +import { useMutation } from '@tanstack/react-query'; +import { ChangeEvent, useState } from 'react'; +import { postUploadImage } from '../api/post'; +import { ImageUploadRes } from '../types/api'; +import { isLargeThanBoundarySize } from '../utils'; + +const useUploadImage = (initImgUrl = '') => { + const [imgFileUrl, setImgFileUrl] = useState(initImgUrl); + const { mutate } = useMutation({ + mutationFn: (imageFile: File) => postUploadImage(imageFile), + onSuccess: async (res) => { + const body = (await res.json()) as ImageUploadRes; + setImgFileUrl(body.imageUrl); + }, + onError: () => { + alert('파일을 업로드하는데 실패하였습니다.'); + }, + }); + const uploadImageFile = (e: ChangeEvent) => { + if (!e.target.files) return; + const uploadedImage = e.target.files[0]; + if (isLargeThanBoundarySize(uploadedImage.size)) { + alert('파일은 5MB 미만 이어야 합니다.'); + return; + } + mutate(uploadedImage); + }; + + return [imgFileUrl, uploadImageFile, setImgFileUrl] as const; +}; + +export default useUploadImage; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx new file mode 100644 index 000000000..4767fff97 --- /dev/null +++ b/frontend/src/index.tsx @@ -0,0 +1,10 @@ +import App from './App'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); +root.render( + + + , +); diff --git a/frontend/src/mocks/browser.ts b/frontend/src/mocks/browser.ts new file mode 100644 index 000000000..750e031c2 --- /dev/null +++ b/frontend/src/mocks/browser.ts @@ -0,0 +1,4 @@ +import { setupWorker } from 'msw'; +import { handlers } from './handlers'; + +export const worker = setupWorker(...handlers); diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts new file mode 100644 index 000000000..db14c2ec1 --- /dev/null +++ b/frontend/src/mocks/handlers.ts @@ -0,0 +1,338 @@ +import { rest } from 'msw'; +import { + cafes, + customers, + rewards, + samples10, + samples12, + samples8, + mockCoupons, + cafeCustomer, + customerCoupons, + usedCustomerRewards, + customerRewards, + stampHistorys, +} from './mockData'; + +const customerList = [...customers]; +const coupons = [...mockCoupons]; + +export const handlers = [ + // 내 카페 조회 + rest.get('/admin/cafes', (req, res, ctx) => { + return res(ctx.status(200), ctx.json(cafes)); + }), + + // 카페 관리 + rest.patch('/admin/cafes/:cafeId', async (req, res, ctx) => { + return res(ctx.status(200)); + }), + + // 카페 등록 + rest.post('/admin/cafes', async (req, res, ctx) => { + const { businessRegistrationNumber, name, roadAddress, detailAddress } = await req.json(); + + if (!businessRegistrationNumber || !name || !roadAddress || !detailAddress) { + return res(ctx.status(400)); + } + return res(ctx.status(201), ctx.set('Location', '/cafes/1')); + }), + + // 스탬프 개수별로 기본 샘플 조회 + rest.get('/admin/coupon-samples', (req, res, ctx) => { + const maxStampCountQueryParam = req.url.searchParams.get('max-stamp-count'); + + if (!maxStampCountQueryParam) return res(ctx.status(400)); + + const maxStampCount = +maxStampCountQueryParam; + + if (maxStampCount === 8) { + return res(ctx.status(200), ctx.json(samples8)); + } + if (maxStampCount === 10) { + return res(ctx.status(200), ctx.json(samples10)); + } + if (maxStampCount === 12) { + return res(ctx.status(200), ctx.json(samples12)); + } + }), + + // 쿠폰 디자인 및 정책 수정 + rest.post('/admin/coupon-setting', async (req, res, ctx) => { + // TODO: cafe id 핸들링 + const cafeIdParam = req.url.searchParams.get('cafe-id'); + const { frontImageUrl, backImageUrl, stampImageUrl, coordinates, reward, expirePeriod } = + await req.json(); + + if ( + !frontImageUrl || + !backImageUrl || + !stampImageUrl || + !coordinates || + !reward || + !expirePeriod + ) { + return res(ctx.status(400)); + } + + return res(ctx.status(204)); + }), + + // 전화번호로 고객 조회 + rest.get('/admin/customers', (req, res, ctx) => { + const phoneNumberQueryParam = req.url.searchParams.get('phone-number'); + const findUserResult = customerList.find( + (customer) => customer.phoneNumber === phoneNumberQueryParam, + ); + + if (!findUserResult) { + return res(ctx.status(200), ctx.json({ customer: [] })); + } + + return res( + ctx.status(200), + ctx.json({ + customer: [findUserResult], + }), + ); + }), + + // 임시 가입 고객 생성 + rest.post('/admin/temporary-customers', async (req, res, ctx) => { + const body = await req.json(); + const createdCustomer = { + id: Math.floor(Math.random() * 1000 + 29), + nickname: '레고(임시회원, 신규)', + phoneNumber: body.phoneNumber, + }; + + customerList.push(createdCustomer); + return res(ctx.status(201), ctx.set('Location', `customers/${createdCustomer.id}`)); + }), + + // 고객의 쿠폰 조회 + rest.get('/admin/customers/:customerId/coupons', (req, res, ctx) => { + const cafeIdQueryParam = req.url.searchParams.get('cafe-id'); + const activeQueryParam = req.url.searchParams.get('active'); + const { customerId } = req.params; + + if (!cafeIdQueryParam || !activeQueryParam || !customerId) { + return res(ctx.status(400)); + } + const findCouponsResult = coupons.find( + (coupon) => coupon.customerId === +customerId && activeQueryParam === 'true', + ); + + if (+cafeIdQueryParam !== 1) { + return res(ctx.status(400)); + } + + if (!findCouponsResult) { + return res(ctx.status(200), ctx.json({ coupons: [] })); + } + + return res(ctx.status(200), ctx.json({ coupons: [findCouponsResult] })); + }), + + // 고객의 리워드 조회 + rest.get('/admin/customers/:customerId/rewards', (req, res, ctx) => { + const usedQueryParam = req.url.searchParams.get('used'); + const cafeIdQueryParam = req.url.searchParams.get('cafe-id'); + const { customerId } = req.params; + + if (!usedQueryParam || !cafeIdQueryParam || !customerId) { + return res(ctx.status(400)); + } + + if (+customerId !== 1) return res(ctx.status(200), ctx.json({ rewards: [] })); + return res(ctx.status(200), ctx.json({ rewards })); + }), + + // 쿠폰 신규 발급 + rest.post('/admin/customers/:customerId/coupons', async (req, res, ctx) => { + const { customerId } = req.params; + + const customerIdNum = +customerId; + const coupon = { + id: Math.floor(Math.random() * 1000 + 23), + customerId: customerIdNum, + nickname: '윤생', + stampCount: 1, + expireDate: '2023:08:11', + isPrevious: 'false', + maxStampCount: 10, + }; + + coupons.push(coupon); + + return res(ctx.status(201), ctx.json({ couponId: coupon.id })); + }), + + // 스탬프 적립 + rest.post('/admin/customers/:customerId/coupons/:couponId/stamps', async (req, res, ctx) => { + const { customerId, couponId } = req.params; + const { earningStampCount } = await req.json(); + const findCustomer = coupons.find( + (coupon) => +customerId === coupon.customerId && +couponId === coupon.id, + ); + + if (!findCustomer) { + return res(ctx.status(400)); + } + + findCustomer.stampCount += +earningStampCount; + + return res(ctx.status(201)); + }), + + // 카페 사장이 고객 목록 조회 가능 + rest.get('/admin/cafes/:cafeId/customers', (req, res, ctx) => { + const { cafeId } = req.params; + + if (!cafeId) { + return res(ctx.status(400)); + } + if (+cafeId !== 1) { + return res(ctx.status(200), ctx.json({ customers: [] })); + } + + return res(ctx.status(200), ctx.json({ customers: cafeCustomer })); + }), + + // 리워드 사용 + rest.patch('/admin/customers/:customerId/rewards/:rewardId', async (req, res, ctx) => { + const { customerId, rewardId } = req.params; + const { cafeId, used } = await req.json(); + + if (!customerId || !rewardId || !cafeId || !used) { + return res(ctx.status(400)); + } + + return res(ctx.status(200)); + }), + + rest.get('/admin/coupon-setting', async (req, res, ctx) => { + const cafeIdParam = req.url.searchParams.get('cafe-id'); + const coupon = { + frontImageUrl: 'https://drive.google.com/uc?export=view&id=1J6HcagcK65D6_i0bDQ7llbvdCnCOkJ7h', + backImageUrl: + 'https://wemix-dev-s3.s3.amazonaws.com/media/sample/%EC%BF%A0%ED%8F%B0(%EB%AA%85%ED%95%A8)/2019/NC236B.jpg', + stampImageUrl: + 'https://blog.kakaocdn.net/dn/Idhl1/btqDj3EXl1n/Q8AkpYkKmc3wkAyXJZX3g0/img.png', + coordinates: [ + { + order: 1, + xCoordinate: 37, + yCoordinate: 50, + }, + { + order: 2, + xCoordinate: 86, + yCoordinate: 50, + }, + { + order: 3, + xCoordinate: 134, + yCoordinate: 50, + }, + { + order: 4, + xCoordinate: 182, + yCoordinate: 50, + }, + { + order: 5, + xCoordinate: 233, + yCoordinate: 50, + }, + { + order: 6, + xCoordinate: 37, + yCoordinate: 100, + }, + { + order: 7, + xCoordinate: 86, + yCoordinate: 100, + }, + { + order: 8, + xCoordinate: 134, + yCoordinate: 100, + }, + { + order: 9, + xCoordinate: 182, + yCoordinate: 100, + }, + { + order: 10, + xCoordinate: 233, + yCoordinate: 100, + }, + ], + }; + return res(ctx.status(200), ctx.json(coupon)); + }), + + /** ----------------- 아래부터는 고객모드의 api 입니다. ----------------- */ + + // 쿠폰 리스트 + rest.get('/coupons', (req, res, ctx) => { + return res(ctx.status(200), ctx.json(customerCoupons)); + }), + + // 카페 정보 조회 + rest.get('/cafes/:cafeId', (req, res, ctx) => { + const cafe = { + cafe: { + id: 1, + name: '우아한 카페', + introduction: + '이 편지는 영국에서 최초로 시작되어 일년에 한바퀴를 돌면서 받는 사람에게 행운을 주었고 지금은 당신에게로 옮겨진 이 편지는 4일 안에 당신 곁을 ...', + openTime: '10:00', + closeTime: '18:00', + telephoneNumber: '01012345678', + cafeImageUrl: 'https://picsum.photos/540/900', + roadAddress: '서울시 송파구', + detailAddress: '루터회관', + }, + }; + + return res(ctx.status(200), ctx.json(cafe)); + }), + + // 쿠폰 즐겨찾기 등록, 해제 + rest.post('/cafes/:cafeId/favorites', async (req, res, ctx) => { + return res(ctx.status(200)); + }), + + // 사용가능한 리워드 조회 + rest.get('/rewards', (req, res, ctx) => { + const used = req.url.searchParams.get('used'); + + if (used) return res(ctx.status(200), ctx.json(usedCustomerRewards)); + return res(ctx.status(200), ctx.json(customerRewards)); + }), + + // 스탬프 조회 + rest.get('/stamp-history', (req, res, ctx) => { + return res(ctx.status(200), ctx.json(stampHistorys)); + }), + + // 쿠폰 삭제 + rest.delete('/coupons/:couponId', async (req, res, ctx) => { + return res(ctx.status(204)); + }), + + rest.get('/admin/login', (req, res, ctx) => { + localStorage.setItem('admin-login-token', 'regorego'); + }), + + rest.get('/profiles', (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ profile: { id: 10, nickname: '강영민', phoneNumber: null, email: null } }), + ); + }), +]; diff --git a/frontend/src/mocks/mockData.ts b/frontend/src/mocks/mockData.ts new file mode 100644 index 000000000..fc5cc12c6 --- /dev/null +++ b/frontend/src/mocks/mockData.ts @@ -0,0 +1,644 @@ +export const cafes = { + cafes: [ + { + id: 1, + name: '윤생까페', + openTime: '09:00', + closeTime: '18:00', + telephoneNumber: '0212345678', + cafeImageUrl: null, + roadAddress: '서울시 송파구', + detailAddress: '루터회관', + businessRegistrationNumber: '00-000-00000', + introduction: '안녕하세요.', + }, + ], +}; + +export const samples8 = { + sampleFrontImages: [ + { + id: 1, + imageUrl: 'https://picsum.photos/270/150', + }, + { + id: 2, + imageUrl: 'https://drive.google.com/uc?export=view&id=1J6HcagcK65D6_i0bDQ7llbvdCnCOkJ7h', + }, + { + id: 3, + imageUrl: 'https://picsum.photos/270/150', + }, + { + id: 4, + imageUrl: 'https://drive.google.com/uc?export=view&id=1J6HcagcK65D6_i0bDQ7llbvdCnCOkJ7h', + }, + { + id: 5, + imageUrl: 'https://picsum.photos/270/150', + }, + { + id: 6, + imageUrl: 'https://drive.google.com/uc?export=view&id=1J6HcagcK65D6_i0bDQ7llbvdCnCOkJ7h', + }, + ], + sampleBackImages: [ + { + id: 1, + imageUrl: 'https://picsum.photos/270/150', + stampCoordinates: [ + { + order: 1, + xCoordinate: 37, + yCoordinate: 50, + }, + { + order: 2, + xCoordinate: 86, + yCoordinate: 50, + }, + { + order: 3, + xCoordinate: 134, + yCoordinate: 50, + }, + { + order: 4, + xCoordinate: 182, + yCoordinate: 50, + }, + { + order: 5, + xCoordinate: 233, + yCoordinate: 50, + }, + { + order: 6, + xCoordinate: 37, + yCoordinate: 100, + }, + { + order: 7, + xCoordinate: 86, + yCoordinate: 100, + }, + { + order: 8, + xCoordinate: 134, + yCoordinate: 100, + }, + ], + }, + ], + sampleStampImages: [ + { + id: 1, + imageUrl: 'https://picsum.photos/50', + }, + { + id: 2, + imageUrl: 'https://drive.google.com/uc?export=view&id=1KVBztQdUCpvp8usHUbIbSBYvQManm6eN', + }, + ], +}; + +export const samples10 = { + sampleFrontImages: [ + { + id: 1, + imageUrl: 'https://picsum.photos/270/150', + }, + { + id: 2, + imageUrl: 'https://drive.google.com/uc?export=view&id=1J6HcagcK65D6_i0bDQ7llbvdCnCOkJ7h', + }, + ], + sampleBackImages: [ + { + id: 2, + imageUrl: 'https://picsum.photos/270/150', + stampCoordinates: [ + { + order: 1, + xCoordinate: 37, + yCoordinate: 50, + }, + { + order: 2, + xCoordinate: 86, + yCoordinate: 50, + }, + { + order: 3, + xCoordinate: 134, + yCoordinate: 50, + }, + { + order: 4, + xCoordinate: 182, + yCoordinate: 50, + }, + { + order: 5, + xCoordinate: 233, + yCoordinate: 50, + }, + { + order: 6, + xCoordinate: 37, + yCoordinate: 100, + }, + { + order: 7, + xCoordinate: 86, + yCoordinate: 100, + }, + { + order: 8, + xCoordinate: 134, + yCoordinate: 100, + }, + { + order: 9, + xCoordinate: 182, + yCoordinate: 100, + }, + { + order: 10, + xCoordinate: 233, + yCoordinate: 100, + }, + ], + }, + ], + sampleStampImages: [ + { + id: 1, + imageUrl: 'https://picsum.photos/50', + }, + { + id: 2, + imageUrl: 'https://drive.google.com/uc?export=view&id=1KVBztQdUCpvp8usHUbIbSBYvQManm6eN', + }, + ], +}; + +export const samples12 = { + sampleFrontImages: [ + { + id: 1, + imageUrl: 'https://picsum.photos/270/150', + }, + ], + + sampleBackImages: [ + { + id: 1, + imageUrl: 'https://picsum.photos/270/150', + stampCoordinates: [], + }, + ], + sampleStampImages: [ + { + id: 1, + imageUrl: 'https://picsum.photos/270/150', + }, + ], +}; + +interface MockCustomer { + id: number; + nickname: string; + phoneNumber: string; +} + +export const customers: MockCustomer[] = [ + { id: 1, nickname: '윤생', phoneNumber: '01011112222' }, + { id: 2, nickname: '라잇', phoneNumber: '01033334444' }, + { id: 3, nickname: '레고', phoneNumber: '01055556666' }, +]; + +export const mockCoupons = [ + { + id: 1, + customerId: 1, + nickname: '윤생', + stampCount: 3, + expireDate: '2023:08:11', + isPrevious: 'false', + maxStampCount: 10, + }, +]; + +export const rewards = [ + { + id: 1, + name: '아메리카노', + }, + { + id: 2, + name: '조각케익', + }, +]; + +// TODO: 리워드를 만들기 위한 스탬프 갯수가 없음 +export const cafeCustomer = [ + { + id: 1, + nickname: '윤생1234', + stampCount: 4, + rewardCount: 3, + visitCount: 10, + firstVisitDate: '23:07:18', + isRegistered: true, + maxStampCount: 10, + }, + { + id: 2, + nickname: '레고밟은한우', + stampCount: 1, + rewardCount: 0, + visitCount: 12, + firstVisitDate: '23:06:22', + isRegistered: false, + maxStampCount: 10, + }, + { + id: 3, + nickname: '라잇', + stampCount: 8, + rewardCount: 6, + visitCount: 8, + firstVisitDate: '23:01:10', + isRegistered: true, + maxStampCount: 8, + }, + { + id: 4, + nickname: '레고밟은한우', + stampCount: 1, + rewardCount: 0, + visitCount: 12, + firstVisitDate: '23:06:22', + isRegistered: false, + maxStampCount: 10, + }, + { + id: 5, + nickname: '라잇', + stampCount: 8, + rewardCount: 6, + visitCount: 8, + firstVisitDate: '23:01:10', + isRegistered: true, + maxStampCount: 8, + }, + { + id: 6, + nickname: '레고밟은한우', + stampCount: 1, + rewardCount: 0, + visitCount: 12, + firstVisitDate: '23:06:22', + isRegistered: false, + maxStampCount: 10, + }, + { + id: 7, + nickname: '라잇', + stampCount: 8, + rewardCount: 6, + visitCount: 8, + firstVisitDate: '23:01:10', + isRegistered: true, + maxStampCount: 8, + }, +]; + +export const customerCoupons = { + coupons: [ + { + cafeInfo: { + id: 1, + name: '깃짱카페', + isFavorites: true, + }, + couponInfos: [ + { + id: 1, + status: 'ACCUMULATING', + stampCount: 1, + maxStampCount: 8, + rewardName: '아메리카노', + frontImageUrl: + 'https://drive.google.com/uc?export=view&id=1_3XRlwig5m846bBUzUv-VqcOxN1PTyPY', + backImageUrl: + 'https://wemix-dev-s3.s3.amazonaws.com/media/sample/%EC%BF%A0%ED%8F%B0(%EB%AA%85%ED%95%A8)/2019/NC236B.jpg', + stampImageUrl: + 'https://blog.kakaocdn.net/dn/Idhl1/btqDj3EXl1n/Q8AkpYkKmc3wkAyXJZX3g0/img.png', + coordinates: [ + { + order: 1, + xCoordinate: 20, + yCoordinate: 35, + }, + { + order: 2, + xCoordinate: 70, + yCoordinate: 35, + }, + ], + }, + ], + }, + { + cafeInfo: { + id: 2, + name: '하디카페', + isFavorites: true, + }, + couponInfos: [ + { + id: 2, + status: 'ACCUMULATING', + stampCount: 3, + maxStampCount: 8, + rewardName: '아메리카노', + frontImageUrl: + 'https://drive.google.com/uc?export=view&id=1hdTvv_yBFdpyDpJWrNMMy9JlBKVNNy7D', + backImageUrl: + 'https://wemix-dev-s3.s3.amazonaws.com/media/sample/%EC%BF%A0%ED%8F%B0(%EB%AA%85%ED%95%A8)/2019/NC241B.jpg', + stampImageUrl: + 'https://w7.pngwing.com/pngs/608/604/png-transparent-rubber-stamp-free-miscellaneous-freight-transport-text.png', + coordinates: [ + { + order: 1, + xCoordinate: 2, + yCoordinate: 5, + }, + { + order: 2, + xCoordinate: 5, + yCoordinate: 5, + }, + ], + }, + ], + }, + { + cafeInfo: { + id: 3, + name: '제나카페', + isFavorites: true, + }, + couponInfos: [ + { + id: 3, + + status: 'ACCUMULATING', + stampCount: 7, + maxStampCount: 8, + rewardName: '아메리카노', + frontImageUrl: + 'https://drive.google.com/uc?export=view&id=1Rn4Gb2vE5eKnPL8SrLwlbv1jgGzy6AWE', + backImageUrl: + 'https://wemix-dev-s3.s3.amazonaws.com/media/sample/%EC%BF%A0%ED%8F%B0(%EB%AA%85%ED%95%A8)/2019/NC240B.jpg', + stampImageUrl: + 'https://w7.pngwing.com/pngs/608/604/png-transparent-rubber-stamp-free-miscellaneous-freight-transport-text.png', + coordinates: [ + { + order: 1, + xCoordinate: 2, + yCoordinate: 5, + }, + { + order: 2, + xCoordinate: 5, + yCoordinate: 5, + }, + ], + }, + ], + }, + { + cafeInfo: { + id: 4, + name: '레고카페', + isFavorites: true, + }, + couponInfos: [ + { + id: 4, + status: 'ACCUMULATING', + stampCount: 2, + maxStampCount: 10, + rewardName: '자바칩 프라푸치노 한 잔', + frontImageUrl: + 'https://drive.google.com/uc?export=view&id=1kklV1yLgmqjdQtBPXt4PLhwfrVAP00S5', + backImageUrl: + 'https://wemix-dev-s3.s3.amazonaws.com/media/sample/%EC%BF%A0%ED%8F%B0(%EB%AA%85%ED%95%A8)/2023/NC209B.jpg', + stampImageUrl: + 'https://w7.pngwing.com/pngs/608/604/png-transparent-rubber-stamp-free-miscellaneous-freight-transport-text.png', + coordinates: [ + { + order: 1, + xCoordinate: 30, + yCoordinate: 48, + }, + { + order: 2, + xCoordinate: 75, + yCoordinate: 53, + }, + ], + }, + ], + }, + { + cafeInfo: { + id: 5, + name: '라잇카페', + isFavorites: true, + }, + couponInfos: [ + { + id: 5, + + status: 'ACCUMULATING', + stampCount: 3, + maxStampCount: 8, + rewardName: '아메리카노', + frontImageUrl: + 'https://drive.google.com/uc?export=view&id=1Jm0UYUrbkXWLhP6GxrXTTWzcI-zREWyF', + backImageUrl: + 'https://wemix-dev-s3.s3.amazonaws.com/media/sample/%EC%BF%A0%ED%8F%B0(%EB%AA%85%ED%95%A8)/2019/NC236B.jpg', + stampImageUrl: + 'https://blog.kakaocdn.net/dn/Idhl1/btqDj3EXl1n/Q8AkpYkKmc3wkAyXJZX3g0/img.png', + coordinates: [ + { + order: 1, + xCoordinate: 2, + yCoordinate: 5, + }, + { + order: 2, + xCoordinate: 5, + yCoordinate: 5, + }, + ], + }, + ], + }, + { + cafeInfo: { + id: 6, + name: '윤생카페', + isFavorites: true, + }, + couponInfos: [ + { + id: 6, + status: 'ACCUMULATING', + stampCount: 3, + maxStampCount: 8, + rewardName: '아메리카노', + frontImageUrl: + 'https://drive.google.com/uc?export=view&id=1ngMdF1isvQlhsZfBI0VNp5VMsGQZ9cgb', + backImageUrl: 'https://source.unsplash.com/random', + stampImageUrl: 'https://source.unsplash.com/random', + coordinates: [ + { + order: 1, + xCoordinate: 2, + yCoordinate: 5, + }, + { + order: 2, + xCoordinate: 5, + yCoordinate: 5, + }, + ], + }, + ], + }, + { + cafeInfo: { + id: 7, + name: '레오카페', + isFavorites: true, + }, + couponInfos: [ + { + id: 7, + + status: 'ACCUMULATING', + stampCount: 3, + maxStampCount: 8, + rewardName: '아메리카노', + frontImageUrl: + 'https://drive.google.com/uc?export=view&id=1J6HcagcK65D6_i0bDQ7llbvdCnCOkJ7h', + backImageUrl: 'https://source.unsplash.com/random', + stampImageUrl: 'https://source.unsplash.com/random', + coordinates: [ + { + order: 1, + xCoordinate: 35, + yCoordinate: 45, + }, + { + order: 2, + xCoordinate: 80, + yCoordinate: 40, + }, + { + order: 3, + xCoordinate: 130, + yCoordinate: 45, + }, + ], + }, + ], + }, + ], +}; + +export const customerRewards = { + rewards: [ + { + id: 1, + rewardName: '아메리카노', + cafeName: '우아한카페', + createdAt: '2023:08:06', + usedAt: null, + }, + { + id: 2, + rewardName: '블록쿠키', + cafeName: '레고카페', + createdAt: '2023:08:04', + usedAt: null, + }, + ], +}; + +export const usedCustomerRewards = { + rewards: [ + { + id: 1, + rewardName: '아메리카노', + cafeName: '라잇카페', + createdAt: '2023:08:05', + usedAt: '2023:08:07', + }, + { + id: 2, + rewardName: '마들렌', + cafeName: '윤생카페', + createdAt: '2023:08:03', + usedAt: '2023:08:07', + }, + { + id: 2, + rewardName: '케이크', + cafeName: '레고카페', + createdAt: '2023:08:01', + usedAt: '2023:08:02', + }, + { + id: 2, + rewardName: '카페라떼', + cafeName: '깃짱카페', + createdAt: '2023:08:01', + usedAt: '2023:08:02', + }, + ], +}; + +export const stampHistorys = { + stampHistorys: [ + { + id: 1, + cafeName: '우아한 카페', + stampCount: 3, + createdAt: '2023:08:12 18:00:00', + }, + { + id: 2, + cafeName: '레오 카페', + stampCount: 2, + createdAt: '2023:08:11 18:10:00', + }, + { + id: 1, + cafeName: '하디 카페', + stampCount: 1, + createdAt: '2023:08:12 18:17:00', + }, + { + id: 1, + cafeName: '우아한 카페', + stampCount: 1, + createdAt: '2023:08:11 18:00:00', + }, + ], +}; diff --git a/frontend/src/pages/Admin/AdminAuth/index.tsx b/frontend/src/pages/Admin/AdminAuth/index.tsx new file mode 100644 index 000000000..5aebb38b0 --- /dev/null +++ b/frontend/src/pages/Admin/AdminAuth/index.tsx @@ -0,0 +1,32 @@ +import { useEffect } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { getAdminOAuthToken } from '../../../api/get'; +import { ROUTER_PATH } from '../../../constants'; + +const AdminAuth = () => { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const code = searchParams.get('code'); + + if (!code) { + throw new Error('code가 없습니다.'); + } + + const getToken = async () => { + const response = await getAdminOAuthToken({ + params: { resourceServer: 'kakao', code: code }, + }); + + localStorage.setItem('admin-login-token', response.accessToken); + + navigate(ROUTER_PATH.customerList); + }; + + useEffect(() => { + getToken(); + }, []); + + return <>; +}; + +export default AdminAuth; diff --git a/frontend/src/pages/Admin/AdminLogin/index.tsx b/frontend/src/pages/Admin/AdminLogin/index.tsx new file mode 100644 index 000000000..88047cfaf --- /dev/null +++ b/frontend/src/pages/Admin/AdminLogin/index.tsx @@ -0,0 +1,17 @@ +import { LoginLogo, CustomerKakaoLoginButton } from '../../../assets'; +import { Container, KakaoLoginImg, LogoImg, NaverLoginLink } from './style'; + +const AdminLogin = () => { + const KAKAO_LOGIN_PAGE_URL = `${process.env.REACT_APP_BASE_URL}/admin/login/kakao`; + + return ( + + + + + + + ); +}; + +export default AdminLogin; diff --git a/frontend/src/pages/Admin/AdminLogin/style.tsx b/frontend/src/pages/Admin/AdminLogin/style.tsx new file mode 100644 index 000000000..07c9c87c1 --- /dev/null +++ b/frontend/src/pages/Admin/AdminLogin/style.tsx @@ -0,0 +1,24 @@ +import { styled } from 'styled-components'; + +export const Container = styled.div` + display: flex; + flex-direction: column; + width: 100vw; + height: 100vh; + align-items: center; + justify-content: center; +`; + +export const LogoImg = styled.img` + width: 300px; + height: 150px; +`; + +export const NaverLoginLink = styled.a` + margin-top: 40px; + background: transparent; +`; + +export const KakaoLoginImg = styled.img` + width: 200px; +`; diff --git a/frontend/src/pages/Admin/CouponDesign/ChoiceTemplate/index.tsx b/frontend/src/pages/Admin/CouponDesign/ChoiceTemplate/index.tsx new file mode 100644 index 000000000..e7c8a2533 --- /dev/null +++ b/frontend/src/pages/Admin/CouponDesign/ChoiceTemplate/index.tsx @@ -0,0 +1,128 @@ +import { Dispatch, SetStateAction, useState } from 'react'; +import { ChoiceTemplateContainer, SampleImg, SampleImageContainer } from './style'; +import TabBar from '../../../../components/TabBar'; +import { TEMPLATE_MENU, TEMPLATE_OPTIONS } from '../../../../constants'; +import { useQuery } from '@tanstack/react-query'; +import { useLocation } from 'react-router-dom'; +import { getCouponSamples } from '../../../../api/get'; +import { parseStampCount } from '../../../../utils'; +import { SampleBackCouponImage, SampleImage, StampCoordinate } from '../../../../types'; +import { SampleCouponRes } from '../../../../types/api'; + +interface ChoiceTemplateProps { + frontImageUrl: string; + backImageUrl: string; + stampImageUrl: string; + setFrontImageUrl: Dispatch>; + setBackImageUrl: Dispatch>; + setStampImageUrl: Dispatch>; + setStampCoordinates: Dispatch>; +} + +const ChoiceTemplate = ({ + frontImageUrl, + backImageUrl, + stampImageUrl, + setFrontImageUrl, + setBackImageUrl, + setStampImageUrl, + setStampCoordinates, +}: ChoiceTemplateProps) => { + const location = useLocation(); + const [templateSelect, setTemplateSelect] = useState(TEMPLATE_MENU.FRONT_IMAGE); + const [selectedImageUrl, setSelectedImageUrl] = useState(''); + const maxStampCount = parseStampCount(location.state.stampCount); + + const { data: sampleImages, status } = useQuery( + ['coupon-samples', maxStampCount], + () => getCouponSamples({ params: { maxStampCount } }), + { + staleTime: Infinity, + }, + ); + + if (status === 'loading') return
페이지 로딩중..
; + if (status === 'error') return
이미지를 불러오는데 실패했습니다. 새로고침 해주세요.
; + + // TODO: 네이밍 변경 + const getImageFromData = (templateSelected: string): SampleImage[] | SampleBackCouponImage[] => { + switch (templateSelected) { + case TEMPLATE_MENU.FRONT_IMAGE: + return sampleImages.sampleFrontImages; + case TEMPLATE_MENU.BACK_IMAGE: + return sampleImages.sampleBackImages; + case TEMPLATE_MENU.STAMP: + return sampleImages.sampleStampImages; + default: + return []; + } + }; + + const selectTabBar = (e: React.ChangeEvent) => { + setTemplateSelect(e.target.value); + switch (e.target.value) { + case TEMPLATE_MENU.FRONT_IMAGE: + setSelectedImageUrl(frontImageUrl); + break; + case TEMPLATE_MENU.BACK_IMAGE: + setSelectedImageUrl(backImageUrl); + break; + case TEMPLATE_MENU.STAMP: + setSelectedImageUrl(stampImageUrl); + break; + default: + break; + } + }; + + const selectSampleImage = (imageUrl: string, coordinates?: StampCoordinate[]) => { + switch (templateSelect) { + case TEMPLATE_MENU.FRONT_IMAGE: + setFrontImageUrl(imageUrl); + break; + case TEMPLATE_MENU.BACK_IMAGE: + if (!coordinates) return; + setBackImageUrl(imageUrl); + setStampCoordinates([...coordinates]); + break; + case TEMPLATE_MENU.STAMP: + setStampImageUrl(imageUrl); + break; + default: + break; + } + setSelectedImageUrl(imageUrl); + }; + + return ( + + + + {getImageFromData(templateSelect).map((element) => ( + { + if ('stampCoordinates' in element) { + selectSampleImage(element.imageUrl, element.stampCoordinates); + } else { + selectSampleImage(element.imageUrl); + } + }} + /> + ))} + + + ); +}; + +export default ChoiceTemplate; diff --git a/frontend/src/pages/Admin/CouponDesign/ChoiceTemplate/style.tsx b/frontend/src/pages/Admin/CouponDesign/ChoiceTemplate/style.tsx new file mode 100644 index 000000000..6f1b56a04 --- /dev/null +++ b/frontend/src/pages/Admin/CouponDesign/ChoiceTemplate/style.tsx @@ -0,0 +1,26 @@ +import { styled } from 'styled-components'; + +export const ChoiceTemplateContainer = styled.div` + border-left: 1px solid ${({ theme }) => theme.colors.black}; +`; + +export const SampleImageContainer = styled.div` + display: flex; + flex-direction: column; + padding: 42px; + width: 100%; + align-items: center; + gap: 42px; + overflow: scroll; + height: 80vh; +`; + +export const SampleImg = styled.img<{ + $templateType: string; + $isSelected: boolean; +}>` + cursor: pointer; + width: ${({ $templateType }) => ($templateType === '스탬프' ? '50px' : '270px')}; + height: ${({ $templateType }) => ($templateType === '스탬프' ? '50px' : '150px')}; + border: ${({ theme, $isSelected }) => $isSelected && `5px solid ${theme.colors.main}`}; +`; diff --git a/frontend/src/pages/Admin/CouponDesign/CouponPreviewSection/index.tsx b/frontend/src/pages/Admin/CouponDesign/CouponPreviewSection/index.tsx new file mode 100644 index 000000000..bdd5efa1b --- /dev/null +++ b/frontend/src/pages/Admin/CouponDesign/CouponPreviewSection/index.tsx @@ -0,0 +1,16 @@ +import { Spacing } from '../../../../style/layout/common'; +import FlippedCoupon, { FlippedCouponProps } from '../../../CouponList/FlippedCoupon'; + +type CouponPreviewSectionProps = FlippedCouponProps; + +const CouponPreviewSection = (props: CouponPreviewSectionProps) => { + return ( +
+ + + +
+ ); +}; + +export default CouponPreviewSection; diff --git a/frontend/src/pages/Admin/CouponDesign/CouponPreviewSection/style.tsx b/frontend/src/pages/Admin/CouponDesign/CouponPreviewSection/style.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/pages/Admin/CouponDesign/CustomCouponDesign/StampCustomModal/index.tsx b/frontend/src/pages/Admin/CouponDesign/CustomCouponDesign/StampCustomModal/index.tsx new file mode 100644 index 000000000..11ac25293 --- /dev/null +++ b/frontend/src/pages/Admin/CouponDesign/CustomCouponDesign/StampCustomModal/index.tsx @@ -0,0 +1,106 @@ +import { Dispatch, MouseEvent, SetStateAction, useEffect, useRef, useState } from 'react'; +import Modal from '../../../../../components/Modal'; +import { BackCouponWrapper, BackImage, ButtonContainer, Stamp, StampBadge } from './style'; +import Text from '../../../../../components/Text'; +import Button from '../../../../../components/Button'; +import { Coordinate, StampCoordinate } from '../../../../../types'; + +interface Props { + isOpen: boolean; + stampCoordinates: StampCoordinate[]; + backImgFileUrl: string; + stampImgFileUrl: string; + maxStampCount: number; + setIsOpen: Dispatch>; + setStampCoordinates: Dispatch>; +} + +const StampCustomModal = ({ + isOpen, + stampCoordinates, + backImgFileUrl, + stampImgFileUrl, + maxStampCount, + setIsOpen, + setStampCoordinates, +}: Props) => { + const [modalRect, setModalRect] = useState(null); + const [drawStampCoordinates, setDrawStampCoordinates] = useState([]); + const modalRef = useRef(null); + + useEffect(() => { + if (isOpen) { + const modalElement = modalRef.current; + if (modalElement) { + setModalRect(modalElement.getBoundingClientRect()); + } + } + }, [isOpen]); + + const recordStampCoordinates = (e: MouseEvent) => { + if (drawStampCoordinates.length >= maxStampCount) return; + + if (modalRect && e.target instanceof HTMLImageElement) { + const boundX = modalRect.left; + const boundY = modalRect.top; + + const drawX = e.clientX - boundX; + const drawY = e.clientY - boundY; + + const xCoordinate = (e.clientX - e.target.getBoundingClientRect().left) / 2; + const yCoordinate = (e.clientY - e.target.getBoundingClientRect().top) / 2; + + setStampCoordinates((prevPos) => [ + ...prevPos, + { order: prevPos.length + 1, xCoordinate, yCoordinate }, + ]); + setDrawStampCoordinates((prevPos) => [ + ...prevPos, + { xCoordinate: drawX, yCoordinate: drawY }, + ]); + } + }; + + const closeModal = () => { + setIsOpen(false); + }; + + const removeLastStamp = () => { + setDrawStampCoordinates(drawStampCoordinates.slice(0, drawStampCoordinates.length - 1)); + setStampCoordinates(stampCoordinates.slice(0, stampCoordinates.length - 1)); + }; + + return ( + <> + {isOpen && ( + + 스탬프 위치 설정 + + {drawStampCoordinates.map((coord, index) => ( + + {index + 1} + + + ))} + + + + + {`${drawStampCoordinates.length}/${maxStampCount}`} + + + + )} + + ); +}; + +export default StampCustomModal; diff --git a/frontend/src/pages/Admin/CouponDesign/CustomCouponDesign/StampCustomModal/style.tsx b/frontend/src/pages/Admin/CouponDesign/CustomCouponDesign/StampCustomModal/style.tsx new file mode 100644 index 000000000..327d3de7f --- /dev/null +++ b/frontend/src/pages/Admin/CouponDesign/CustomCouponDesign/StampCustomModal/style.tsx @@ -0,0 +1,52 @@ +import styled from 'styled-components'; + +export const BackCouponWrapper = styled.div` + display: flex; + width: 100%; + height: 350px; + + align-items: center; + justify-content: center; + box-sizing: border-box; +`; + +export const Stamp = styled.span<{ + $x: number; + $y: number; +}>` + display: flex; + position: absolute; + justify-content: center; + align-items: center; + left: ${(props) => props.$x}px; + top: ${(props) => props.$y}px; + width: 50px; + height: 50px; + transform: translate(-50%, -50%); +`; + +export const StampBadge = styled.span` + display: flex; + position: absolute; + top: -10px; + right: -10px; + justify-content: center; + width: 20px; + height: 20px; + background-color: yellow; + border: 1px solid black; + border-radius: 50%; + line-height: 20px; +`; + +export const BackImage = styled.img` + width: 540px; + height: 300px; +`; + +export const ButtonContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: center; +`; diff --git a/frontend/src/pages/Admin/CouponDesign/CustomCouponDesign/index.tsx b/frontend/src/pages/Admin/CouponDesign/CustomCouponDesign/index.tsx new file mode 100644 index 000000000..9e83747ba --- /dev/null +++ b/frontend/src/pages/Admin/CouponDesign/CustomCouponDesign/index.tsx @@ -0,0 +1,143 @@ +import { + CustomCouponDesignContainer, + ImageUploadContainer, + SaveButtonWrapper, + StampCustomButtonWrapper, +} from './style'; +import { RowSpacing, Spacing } from '../../../../style/layout/common'; +import CustomCouponSection from '../CustomCouponSection'; +import CustomStampSection from '../CustomStampSection'; +import Button from '../../../../components/Button'; +import useUploadImage from '../../../../hooks/useUploadImage'; +import { useState } from 'react'; +import { parseExpireDate, parseStampCount } from '../../../../utils'; +import Text from '../../../../components/Text'; +import StampCustomModal from './StampCustomModal'; +import { CouponDesignLocation, StampCoordinate } from '../../../../types'; +import CouponPreviewSection from '../CouponPreviewSection'; +import { useLocation } from 'react-router-dom'; +import { useMutateCouponPolicy } from '../hooks/useMutateCouponPolicy'; +import CouponPreviewImg from '../../../../assets/coupon_preview.png'; +import StampPreviewImg from '../../../../assets/stamp_preview.png'; +import { useRedirectRegisterPage } from '../../../../hooks/useRedirectRegisterPage'; + +const CustomCouponDesign = () => { + const cafeId = useRedirectRegisterPage(); + const { state } = useLocation() as unknown as CouponDesignLocation; + const [frontImageUrl, uploadFrontImageUrl] = useUploadImage(CouponPreviewImg); + const [backImageUrl, uploadBackImageUrl] = useUploadImage(CouponPreviewImg); + const [stampCoordinates, setStampCoordinates] = useState([]); + const [stampImageUrl, uploadStampImage] = useUploadImage(StampPreviewImg); + const [isModalOpen, setIsModalOpen] = useState(false); + const isCustom = state.createdType === 'custom'; + const maxStampCount = parseStampCount(state.stampCount); + const { mutate } = useMutateCouponPolicy(); + + const changeCouponDesignAndPolicy = () => { + if ( + stampCoordinates.length !== maxStampCount || + !frontImageUrl || + !stampImageUrl || + !backImageUrl + ) { + alert('모두 설정해주세요.'); + return; + } + + const couponSettingBody = { + frontImageUrl, + backImageUrl, + stampImageUrl, + coordinates: stampCoordinates, + reward: state.reward, + expirePeriod: parseExpireDate(state.expirePeriod.value), + maxStampCount: maxStampCount, + }; + + mutate({ params: { cafeId }, body: couponSettingBody }); + }; + + const customStampPosition = () => { + if (!stampImageUrl || !backImageUrl) { + alert('먼저 쿠폰 뒷면, 스탬프 이미지를 업로드해주세요.'); + return; + } + + setIsModalOpen(true); + }; + + return ( + <> + + +
+ 쿠폰 제작 및 변경 + + 예상 쿠폰 이미지 + + +
+ + + +
+ +
+ + + +
+
+ + + + + + + +
+ +
+ + + ); +}; + +export default CustomCouponDesign; diff --git a/frontend/src/pages/Admin/CouponDesign/CustomCouponDesign/style.tsx b/frontend/src/pages/Admin/CouponDesign/CustomCouponDesign/style.tsx new file mode 100644 index 000000000..66bd43a12 --- /dev/null +++ b/frontend/src/pages/Admin/CouponDesign/CustomCouponDesign/style.tsx @@ -0,0 +1,62 @@ +import { styled } from 'styled-components'; +import { PageContainer } from '../../../../style/layout/common'; + +export const CustomCouponDesignContainer = styled(PageContainer)` + display: flex; +`; + +export const ImageUploadContainer = styled.div` + display: flex; + justify-content: space-between; +`; + +export const SaveButtonWrapper = styled.div` + display: flex; + flex-direction: row-reverse; + width: 100%; +`; + +export const PreviewImageWrapper = styled.div<{ + $width: number; + $height: number; + $opacity?: number; +}>` + position: relative; + width: ${({ $width }) => `${$width}px`}; + height: ${({ $height }) => `${$height}px`}; + border: 1px dotted ${({ theme }) => theme.colors.black}; +`; + +export const PreviewImage = styled.img<{ $width: number; $height: number; $opacity?: number }>` + width: ${({ $width }) => `${$width}px`}; + height: ${({ $height }) => `${$height}px`}; + opacity: ${({ $opacity }) => ($opacity ? $opacity : '1')}; + object-fit: cover; +`; + +export const ImageUpLoadInputLabel = styled.label` + display: flex; + align-items: center; + justify-content: center; + gap: 5px; + width: 150px; + height: 40px; + background: #eee; + border-radius: 5px; + border: 1px solid ${({ theme }) => theme.colors.text}; + color: ${({ theme }) => theme.colors.text}; + cursor: pointer; + &:hover { + color: tomato; + } +`; + +export const ImageUpLoadInput = styled.input` + display: none; +`; + +export const StampCustomButtonWrapper = styled.div` + display: flex; + flex-direction: row-reverse; + width: 100%; +`; diff --git a/frontend/src/pages/Admin/CouponDesign/CustomCouponSection/index.tsx b/frontend/src/pages/Admin/CouponDesign/CustomCouponSection/index.tsx new file mode 100644 index 000000000..11616d76a --- /dev/null +++ b/frontend/src/pages/Admin/CouponDesign/CustomCouponSection/index.tsx @@ -0,0 +1,63 @@ +import { CouponPreviewHeader } from './style'; +import { Spacing } from '../../../../style/layout/common'; +import { + ImageUpLoadInput, + ImageUpLoadInputLabel, + PreviewImage, + PreviewImageWrapper, +} from '../CustomCouponDesign/style'; +import Text from '../../../../components/Text'; +import { ChangeEvent } from 'react'; +import CouponLoadImg from '../../../../assets/coupon_loading_img.png'; +import CouponPreviewImg from '../../../../assets/coupon_preview.png'; +import { selectImgUrl, useLoadImg } from '../hooks/useLoadImg'; + +interface CustomCouponSectionProps { + label: string; + uploadImageInputId: string; + imgFileUrl: string; + isCustom: boolean; + uploadImageFile: (e: ChangeEvent) => void; +} + +const CustomCouponSection = ({ + label, + uploadImageInputId, + imgFileUrl, + isCustom, + uploadImageFile, +}: CustomCouponSectionProps) => { + const { isLoading, handleImageLoad } = useLoadImg(imgFileUrl); + + return ( + <> + + + + {isCustom && ( + + 이미지 업로드 + + + )} + + + + + + + 270 * 150 의 이미지만 올려주세요. + + ); +}; + +export default CustomCouponSection; diff --git a/frontend/src/pages/Admin/CouponDesign/CustomCouponSection/style.tsx b/frontend/src/pages/Admin/CouponDesign/CustomCouponSection/style.tsx new file mode 100644 index 000000000..0d87e313d --- /dev/null +++ b/frontend/src/pages/Admin/CouponDesign/CustomCouponSection/style.tsx @@ -0,0 +1,6 @@ +import { styled } from 'styled-components'; + +export const CouponPreviewHeader = styled.div` + display: flex; + justify-content: space-between; +`; diff --git a/frontend/src/pages/Admin/CouponDesign/CustomStampSection/index.tsx b/frontend/src/pages/Admin/CouponDesign/CustomStampSection/index.tsx new file mode 100644 index 000000000..3915412fb --- /dev/null +++ b/frontend/src/pages/Admin/CouponDesign/CustomStampSection/index.tsx @@ -0,0 +1,56 @@ +import { ChangeEvent } from 'react'; +import { Spacing } from '../../../../style/layout/common'; +import { + ImageUpLoadInput, + ImageUpLoadInputLabel, + PreviewImage, + PreviewImageWrapper, +} from '../CustomCouponDesign/style'; +import { selectImgUrl, useLoadImg } from '../hooks/useLoadImg'; +import StampLoadImg from '../../../../assets/stamp_load_img.png'; +import StampPreviewImg from '../../../../assets/stamp_preview.png'; + +interface CustomStampSectionProps { + label: string; + uploadImageInputId: string; + imgFileUrl: string; + isCustom: boolean; + uploadImageFile: (e: ChangeEvent) => void; +} + +const CustomStampSection = ({ + label, + uploadImageInputId, + imgFileUrl, + isCustom, + uploadImageFile, +}: CustomStampSectionProps) => { + const { isLoading, handleImageLoad } = useLoadImg(imgFileUrl); + + return ( + <> + + + + {isCustom && ( + 이미지 업로드 + + )} + + + + + + ); +}; + +export default CustomStampSection; diff --git a/frontend/src/pages/Admin/CouponDesign/TemplateCouponDesign/index.tsx b/frontend/src/pages/Admin/CouponDesign/TemplateCouponDesign/index.tsx new file mode 100644 index 000000000..5d017b47d --- /dev/null +++ b/frontend/src/pages/Admin/CouponDesign/TemplateCouponDesign/index.tsx @@ -0,0 +1,124 @@ +import { useLocation } from 'react-router-dom'; +import Text from '../../../../components/Text'; +import { RowSpacing, Spacing } from '../../../../style/layout/common'; +import { + CustomCouponDesignContainer, + ImageUploadContainer, + SaveButtonWrapper, +} from '../CustomCouponDesign/style'; +import useUploadImage from '../../../../hooks/useUploadImage'; +import { CouponDesignLocation, StampCoordinate } from '../../../../types'; +import { useState } from 'react'; +import { parseExpireDate, parseStampCount } from '../../../../utils'; +import CustomCouponSection from '../CustomCouponSection'; +import CouponPreviewSection from '../CouponPreviewSection'; +import CustomStampSection from '../CustomStampSection'; +import Button from '../../../../components/Button'; +import ChoiceTemplate from '../ChoiceTemplate'; +import { useMutateCouponPolicy } from '../hooks/useMutateCouponPolicy'; +import CouponPreviewImg from '../../../../assets/coupon_preview.png'; +import StampPreviewImg from '../../../../assets/stamp_preview.png'; +import { CouponSettingReqBody } from '../../../../types/api'; +import { useRedirectRegisterPage } from '../../../../hooks/useRedirectRegisterPage'; + +const TemplateCouponDesign = () => { + const cafeId = useRedirectRegisterPage(); + const { state } = useLocation() as unknown as CouponDesignLocation; + const [frontImageUrl, uploadFrontImageUrl, setFrontImageUrl] = useUploadImage(CouponPreviewImg); + const [backImageUrl, uploadBackImageUrl, setBackImageUrl] = useUploadImage(CouponPreviewImg); + const [stampCoordinates, setStampCoordinates] = useState([]); + const [stampImageUrl, uploadStampImageUrl, setStampImageUrl] = useUploadImage(StampPreviewImg); + const { mutate } = useMutateCouponPolicy(); + const isCustom = state.createdType === 'custom'; + const maxStampCount = parseStampCount(state.stampCount); + + const changeCouponDesignAndPolicy = () => { + if (stampCoordinates.length === 0 || !frontImageUrl || !stampImageUrl || !backImageUrl) { + alert('이미지를 모두 선택해주세요.'); + return; + } + + const couponSettingBody: CouponSettingReqBody = { + frontImageUrl, + backImageUrl, + stampImageUrl, + coordinates: stampCoordinates, + reward: state.reward, + expirePeriod: parseExpireDate(state.expirePeriod.value), + maxStampCount: maxStampCount, + }; + + mutate({ params: { cafeId }, body: couponSettingBody }); + }; + + return ( + <> + + +
+ 쿠폰 템플릿으로 제작 + + 예상 쿠폰 이미지 + + + +
+ + + +
+ +
+ + + +
+
+ + + + +
+ + +
+ + ); +}; + +export default TemplateCouponDesign; diff --git a/frontend/src/pages/Admin/CouponDesign/hooks/useLoadImg.ts b/frontend/src/pages/Admin/CouponDesign/hooks/useLoadImg.ts new file mode 100644 index 000000000..af14422e3 --- /dev/null +++ b/frontend/src/pages/Admin/CouponDesign/hooks/useLoadImg.ts @@ -0,0 +1,30 @@ +import { useEffect, useState } from 'react'; + +export const useLoadImg = (imgFileUrl: string) => { + const [prevUrl, setPrevUrl] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (prevUrl !== imgFileUrl) { + setIsLoading(true); + } + }, [imgFileUrl]); + + const handleImageLoad = () => { + setIsLoading(false); + setPrevUrl(imgFileUrl); + }; + + return { isLoading, handleImageLoad }; +}; + +export const selectImgUrl = ( + imgUrl: string, + previewImgUrl: string, + loadImgUrl: string, + isLoading: boolean, +) => { + if (isLoading) return loadImgUrl; + if (!imgUrl) return previewImgUrl; + return imgUrl; +}; diff --git a/frontend/src/pages/Admin/CouponDesign/hooks/useMutateCouponPolicy.ts b/frontend/src/pages/Admin/CouponDesign/hooks/useMutateCouponPolicy.ts new file mode 100644 index 000000000..acf51e7a1 --- /dev/null +++ b/frontend/src/pages/Admin/CouponDesign/hooks/useMutateCouponPolicy.ts @@ -0,0 +1,17 @@ +import { useMutation } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; +import { postCouponSetting } from '../../../../api/post'; +import { ROUTER_PATH } from '../../../../constants'; + +export const useMutateCouponPolicy = () => { + const navigate = useNavigate(); + + const { mutate } = useMutation({ + mutationFn: postCouponSetting, + onSuccess: () => { + navigate(ROUTER_PATH.customerList); + }, + }); + + return { mutate }; +}; diff --git a/frontend/src/pages/Admin/CustomerList/Customers/index.tsx b/frontend/src/pages/Admin/CustomerList/Customers/index.tsx new file mode 100644 index 000000000..8a631c259 --- /dev/null +++ b/frontend/src/pages/Admin/CustomerList/Customers/index.tsx @@ -0,0 +1,56 @@ +import { Customer } from '../../../../types'; +import { CustomersRes } from '../../../../types/api'; +import { + Container, + Badge, + CustomerBox, + InfoContainer, + LeftInfo, + Name, + NameContainer, + RightInfo, +} from './style'; + +interface CustomersProps { + customersData: CustomersRes; +} + +const Customers = ({ customersData }: CustomersProps) => { + return ( + + {customersData.customers.map( + ({ + id, + nickname, + stampCount, + maxStampCount, + rewardCount, + isRegistered, + firstVisitDate, + visitCount, + }: Customer) => ( + + + + {nickname} + {isRegistered ? '회원' : '임시'} + + + 스탬프: {stampCount}/{maxStampCount}
+ 리워드: {rewardCount}개 +
+
+ + + 첫 방문일: {firstVisitDate} +
방문 횟수: {visitCount}번 +
+
+
+ ), + )} +
+ ); +}; + +export default Customers; diff --git a/frontend/src/pages/Admin/CustomerList/Customers/style.tsx b/frontend/src/pages/Admin/CustomerList/Customers/style.tsx new file mode 100644 index 000000000..34939dbf5 --- /dev/null +++ b/frontend/src/pages/Admin/CustomerList/Customers/style.tsx @@ -0,0 +1,68 @@ +import { styled } from 'styled-components'; + +export const Container = styled.div` + display: flex; + flex-direction: column; + overflow: scroll; + max-height: 550px; +`; + +export const Badge = styled.div<{ $isRegistered: boolean }>` + width: 40px; + height: 18px; + border-radius: 4px; + line-height: 18px; + text-align: center; + font-size: 12px; + color: black; + background: ${({ $isRegistered, theme }) => + $isRegistered ? theme.colors.point : theme.colors.gray300}; +`; + +export const NameContainer = styled.div` + display: flex; + align-items: center; + gap: 10px; + color: #222; +`; + +export const RightInfo = styled.div` + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: flex-end; + margin-top: 10px; +`; + +export const InfoContainer = styled.span` + font-size: 14px; + color: ${({ theme }) => theme.colors.text}; + line-height: 16px; +`; + +export const CustomerBox = styled.ul` + display: flex; + justify-content: space-between; + min-height: 90px; + padding: 15px; + + margin: 0 20px 15px 20px; + border-radius: 10px; + background-color: ${({ theme }) => theme.colors.gray100}; + box-shadow: 0 10px 10px -3px ${({ theme }) => theme.colors.gray300}; + + &:first-of-type { + margin-top: 20px; + } +`; + +export const LeftInfo = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; +`; + +export const Name = styled.h1` + font-size: 20px; + font-weight: 500; +`; diff --git a/frontend/src/pages/Admin/CustomerList/index.tsx b/frontend/src/pages/Admin/CustomerList/index.tsx new file mode 100644 index 000000000..91ed25b64 --- /dev/null +++ b/frontend/src/pages/Admin/CustomerList/index.tsx @@ -0,0 +1,91 @@ +import { CustomerContainer, Container, EmptyCustomers } from './style'; +import Text from '../../../components/Text'; +import { useEffect, useState } from 'react'; +import SearchBar from '../../../components/SearchBar'; +import { useQuery } from '@tanstack/react-query'; +import SelectBox from '../../../components/SelectBox'; +import { getCustomers } from '../../../api/get'; +import { CUSTOMERS_ORDER_OPTIONS, INVALID_CAFE_ID, ROUTER_PATH } from '../../../constants'; +import { Customer } from '../../../types'; +import { CustomersRes } from '../../../types/api'; +import LoadingSpinner from '../../../components/LoadingSpinner'; +import Customers from './Customers'; +import { useRedirectRegisterPage } from '../../../hooks/useRedirectRegisterPage'; +import { useNavigate } from 'react-router-dom'; + +const CustomerList = () => { + const navigate = useNavigate(); + const cafeId = useRedirectRegisterPage(); + const [searchWord, setSearchWord] = useState(''); + const [orderOption, setOrderOption] = useState({ key: 'stampCount', value: '스탬프순' }); + const orderCustomer = (customers: Customer[]) => { + customers.sort((a: Customer, b: Customer) => { + if (a[orderOption.key as keyof Customer] === b[orderOption.key as keyof Customer]) { + return a['nickname'] > b['nickname'] ? 1 : -1; + } + return a[orderOption.key as keyof Customer] < b[orderOption.key as keyof Customer] ? 1 : -1; + }); + }; + + const { data, status } = useQuery({ + queryKey: ['customers'], + queryFn: () => + getCustomers({ + params: { + cafeId, + }, + }), + onSuccess: (data) => { + orderCustomer(data.customers); + }, + enabled: cafeId !== INVALID_CAFE_ID, + }); + + useEffect(() => { + if ( + localStorage.getItem('admin-login-token') === '' || + !localStorage.getItem('admin-login-token') + ) + navigate(ROUTER_PATH.adminLogin); + if (status === 'success' && data.customers.length !== 0) { + orderCustomer(data.customers); + } + }, [orderOption]); + + if (status === 'loading') return ; + if (status === 'error') return Error; + + if (data.customers.length === 0) + return ( + + 내 고객 목록 + + 아직 보유고객이 없어요!
+ 카페를 방문한 고객에게 스탬프를 적립해 보세요. +
+
+ ); + + const searchCustomer = () => { + if (searchWord === '') return; + + // TODO: 추후에 백엔드와 검색 기능 토의 후 수정 예정 + }; + + return ( + + 내 고객 목록 + + + + + + + ); +}; + +export default CustomerList; diff --git a/frontend/src/pages/Admin/CustomerList/style.tsx b/frontend/src/pages/Admin/CustomerList/style.tsx new file mode 100644 index 000000000..92f14c8e2 --- /dev/null +++ b/frontend/src/pages/Admin/CustomerList/style.tsx @@ -0,0 +1,29 @@ +import { styled } from 'styled-components'; + +export const CustomerContainer = styled.div` + display: flex; + flex-direction: column; + margin-top: 40px; + width: 100%; +`; + +export const CustomerBoxContainer = styled.div` + display: flex; + flex-direction: column; + overflow: scroll; + height: 540px; +`; + +export const EmptyCustomers = styled.p` + display: flex; + margin: 80px 10px; +`; + +export const Container = styled.div` + display: flex; + align-self: flex-end; + align-items: center; + gap: 20px; + margin-top: 30px; + margin-right: 30px; +`; diff --git a/frontend/src/pages/Admin/EarnStamp/SelectCoupon/index.tsx b/frontend/src/pages/Admin/EarnStamp/SelectCoupon/index.tsx new file mode 100644 index 000000000..019f60f76 --- /dev/null +++ b/frontend/src/pages/Admin/EarnStamp/SelectCoupon/index.tsx @@ -0,0 +1,209 @@ +import { useLocation, useNavigate } from 'react-router-dom'; +import Button from '../../../../components/Button'; +import { Spacing } from '../../../../style/layout/common'; +import { ChangeEvent, useState } from 'react'; +import { + CouponLabelContainer, + CouponSelector, + CouponSelectorLabel, + SelectDescription, + SelectTitle, + SelectorItemWrapper, +} from './style'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import FlippedCoupon from '../../../CouponList/FlippedCoupon'; +import { INVALID_CAFE_ID, ROUTER_PATH } from '../../../../constants'; +import { useRedirectRegisterPage } from '../../../../hooks/useRedirectRegisterPage'; +import { getCoupon, getCouponDesign, getCustomer } from '../../../../api/get'; +import { postIssueCoupon, postRegisterUser } from '../../../../api/post'; +import { formatDate } from '../../../../utils'; +import Text from '../../../../components/Text'; +import { CouponActivate } from '../../../../types'; +import { CustomerPhoneNumberRes, IssueCouponRes, IssuedCouponsRes } from '../../../../types/api'; +import { LuStamp } from 'react-icons/lu'; +import { MdAddCard } from 'react-icons/md'; +import { CouponSelectorContainer, CouponSelectorWrapper } from '../style'; + +const SelectCoupon = () => { + const cafeId = useRedirectRegisterPage(); + const location = useLocation(); + const navigate = useNavigate(); + const [isPrevious, setIsPrevious] = useState(true); + const [selectedCoupon, setSelectedCoupon] = useState('current'); + const phoneNumber = location.state.phoneNumber; + + const { + data: customer, + status: customerStatus, + refetch: refetchCustomer, + } = useQuery(['customer', phoneNumber], { + queryFn: () => getCustomer({ params: { phoneNumber } }), + onSuccess: (data) => { + if (data.customer.length === 0) { + mutateTempCustomer({ body: phoneNumber }); + } + }, + }); + + const { mutate: mutateTempCustomer } = useMutation({ + mutationFn: postRegisterUser, + onSuccess: () => { + setSelectedCoupon('new'); + refetchCustomer(); + }, + onError: () => { + throw new Error('스탬프 적립에 실패했습니다.'); + }, + }); + + const { data: coupon, status: couponStatus } = useQuery( + ['coupon', customer], + { + queryFn: async () => { + if (!customer) throw new Error('고객 정보를 불러오지 못했습니다.'); + return await getCoupon({ params: { customerId: customer.customer[0].id, cafeId } }); + }, + enabled: !!customer && cafeId !== INVALID_CAFE_ID, + }, + ); + + const { data: couponDesignData, status: couponDesignStatus } = useQuery({ + queryKey: ['couponDesign'], + queryFn: () => getCouponDesign({ params: { cafeId } }), + enabled: cafeId !== INVALID_CAFE_ID, + }); + + const { mutate: mutateIssueCoupon } = useMutation({ + mutationFn: async () => { + if (!customer) throw new Error('고객 정보를 불러오지 못했습니다.'); + return await postIssueCoupon({ + params: { customerId: customer.customer[0].id }, + body: { + cafeId, + }, + }); + }, + onSuccess: (data: IssueCouponRes) => { + const newCouponId = +data.couponId; + navigate(ROUTER_PATH.earnStamp, { + state: { + isPrevious, + customer: foundCustomer, + couponId: newCouponId, + couponDesignData, + }, + }); + }, + onError: () => { + throw new Error('스탬프 적립에 실패했습니다.'); + }, + }); + + if ( + couponStatus === 'loading' || + customerStatus === 'loading' || + couponDesignStatus === 'loading' + ) + return

Loading

; + + if (couponStatus === 'error' || customerStatus === 'error' || couponDesignStatus === 'error') + return

Error

; + + const selectCoupon = (e: ChangeEvent) => { + const { value } = e.target; + if (value !== 'current' && value !== 'new') return; + + setIsPrevious(value === 'current'); + setSelectedCoupon(value); + }; + + const moveNextStep = () => { + if (selectedCoupon === 'current' && coupon.coupons.length !== 0) { + navigate(ROUTER_PATH.earnStamp, { + state: { + isPrevious, + customer: foundCustomer, + couponId: foundCoupon.id, + couponDesignData, + }, + }); + } + if (selectedCoupon === 'new' || coupon.coupons.length === 0) mutateIssueCoupon(); + }; + + const foundCustomer = customer.customer[0]; + const foundCoupon = coupon.coupons[0]; + + return ( + <> + + 스탬프 적립 + + step1. {foundCustomer.nickname} 고객님의 쿠폰을 선택해주세요. + + {coupon.coupons.length > 0 ? ( + <> + + + 현재 스탬프 개수: {foundCoupon.stampCount}/{foundCoupon.maxStampCount} + + + + + 쿠폰 유효기간: {formatDate(foundCoupon.expireDate)}까지 + + + + + + 새 쿠폰 발급 + + 새 쿠폰을 만들고 +
새 쿠폰에 적립할게요. +
+ +
+
+ + + + + 현재 쿠폰 적립 + + 고객님이 보유하신
+ 현재 쿠폰에 적립할게요. +
+ +
+
+
+ + ) : ( + 다음 버튼을 눌러 신규 쿠폰을 발급합니다. + )} + +
+ + ); +}; + +export default SelectCoupon; diff --git a/frontend/src/pages/Admin/EarnStamp/SelectCoupon/style.tsx b/frontend/src/pages/Admin/EarnStamp/SelectCoupon/style.tsx new file mode 100644 index 000000000..f34a958dd --- /dev/null +++ b/frontend/src/pages/Admin/EarnStamp/SelectCoupon/style.tsx @@ -0,0 +1,56 @@ +import styled from 'styled-components'; + +export const TitleWrapper = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; +`; + +export const CouponSelector = styled.input` + appearance: none; +`; + +export const CouponSelectorLabel = styled.label<{ $isChecked: boolean }>` + position: relative; + width: 200px; + height: ${({ $isChecked }) => ($isChecked ? '200px' : '60px')}; + border-radius: 20px; + padding: 17px 20px; + color: ${({ $isChecked, theme }) => ($isChecked ? 'black' : theme.colors.gray)}; + border: 3px solid + ${({ $isChecked, theme }) => ($isChecked ? theme.colors.main : theme.colors.gray)}; + box-shadow: ${({ $isChecked }) => ($isChecked ? '0px 0px 5px 2px rgba(0, 0, 0, 0.25)' : '')}; + + overflow: hidden; + transform: ${({ $isChecked }) => ($isChecked ? 'scale(1.1)' : '')}; + transition: all 0.2s; + + cursor: pointer; + + & > svg { + position: absolute; + bottom: 10px; + right: 10px; + } +`; + +export const SelectorItemWrapper = styled.div` + display: flex; + align-items: flex-start; +`; + +export const CouponLabelContainer = styled.div` + display: flex; + gap: 20px; +`; + +export const SelectTitle = styled.header` + font-size: 20px; + font-weight: 700; +`; + +export const SelectDescription = styled.p` + font-size: 16px; + margin-top: 20px; + line-height: 20px; +`; diff --git a/frontend/src/pages/Admin/EarnStamp/index.tsx b/frontend/src/pages/Admin/EarnStamp/index.tsx new file mode 100644 index 000000000..e54a9be27 --- /dev/null +++ b/frontend/src/pages/Admin/EarnStamp/index.tsx @@ -0,0 +1,90 @@ +import { useLocation, useNavigate } from 'react-router-dom'; +import Button from '../../../components/Button'; +import { Spacing } from '../../../style/layout/common'; +import { useState } from 'react'; +import Stepper from '../../../components/Stepper'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { CouponSelectorContainer, CouponSelectorWrapper } from './style'; +import { getCoupon } from '../../../api/get'; +import { postEarnStamp } from '../../../api/post'; +import Text from '../../../components/Text'; +import { INVALID_CAFE_ID, ROUTER_PATH } from '../../../constants'; +import { IssuedCouponsRes } from '../../../types/api'; +import FlippedCoupon from '../../CouponList/FlippedCoupon'; +import { useRedirectRegisterPage } from '../../../hooks/useRedirectRegisterPage'; +import LoadingSpinner from '../../../components/LoadingSpinner'; + +const EarnStamp = () => { + const cafeId = useRedirectRegisterPage(); + const [stamp, setStamp] = useState(1); + const { state } = useLocation(); + const navigate = useNavigate(); + + const { mutate } = useMutation({ + mutationFn: postEarnStamp, + onSuccess: () => { + alert('스탬프 적립에 성공했습니다.'); + navigate(ROUTER_PATH.customerList); + }, + onError: () => { + throw new Error('스탬프 적립에 실패했습니다.'); + }, + }); + + const { data: couponData, status: couponStatus } = useQuery( + ['earn-stamp-coupons', state.customer], + () => getCoupon({ params: { customerId: state.customer.id, cafeId } }), + { + enabled: cafeId !== INVALID_CAFE_ID, + }, + ); + + if (couponStatus === 'error') return

Error

; + if (couponStatus === 'loading') return ; + + const earnStamp = () => { + mutate({ + params: { + customerId: Number(state.customer.id), + couponId: couponData.coupons[0].id, + }, + body: { + earningStampCount: stamp, + }, + }); + }; + + return ( + <> + + 스탬프 적립 + + + step2. {state.customer.nickname} 고객에게 적립할 스탬프 갯수를 입력해주세요. + + + + + + 현재 스탬프 개수: {couponData.coupons[0].stampCount}/ + {couponData.coupons[0].maxStampCount} + + + + + 쿠폰 유효기간: {couponData.coupons[0].expireDate}까지 + + + + + ); +}; + +export default EarnStamp; diff --git a/frontend/src/pages/Admin/EarnStamp/style.tsx b/frontend/src/pages/Admin/EarnStamp/style.tsx new file mode 100644 index 000000000..e1f693a91 --- /dev/null +++ b/frontend/src/pages/Admin/EarnStamp/style.tsx @@ -0,0 +1,46 @@ +import styled from 'styled-components'; + +export const CouponStepperWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +export const StepperGuide = styled.p` + font-size: 20px; + font-weight: 600; + line-height: 25px; +`; + +export const CouponSelectorWrapper = styled.div` + display: flex; + flex-direction: column; + position: absolute; + top: 0; + right: -350px; + + & > h1 { + font-size: 20px; + font-weight: 500; + } + + & > span { + font-size: 14px; + } +`; + +export const CouponSelectorContainer = styled.main` + display: flex; + flex-direction: column; + width: 400px; + margin-top: 40px; + height: 400px; + position: relative; + + & > button { + position: absolute; + bottom: 0; + right: 0; + } +`; diff --git a/frontend/src/pages/Admin/EnterPhoneNumber/Dialpad/index.tsx b/frontend/src/pages/Admin/EnterPhoneNumber/Dialpad/index.tsx new file mode 100644 index 000000000..4ef6035d3 --- /dev/null +++ b/frontend/src/pages/Admin/EnterPhoneNumber/Dialpad/index.tsx @@ -0,0 +1,38 @@ +import useDialPad from '../../../../hooks/useDialPad'; +import { BaseInput, Container, KeyContainer, Pad } from './style'; + +const DIAL_KEYS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '←', '0', '입력'] as const; + +export type DialKeyType = (typeof DIAL_KEYS)[number]; + +const Dialpad = () => { + const { phoneNumber, phoneNumberRef, handlePhoneNumber, handleBackspace, pressPad } = + useDialPad(); + + return ( + + + + {DIAL_KEYS.map((dialKey) => ( + + {dialKey} + + ))} + + + ); +}; + +export default Dialpad; diff --git a/frontend/src/pages/Admin/EnterPhoneNumber/Dialpad/style.tsx b/frontend/src/pages/Admin/EnterPhoneNumber/Dialpad/style.tsx new file mode 100644 index 000000000..b7469e80d --- /dev/null +++ b/frontend/src/pages/Admin/EnterPhoneNumber/Dialpad/style.tsx @@ -0,0 +1,54 @@ +import { styled } from 'styled-components'; + +export const Container = styled.section` + display: flex; + flex-direction: column; + border-left: 1px solid black; + border-right: 1px solid black; + border-collapse: separate; + min-width: 580px; +`; + +export const KeyContainer = styled.div` + display: grid; + height: 100%; + + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(4, 1fr); +`; + +export const Pad = styled.button` + display: flex; + align-items: center; + justify-content: center; + border: 1px solid black; + + font-size: 50px; + background-color: white; + color: black; + + &:active { + background-color: ${({ theme }) => theme.colors.gray100}; + transform: scale(0.98); + } + &:last-child { + background-color: ${({ theme }) => theme.colors.main}; + color: ${({ theme }) => theme.colors.point}; + } +`; + +export const BaseInput = styled.input` + outline: none; + padding: 20px 0px; + height: 210px; + border: 1px solid black; + border-top: none; + + font-size: 50px; + text-align: center; + + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } +`; diff --git a/frontend/src/pages/Admin/EnterPhoneNumber/index.tsx b/frontend/src/pages/Admin/EnterPhoneNumber/index.tsx new file mode 100644 index 000000000..858c6e182 --- /dev/null +++ b/frontend/src/pages/Admin/EnterPhoneNumber/index.tsx @@ -0,0 +1,35 @@ +import Dialpad from './Dialpad'; +import { Container, IconWrapper, PageContainer, PrivacyBox, Title } from './style'; +import { IoIosArrowBack } from 'react-icons/io'; +import { useNavigate } from 'react-router-dom'; +import { ROUTER_PATH } from '../../../constants'; +import { useRedirectRegisterPage } from '../../../hooks/useRedirectRegisterPage'; + +const EnterPhoneNumber = () => { + useRedirectRegisterPage(); + const navigate = useNavigate(); + + const navigateBack = () => { + navigate(ROUTER_PATH.customerList); + }; + + return ( + + + <IconWrapper onClick={navigateBack}> + <IoIosArrowBack size="40" /> + </IconWrapper> + 전화번호 입력 + + + +

개인정보 제공동의

+

전화번호를 입력하시면 개인정보 제공에 동의하시는 것으로 간주됩니다.

+
+ +
+
+ ); +}; + +export default EnterPhoneNumber; diff --git a/frontend/src/pages/Admin/EnterPhoneNumber/style.tsx b/frontend/src/pages/Admin/EnterPhoneNumber/style.tsx new file mode 100644 index 000000000..18a9b4dc0 --- /dev/null +++ b/frontend/src/pages/Admin/EnterPhoneNumber/style.tsx @@ -0,0 +1,49 @@ +import { styled } from 'styled-components'; + +export const Title = styled.header` + position: relative; + width: 100vw; + height: 110px; + border-bottom: 3px solid ${({ theme }) => theme.colors.gray400}; + padding: auto 0; + line-height: 110px; + + text-align: center; + font-size: 30px; + font-weight: 600; +`; + +export const IconWrapper = styled.div` + display: flex; + align-items: center; + position: absolute; + left: 30px; + top: 0; + bottom: 0; +`; + +export const Container = styled.div` + display: flex; + height: 100%; +`; + +export const PrivacyBox = styled.section` + padding: 50px 10%; + width: 53%; + + & > p { + font-size: 16px; + line-height: 24px; + } + + & > h1 { + font-size: 24px; + font-weight: 700; + margin-bottom: 15px; + } +`; + +export const PageContainer = styled.div` + width: 100vw; + height: 87vh; +`; diff --git a/frontend/src/pages/Admin/ManageCafe/TimeRangePicker/index.tsx b/frontend/src/pages/Admin/ManageCafe/TimeRangePicker/index.tsx new file mode 100644 index 000000000..d5797ec24 --- /dev/null +++ b/frontend/src/pages/Admin/ManageCafe/TimeRangePicker/index.tsx @@ -0,0 +1,73 @@ +import { ChangeEvent, Dispatch, SetStateAction } from 'react'; +import { Time } from '../../../../types'; +import { TimePickerWrapper, TimeRangePickerContainer } from './style'; + +interface TimePickerProps { + startTime: Time; + endTime: Time; + setStartTime: Dispatch>; + setEndTime: Dispatch>; +} + +const TimePicker = ({ startTime, endTime, setEndTime, setStartTime }: TimePickerProps) => { + const handleStartTimeChange = (e: ChangeEvent) => { + const { name, value } = e.target; + setStartTime((prevStartTime) => ({ + ...prevStartTime, + [name]: value, + })); + }; + + const handleEndTimeChange = (e: ChangeEvent) => { + const { name, value } = e.target; + setEndTime((prevEndTime) => ({ + ...prevEndTime, + [name]: value, + })); + }; + + const hours = Array.from({ length: 24 }, (_, index) => index.toString().padStart(2, '0')); + const minutes = Array.from({ length: 6 }, (_, index) => (index * 10).toString().padStart(2, '0')); + + return ( + + + + : + + + ~ + + + : + + + + ); +}; + +export default TimePicker; diff --git a/frontend/src/pages/Admin/ManageCafe/TimeRangePicker/style.tsx b/frontend/src/pages/Admin/ManageCafe/TimeRangePicker/style.tsx new file mode 100644 index 000000000..3f5853a54 --- /dev/null +++ b/frontend/src/pages/Admin/ManageCafe/TimeRangePicker/style.tsx @@ -0,0 +1,34 @@ +import styled from 'styled-components'; + +export const TimeRangePickerContainer = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 30px; + color: black; + + select { + width: 50px; + height: 30px; + font-size: 1rem; + border: none; + border-radius: 8px; + background-color: transparent; + text-align: center; + outline: none; + + transition: all 0.3s; + } + + select:hover { + color: tomato; + font-weight: 500; + } +`; + +export const TimePickerWrapper = styled.div` + border: 1px solid #ddd; + border-radius: 8px; + padding: 2px; +`; diff --git a/frontend/src/pages/Admin/ManageCafe/index.tsx b/frontend/src/pages/Admin/ManageCafe/index.tsx new file mode 100644 index 000000000..52c646a0d --- /dev/null +++ b/frontend/src/pages/Admin/ManageCafe/index.tsx @@ -0,0 +1,238 @@ +import Text from '../../../components/Text'; +import { Input } from '../../../components/Input'; +import Button from '../../../components/Button'; +import { + ManageCafeForm, + PageContainer, + PreviewBackImage, + PreviewContainer, + PreviewContentContainer, + PreviewCouponBackImage, + PreviewOverviewContainer, + RestrictionLabel, + StepTitle, + TextArea, + Wrapper, +} from './style'; +import TimeRangePicker from './TimeRangePicker'; +import { ChangeEvent, FormEventHandler, useEffect, useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + ImageUpLoadInput, + ImageUpLoadInputLabel, + PreviewImage, + PreviewImageWrapper, +} from '../CouponDesign/CustomCouponDesign/style'; +import useUploadImage from '../../../hooks/useUploadImage'; +import { FaRegClock, FaPhoneAlt } from 'react-icons/fa'; +import { AiOutlineUpload } from 'react-icons/ai'; +import { FaLocationDot } from 'react-icons/fa6'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { getCafe, getCouponDesign } from '../../../api/get'; +import { isEmptyData, parsePhoneNumber, parseTime } from '../../../utils'; +import { patchCafeInfo } from '../../../api/patch'; +import { INVALID_CAFE_ID, ROUTER_PATH } from '../../../constants'; +import { Cafe, Time } from '../../../types'; +import { CafeInfoReqBody } from '../../../types/api'; +import LoadingSpinner from '../../../components/LoadingSpinner'; +import { useRedirectRegisterPage } from '../../../hooks/useRedirectRegisterPage'; +import defaultCafeImg from '../../../assets/default_cafe_bg.png'; + +const ManageCafe = () => { + const cafeId = useRedirectRegisterPage(); + const navigate = useNavigate(); + const [cafeImage, uploadCafeImage, setCafeImage] = useUploadImage(); + const [phoneNumber, setPhoneNumber] = useState(''); + const [introduction, setIntroduction] = useState(''); + + const [openTime, setOpenTime] = useState