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