diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml
new file mode 100644
index 000000000..226ccc318
--- /dev/null
+++ b/.github/workflows/backend-ci.yml
@@ -0,0 +1,51 @@
+name: backend-ci
+
+on:
+ pull_request:
+ branches: [ "main", "dev", "dev-be" ]
+ paths: 'backend/**'
+
+defaults:
+ run:
+ working-directory: backend/bang-ggood
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v3
+ with:
+ gradle-version: 8.8
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ - name: Write application.yml
+ env:
+ APPLICATION_YML: ${{ secrets.APPLICATION_YML }}
+ APPLICATION_LOCAL_YML: ${{ secrets.APPLICATION_LOCAL_YML }}
+ APPLICATION_TEST_YML: ${{ secrets.APPLICATION_TEST_YML }}
+ APPLICATION_READ_WRITE_TEST_YML: ${{ secrets.APPLICATION_READ_WRITE_TEST_YML }}
+
+ run: |
+ echo "${APPLICATION_YML}" > src/test/resources/application.yml
+ echo "${APPLICATION_LOCAL_YML}" > src/test/resources/application-local.yml
+ echo "${APPLICATION_TEST_YML}" > src/test/resources/application-test.yml
+ echo "${APPLICATION_READ_WRITE_TEST_YML}" > src/test/resources/application-read-write-test.yml
+
+ - name: Build with Gradle
+ run: ./gradlew clean build
+
+
diff --git a/.github/workflows/backend-dev-cd.yml b/.github/workflows/backend-dev-cd.yml
new file mode 100644
index 000000000..2dbbd946d
--- /dev/null
+++ b/.github/workflows/backend-dev-cd.yml
@@ -0,0 +1,83 @@
+name: backend-dev-cd
+
+on:
+ pull_request:
+ branches: [ "dev" ]
+ types: [closed]
+
+jobs:
+ build:
+ if: github.event.pull_request.merged == true
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v3
+ with:
+ gradle-version: 8.8
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x backend/bang-ggood/gradlew
+
+ - name: Write application.yml
+ env:
+ APPLICATION_YML: ${{ secrets.APPLICATION_YML }}
+ APPLICATION_DEV_YML: ${{ secrets.APPLICATION_DEV_YML }}
+ APPLICATION_TEST_YML: ${{ secrets.APPLICATION_TEST_YML }}
+ APPLICATION_READ_WRITE_TEST_YML: ${{ secrets.APPLICATION_READ_WRITE_TEST_YML }}
+
+ run: |
+ echo "${APPLICATION_YML}" > backend/bang-ggood/src/main/resources/application.yml
+ echo "${APPLICATION_DEV_YML}" > backend/bang-ggood/src/main/resources/application-dev.yml
+ echo "${APPLICATION_TEST_YML}" > backend/bang-ggood/src/test/resources/application-test.yml
+ echo "${APPLICATION_READ_WRITE_TEST_YML}" > backend/bang-ggood/src/test/resources/application-read-write-test.yml
+
+ - name: Build with Gradle
+ run: ./gradlew clean build
+ working-directory: backend/bang-ggood
+
+ - name: Upload build artifact
+ uses: actions/upload-artifact@v3
+ with:
+ working-directory: backend/bang-ggood
+ name: bang-ggood-be-develop-jar
+ path: ./**/*.jar
+
+ deploy:
+ needs: build
+ runs-on: bang-ggood-dev
+ steps:
+ - name: change permission
+ run: sudo chown -R ubuntu:ubuntu /home/ubuntu/actions-runner/_work/2024-bang-ggood
+
+ - name: Download build artifact
+ uses: actions/download-artifact@v3
+ with:
+ name: bang-ggood-be-develop-jar
+
+ - name: Create unhealth_flag file
+ run: echo "unhealth" | sudo tee /etc/nginx/sites-available/unhealth_flag.txt > /dev/null
+
+
+ - name: Sleep for 30 seconds
+ run: sleep 30
+
+ - name: Turn off the server 8080 if runs
+ run: sudo fuser -k -n tcp 8080 || true
+
+ - name: Start server
+ run: sudo nohup java -jar -Dspring.profiles.active=dev -Duser.timezone=Asia/Seoul ./backend/bang-ggood/build/libs/*SNAPSHOT.jar > /home/ubuntu/actions-runner/server.log 2>&1 &
+
+ - name: Delete unhealth_flag file
+ run: sudo rm /etc/nginx/sites-available/unhealth_flag.txt
diff --git a/.github/workflows/backend-prod-cd.yml b/.github/workflows/backend-prod-cd.yml
new file mode 100644
index 000000000..c31e3749b
--- /dev/null
+++ b/.github/workflows/backend-prod-cd.yml
@@ -0,0 +1,91 @@
+name: backend-prod-cd
+
+on:
+ pull_request:
+ branches: [ "main" ]
+ types: [closed]
+
+jobs:
+ build:
+ if: github.event.pull_request.merged == true
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v3
+ with:
+ gradle-version: 8.8
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x backend/bang-ggood/gradlew
+
+ - name: Write application.yml
+ env:
+ APPLICATION_YML: ${{ secrets.APPLICATION_YML }}
+ APPLICATION_PROD_YML: ${{ secrets.APPLICATION_PROD_YML }}
+ APPLICATION_TEST_YML: ${{ secrets.APPLICATION_TEST_YML }}
+ APPLICATION_READ_WRITE_TEST_YML: ${{ secrets.APPLICATION_READ_WRITE_TEST_YML }}
+
+ run: |
+ echo "${APPLICATION_YML}" > backend/bang-ggood/src/main/resources/application.yml
+ echo "${APPLICATION_PROD_YML}" > backend/bang-ggood/src/main/resources/application-prod.yml
+ echo "${APPLICATION_TEST_YML}" > backend/bang-ggood/src/test/resources/application-test.yml
+ echo "${APPLICATION_READ_WRITE_TEST_YML}" > backend/bang-ggood/src/test/resources/application-read-write-test.yml
+
+ - name: Build with Gradle
+ run: ./gradlew clean build
+ working-directory: backend/bang-ggood
+
+ - name: Upload build artifact
+ uses: actions/upload-artifact@v3
+ with:
+ working-directory: backend/bang-ggood
+ name: bang-ggood-be-develop-jar
+ path: ./**/*.jar
+
+ deploy1:
+ needs: build
+ runs-on: bang-ggood-prod1
+ steps:
+ - name: Change permission
+ run: sudo chown -R ubuntu:ubuntu /home/ubuntu/actions-runner/_work/2024-bang-ggood
+
+ - name: Download build artifact
+ uses: actions/download-artifact@v3
+ with:
+ name: bang-ggood-be-develop-jar
+
+ - name: Turn off the server 8080 if it runs
+ run: sudo fuser -k -n tcp 8080 || true
+
+ - name: Start server
+ run: sudo nohup java -jar -Dspring.profiles.active=prod -Duser.timezone=Asia/Seoul ./backend/bang-ggood/build/libs/*SNAPSHOT.jar > /home/ubuntu/actions-runner/server.log 2>&1 &
+
+ deploy2:
+ needs: deploy1
+ runs-on: bang-ggood-prod2
+ steps:
+ - name: Change permission
+ run: sudo chown -R ubuntu:ubuntu /home/ubuntu/actions-runner/_work/2024-bang-ggood
+
+ - name: Download build artifact
+ uses: actions/download-artifact@v3
+ with:
+ name: bang-ggood-be-develop-jar
+
+ - name: Turn off the server 8080 if it runs
+ run: sudo fuser -k -n tcp 8080 || true
+
+ - name: Start server
+ run: sudo nohup java -jar -Dspring.profiles.active=prod -Duser.timezone=Asia/Seoul ./backend/bang-ggood/build/libs/*SNAPSHOT.jar > /home/ubuntu/actions-runner/server.log 2>&1 &
diff --git a/.github/workflows/discord-pr-notify.yml b/.github/workflows/discord-pr-notify.yml
new file mode 100644
index 000000000..b03d24931
--- /dev/null
+++ b/.github/workflows/discord-pr-notify.yml
@@ -0,0 +1,42 @@
+name: Discord PR
+
+on:
+ pull_request:
+ branches: ["dev-be"]
+ paths: 'backend/**'
+
+jobs:
+ create-issue:
+ name: Discord notification
+ runs-on: ubuntu-latest
+ steps:
+ - name: Send PR
+ uses: Ilshidur/action-discord@0.3.2
+ env:
+ DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }}
+ DISCORD_USERNAME: Bang_GGood_Master
+ DISCORD_EMBEDS: >
+ [
+ {
+ "title": "똑똑~ 방 리모델링 요청이 왔어요!🏠\n${{ github.event.pull_request.title }}",
+ "color": 16777123,
+ "description": "${{ github.event.pull_request.html_url }}",
+ "fields": [
+ {
+ "name": "Pull Request Number",
+ "value": "#${{ github.event.pull_request.number }}",
+ "inline": true
+ },
+ {
+ "name": "Author",
+ "value": "${{ github.event.pull_request.user.login }}",
+ "inline": true
+ },
+ {
+ "name": "Reviewers",
+ "value": "${{ join(github.event.pull_request.requested_reviewers.*.login, ', ') }}",
+ "inline": false
+ }
+ ]
+ }
+ ]
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/.idea/2024-bang-ggood.iml b/.idea/2024-bang-ggood.iml
new file mode 100644
index 000000000..5e125a0d5
--- /dev/null
+++ b/.idea/2024-bang-ggood.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 000000000..639900d13
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 000000000..35e2d536f
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 000000000..35eb1ddfb
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..4f4badeb3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,20 @@
+# 방끗; 방 구하기 끗, 그래서 방긋 :)
+
+![방방이배너](https://github.com/user-attachments/assets/d8615115-6a82-444b-bbb4-2c03a554884e)
+
+새 출발을 위해 새집을 구해야 하는 당신!
+방을 구한 경험과 지식이 부족한 자취 초보라면, 어떻게 방을 구해야 할지 막막할지도 몰라요.
+
+방을 보는 시간은 **단 10분!**
+
+“어디서부터 봐야 하지? 무엇을 봐야 되지?”
+”내 눈에는 다 좋아보이는데?”
+”방금 봤던 집들을 다 기억하기 힘든데.."
+
+**방끗**은 이런 고민하는 자취 초보들을 위해,
+❗️ 방을 둘러볼 때 깜빡할 수 있는, 유심히 살펴봐야 할 것들을 **체크리스트**로 알려 드려요.
+❗️ 여러분이 중요하게 생각하는 **질문만 모아**서 나만의 체크리스트를 만들 수 있어요.
+❗️ 여러 방을 돌아보며 작성했던 체크리스트들을 비교하여 여러분에게 **최적의 방을 추천**해드려요.
+
+나만의 체크리스트로 최적의 방을 찾아 나가는 과정, 방끗이 함께할게요.
+자취방에서 방긋 웃을 수 있게, 방끗을 사용해 보세요! ☺️
diff --git a/backend/bang-ggood/.DS_Store b/backend/bang-ggood/.DS_Store
new file mode 100644
index 000000000..2f804dcc3
Binary files /dev/null and b/backend/bang-ggood/.DS_Store differ
diff --git a/backend/bang-ggood/.gitignore b/backend/bang-ggood/.gitignore
new file mode 100644
index 000000000..3bee82f80
--- /dev/null
+++ b/backend/bang-ggood/.gitignore
@@ -0,0 +1,39 @@
+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 ###
+src/main/resources/*.yml
+src/test/resources/*.yml
+.idea
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
diff --git a/backend/bang-ggood/build.gradle b/backend/bang-ggood/build.gradle
new file mode 100644
index 000000000..9d9343fbd
--- /dev/null
+++ b/backend/bang-ggood/build.gradle
@@ -0,0 +1,44 @@
+plugins {
+ id 'java'
+ id 'org.springframework.boot' version '3.3.2'
+ id 'io.spring.dependency-management' version '1.1.6'
+}
+
+group = 'com.bang-ggood'
+version = '0.0.1-SNAPSHOT'
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(17)
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+ implementation 'org.springframework.boot:spring-boot-starter-aop'
+ implementation 'org.projectlombok:lombok'
+ implementation 'com.mysql:mysql-connector-j'
+ implementation 'com.opencsv:opencsv:5.9'
+
+ implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
+ implementation 'io.jsonwebtoken:jjwt-impl:0.11.2'
+ implementation 'io.jsonwebtoken:jjwt-gson:0.11.2'
+
+ annotationProcessor 'org.projectlombok:lombok'
+ runtimeOnly 'com.h2database:h2'
+
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testImplementation 'io.rest-assured:rest-assured:5.3.1'
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+}
+
+tasks.named('test') {
+ useJUnitPlatform()
+}
diff --git a/backend/bang-ggood/gradle/wrapper/gradle-wrapper.jar b/backend/bang-ggood/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..e6441136f
Binary files /dev/null and b/backend/bang-ggood/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/backend/bang-ggood/gradle/wrapper/gradle-wrapper.properties b/backend/bang-ggood/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..a4413138c
--- /dev/null
+++ b/backend/bang-ggood/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/backend/bang-ggood/gradlew b/backend/bang-ggood/gradlew
new file mode 100644
index 000000000..b740cf133
--- /dev/null
+++ b/backend/bang-ggood/gradlew
@@ -0,0 +1,249 @@
+#!/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/platforms/jvm/plugins-application/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##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && 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
+ if ! command -v java >/dev/null 2>&1
+ then
+ 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
+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=SC2039,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=SC2039,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, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+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/bang-ggood/gradlew.bat b/backend/bang-ggood/gradlew.bat
new file mode 100644
index 000000000..7101f8e46
--- /dev/null
+++ b/backend/bang-ggood/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. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+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/bang-ggood/settings.gradle b/backend/bang-ggood/settings.gradle
new file mode 100644
index 000000000..3916b736c
--- /dev/null
+++ b/backend/bang-ggood/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'bang-ggood'
diff --git a/backend/bang-ggood/src/.DS_Store b/backend/bang-ggood/src/.DS_Store
new file mode 100644
index 000000000..f6de40efe
Binary files /dev/null and b/backend/bang-ggood/src/.DS_Store differ
diff --git a/backend/bang-ggood/src/main/.DS_Store b/backend/bang-ggood/src/main/.DS_Store
new file mode 100644
index 000000000..a6d2d58b9
Binary files /dev/null and b/backend/bang-ggood/src/main/.DS_Store differ
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/Application.java b/backend/bang-ggood/src/main/java/com/bang_ggood/Application.java
new file mode 100644
index 000000000..537bca4dd
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/Application.java
@@ -0,0 +1,17 @@
+package com.bang_ggood;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+
+@EnableJpaAuditing
+@EnableAspectJAutoProxy
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/BaseEntity.java b/backend/bang-ggood/src/main/java/com/bang_ggood/BaseEntity.java
new file mode 100644
index 000000000..81182c068
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/BaseEntity.java
@@ -0,0 +1,37 @@
+package com.bang_ggood;
+
+import jakarta.persistence.EntityListeners;
+import jakarta.persistence.MappedSuperclass;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+import java.time.LocalDateTime;
+
+@MappedSuperclass
+@EntityListeners(AuditingEntityListener.class)
+public abstract class BaseEntity {
+
+ @CreatedDate
+ private LocalDateTime createdAt;
+
+ @LastModifiedDate
+ private LocalDateTime modifiedAt;
+
+ private boolean deleted = false;
+
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ public LocalDateTime getModifiedAt() {
+ return modifiedAt;
+ }
+
+ public void delete() {
+ this.deleted = true;
+ }
+
+ public boolean isDeleted() {
+ return deleted;
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/article/controller/ArticleController.java b/backend/bang-ggood/src/main/java/com/bang_ggood/article/controller/ArticleController.java
new file mode 100644
index 000000000..ba05787a9
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/article/controller/ArticleController.java
@@ -0,0 +1,51 @@
+package com.bang_ggood.article.controller;
+
+import com.bang_ggood.article.dto.request.ArticleCreateRequest;
+import com.bang_ggood.article.dto.response.ArticleResponse;
+import com.bang_ggood.article.dto.response.ArticlesResponses;
+import com.bang_ggood.article.service.ArticleService;
+import com.bang_ggood.auth.config.AuthRequiredPrincipal;
+import com.bang_ggood.user.domain.User;
+import jakarta.validation.Valid;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+import java.net.URI;
+
+@RestController
+public class ArticleController {
+
+ private final ArticleService articleService;
+
+ public ArticleController(ArticleService articleService) {
+ this.articleService = articleService;
+ }
+
+ @PostMapping("/articles")
+ public ResponseEntity createArticle(@AuthRequiredPrincipal User user,
+ @Valid @RequestBody ArticleCreateRequest request) {
+ Long id = articleService.createArticle(request);
+ return ResponseEntity.created(URI.create("/article/" + id)).build();
+ }
+
+ @GetMapping("/articles/{id}")
+ public ResponseEntity readArticle(@PathVariable("id") Long id) {
+ return ResponseEntity.ok(articleService.readArticle(id));
+ }
+
+ @GetMapping("/articles")
+ public ResponseEntity readArticles() {
+ return ResponseEntity.ok(articleService.readArticles());
+ }
+
+ @DeleteMapping("/articles/{id}")
+ public ResponseEntity deleteArticle(@AuthRequiredPrincipal User user,
+ @PathVariable("id") Long id) {
+ articleService.deleteArticle(id);
+ return ResponseEntity.noContent().build();
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/article/domain/Article.java b/backend/bang-ggood/src/main/java/com/bang_ggood/article/domain/Article.java
new file mode 100644
index 000000000..988238da9
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/article/domain/Article.java
@@ -0,0 +1,69 @@
+package com.bang_ggood.article.domain;
+
+import com.bang_ggood.BaseEntity;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import java.util.Objects;
+
+import static lombok.AccessLevel.PROTECTED;
+
+@Getter
+@NoArgsConstructor(access = PROTECTED)
+@Entity
+public class Article extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private String title;
+
+ private String content;
+
+ private String keyword;
+
+ private String summary;
+
+ private String thumbnail;
+
+ public Article(String title, String content, String keyword, String summary, String thumbnail) {
+ this.title = title;
+ this.content = content;
+ this.keyword = keyword;
+ this.summary = summary;
+ this.thumbnail = thumbnail;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Article article = (Article) o;
+ return Objects.equals(id, article.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(id);
+ }
+
+ @Override
+ public String toString() {
+ return "Article{" +
+ "id=" + id +
+ ", title='" + title + '\'' +
+ ", content='" + content + '\'' +
+ ", keyword='" + keyword + '\'' +
+ ", summary='" + summary + '\'' +
+ ", thumbnail='" + thumbnail + '\'' +
+ '}';
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/article/dto/request/ArticleCreateRequest.java b/backend/bang-ggood/src/main/java/com/bang_ggood/article/dto/request/ArticleCreateRequest.java
new file mode 100644
index 000000000..80429d638
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/article/dto/request/ArticleCreateRequest.java
@@ -0,0 +1,12 @@
+package com.bang_ggood.article.dto.request;
+
+import com.bang_ggood.article.domain.Article;
+import jakarta.validation.constraints.NotBlank;
+
+public record ArticleCreateRequest(@NotBlank(message = "제목을 입력해야 합니다.") String title, String content, String keyword,
+ String summary, String thumbnail) {
+
+ public Article toEntity() {
+ return new Article(title, content, keyword, summary, thumbnail);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/article/dto/response/ArticleResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/article/dto/response/ArticleResponse.java
new file mode 100644
index 000000000..3aded2c35
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/article/dto/response/ArticleResponse.java
@@ -0,0 +1,20 @@
+package com.bang_ggood.article.dto.response;
+
+import com.bang_ggood.article.domain.Article;
+import java.time.LocalDateTime;
+
+public record ArticleResponse(Long articleId, String title, String content, String keyword, String summary,
+ String thumbnail, LocalDateTime createdAt) {
+
+ public static ArticleResponse from(Article article) {
+ return new ArticleResponse(
+ article.getId(),
+ article.getTitle(),
+ article.getContent(),
+ article.getKeyword(),
+ article.getSummary(),
+ article.getThumbnail(),
+ article.getCreatedAt()
+ );
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/article/dto/response/ArticlesResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/article/dto/response/ArticlesResponse.java
new file mode 100644
index 000000000..431b52a15
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/article/dto/response/ArticlesResponse.java
@@ -0,0 +1,18 @@
+package com.bang_ggood.article.dto.response;
+
+import com.bang_ggood.article.domain.Article;
+import java.time.LocalDateTime;
+
+public record ArticlesResponse(Long articleId, String title, String keyword, String summary,
+ LocalDateTime createdAt) {
+
+ public static ArticlesResponse from(Article article) {
+ return new ArticlesResponse(
+ article.getId(),
+ article.getTitle(),
+ article.getKeyword(),
+ article.getSummary(),
+ article.getCreatedAt()
+ );
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/article/dto/response/ArticlesResponses.java b/backend/bang-ggood/src/main/java/com/bang_ggood/article/dto/response/ArticlesResponses.java
new file mode 100644
index 000000000..1156be803
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/article/dto/response/ArticlesResponses.java
@@ -0,0 +1,6 @@
+package com.bang_ggood.article.dto.response;
+
+import java.util.List;
+
+public record ArticlesResponses(List articles) {
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/article/repository/ArticleRepository.java b/backend/bang-ggood/src/main/java/com/bang_ggood/article/repository/ArticleRepository.java
new file mode 100644
index 000000000..eee97d7a1
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/article/repository/ArticleRepository.java
@@ -0,0 +1,34 @@
+package com.bang_ggood.article.repository;
+
+import com.bang_ggood.article.domain.Article;
+import com.bang_ggood.global.exception.BangggoodException;
+import com.bang_ggood.global.exception.ExceptionCode;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import java.util.List;
+import java.util.Optional;
+
+public interface ArticleRepository extends JpaRepository {
+
+ default Article getById(Long id) {
+ return findById(id).orElseThrow(() -> new BangggoodException(ExceptionCode.ARTICLE_NOT_FOUND));
+ }
+
+ @Query("SELECT a FROM Article a " +
+ "WHERE a.id = :id " +
+ "AND a.deleted = false")
+ Optional findById(@Param("id") Long id);
+
+ @Query("SELECT a FROM Article a " +
+ "WHERE a.deleted = false " +
+ "ORDER BY a.createdAt DESC, a.id DESC")
+ List findLatestArticles();
+
+ @Modifying(flushAutomatically = true, clearAutomatically = true)
+ @Query("UPDATE Article a " +
+ "SET a.deleted = true " +
+ "WHERE a.id = :id")
+ void deleteById(@Param("id") Long id);
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/article/service/ArticleService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/article/service/ArticleService.java
new file mode 100644
index 000000000..e76eb776a
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/article/service/ArticleService.java
@@ -0,0 +1,45 @@
+package com.bang_ggood.article.service;
+
+import com.bang_ggood.article.domain.Article;
+import com.bang_ggood.article.dto.request.ArticleCreateRequest;
+import com.bang_ggood.article.dto.response.ArticleResponse;
+import com.bang_ggood.article.dto.response.ArticlesResponse;
+import com.bang_ggood.article.dto.response.ArticlesResponses;
+import com.bang_ggood.article.repository.ArticleRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.List;
+
+@RequiredArgsConstructor
+@Service
+public class ArticleService {
+
+ private final ArticleRepository articleRepository;
+
+ @Transactional
+ public long createArticle(ArticleCreateRequest request) {
+ Article article = request.toEntity();
+ articleRepository.save(article);
+ return article.getId();
+ }
+
+ @Transactional(readOnly = true)
+ public ArticleResponse readArticle(Long id) {
+ Article article = articleRepository.getById(id);
+ return ArticleResponse.from(article);
+ }
+
+ @Transactional(readOnly = true)
+ public ArticlesResponses readArticles() {
+ List articles = articleRepository.findLatestArticles().stream()
+ .map(ArticlesResponse::from)
+ .toList();
+ return new ArticlesResponses(articles);
+ }
+
+ @Transactional
+ public void deleteArticle(Long id) {
+ articleRepository.deleteById(id);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/config/AuthRequiredPrincipal.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/config/AuthRequiredPrincipal.java
new file mode 100644
index 000000000..e3fa7e019
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/config/AuthRequiredPrincipal.java
@@ -0,0 +1,11 @@
+package com.bang_ggood.auth.config;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface AuthRequiredPrincipal {
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/config/AuthRequiredPrincipalArgumentResolver.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/config/AuthRequiredPrincipalArgumentResolver.java
new file mode 100644
index 000000000..7761d0cc0
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/config/AuthRequiredPrincipalArgumentResolver.java
@@ -0,0 +1,40 @@
+package com.bang_ggood.auth.config;
+
+import com.bang_ggood.auth.controller.cookie.CookieResolver;
+import com.bang_ggood.auth.service.AuthService;
+import com.bang_ggood.user.domain.User;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.core.MethodParameter;
+import org.springframework.stereotype.Component;
+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;
+
+@Component
+public class AuthRequiredPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
+
+ private final CookieResolver cookieResolver;
+ private final AuthService authService;
+
+ public AuthRequiredPrincipalArgumentResolver(CookieResolver cookieResolver, AuthService authService) {
+ this.cookieResolver = cookieResolver;
+ this.authService = authService;
+ }
+
+ @Override
+ public boolean supportsParameter(MethodParameter parameter) {
+ return User.class.isAssignableFrom(parameter.getParameterType())
+ && parameter.hasParameterAnnotation(AuthRequiredPrincipal.class);
+ }
+
+ @Override
+ public User resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
+ NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
+ HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
+
+ cookieResolver.checkLoginRequired(request);
+ String token = cookieResolver.extractAccessToken(request);
+ return authService.getAuthUser(token);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/config/UserPrincipal.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/config/UserPrincipal.java
new file mode 100644
index 000000000..4863133e2
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/config/UserPrincipal.java
@@ -0,0 +1,11 @@
+package com.bang_ggood.auth.config;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface UserPrincipal {
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/config/UserPrincipalArgumentResolver.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/config/UserPrincipalArgumentResolver.java
new file mode 100644
index 000000000..c628c799f
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/config/UserPrincipalArgumentResolver.java
@@ -0,0 +1,43 @@
+package com.bang_ggood.auth.config;
+
+import com.bang_ggood.auth.controller.cookie.CookieResolver;
+import com.bang_ggood.auth.service.AuthService;
+import com.bang_ggood.user.domain.User;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.core.MethodParameter;
+import org.springframework.stereotype.Component;
+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;
+
+@Component
+public class UserPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
+
+ private final CookieResolver cookieResolver;
+ private final AuthService authService;
+
+ public UserPrincipalArgumentResolver(CookieResolver cookieResolver, AuthService authService) {
+ this.cookieResolver = cookieResolver;
+ this.authService = authService;
+ }
+
+ @Override
+ public boolean supportsParameter(MethodParameter parameter) {
+ return User.class.isAssignableFrom(parameter.getParameterType())
+ && parameter.hasParameterAnnotation(UserPrincipal.class);
+ }
+
+ @Override
+ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
+ NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
+ HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
+
+ if (cookieResolver.isTokenEmpty(request)) {
+ return authService.assignGuestUser();
+ }
+
+ String token = cookieResolver.extractAccessToken(request);
+ return authService.getAuthUser(token);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/controller/AuthController.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/controller/AuthController.java
new file mode 100644
index 000000000..1bc64613e
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/controller/AuthController.java
@@ -0,0 +1,134 @@
+package com.bang_ggood.auth.controller;
+
+import com.bang_ggood.auth.config.AuthRequiredPrincipal;
+import com.bang_ggood.auth.controller.cookie.CookieProvider;
+import com.bang_ggood.auth.controller.cookie.CookieResolver;
+import com.bang_ggood.auth.dto.request.LocalLoginRequestV1;
+import com.bang_ggood.auth.dto.request.OauthLoginRequest;
+import com.bang_ggood.auth.dto.request.RegisterRequestV1;
+import com.bang_ggood.auth.dto.response.AuthTokenResponse;
+import com.bang_ggood.auth.dto.response.TokenExistResponse;
+import com.bang_ggood.auth.service.AuthService;
+import com.bang_ggood.user.domain.User;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseCookie;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+import java.net.URI;
+
+@RequiredArgsConstructor
+@RestController
+public class AuthController {
+
+ private final AuthService authService;
+ private final CookieProvider cookieProvider;
+ private final CookieResolver cookieResolver;
+
+ @PostMapping("/v1/local-auth/register")
+ public ResponseEntity register(@Valid @RequestBody RegisterRequestV1 request) {
+ Long userId = authService.register(request);
+ return ResponseEntity.created(URI.create("/v1/local-auth/register/" + userId)).build();
+ }
+
+ @DeleteMapping("/v1/withdraw")
+ public ResponseEntity withdraw(@AuthRequiredPrincipal User user,
+ HttpServletRequest httpServletRequest) {
+ String accessToken = cookieResolver.extractAccessToken(httpServletRequest);
+ String refreshToken = cookieResolver.extractRefreshToken(httpServletRequest);
+
+ authService.logout(accessToken, refreshToken);
+
+ ResponseCookie deletedAccessTokenCookie = cookieProvider.deleteAccessTokenCookie();
+ ResponseCookie deletedRefreshTokenCookie = cookieProvider.deleteRefreshTokenCookie();
+
+ authService.withdraw(user);
+
+ return ResponseEntity.noContent()
+ .header(HttpHeaders.SET_COOKIE, deletedAccessTokenCookie.toString())
+ .header(HttpHeaders.SET_COOKIE, deletedRefreshTokenCookie.toString())
+ .build();
+ }
+
+ @PostMapping("/oauth/login")
+ public ResponseEntity oauthLogin(@Valid @RequestBody OauthLoginRequest request) {
+ AuthTokenResponse response = authService.oauthLogin(request);
+
+ ResponseCookie accessTokenCookie = cookieProvider.createAccessTokenCookie(response.accessToken());
+ ResponseCookie refreshTokenCookie = cookieProvider.createRefreshTokenCookie(response.refreshToken());
+
+ return ResponseEntity.ok()
+ .header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString())
+ .header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString())
+ .build();
+ }
+
+ @PostMapping("/v1/local-auth/login")
+ public ResponseEntity localLogin(@Valid @RequestBody LocalLoginRequestV1 request) {
+ AuthTokenResponse response = authService.localLogin(request);
+
+ ResponseCookie accessTokenCookie = cookieProvider.createAccessTokenCookie(response.accessToken());
+ ResponseCookie refreshTokenCookie = cookieProvider.createRefreshTokenCookie(response.refreshToken());
+
+ return ResponseEntity.ok()
+ .header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString())
+ .header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString())
+ .build();
+ }
+
+ @PostMapping("/v1/logout")
+ public ResponseEntity logout(@AuthRequiredPrincipal User user,
+ HttpServletRequest httpServletRequest) {
+ String accessToken = cookieResolver.extractAccessToken(httpServletRequest);
+ String refreshToken = cookieResolver.extractRefreshToken(httpServletRequest);
+
+ authService.logout(accessToken, refreshToken);
+
+ ResponseCookie deletedAccessTokenCookie = cookieProvider.deleteAccessTokenCookie();
+ ResponseCookie deletedRefreshTokenCookie = cookieProvider.deleteRefreshTokenCookie();
+
+ return ResponseEntity.noContent()
+ .header(HttpHeaders.SET_COOKIE, deletedAccessTokenCookie.toString())
+ .header(HttpHeaders.SET_COOKIE, deletedRefreshTokenCookie.toString())
+ .build();
+ }
+
+ @PostMapping("/accessToken/reissue")
+ public ResponseEntity reissueAccessToken(HttpServletRequest httpServletRequest) {
+ cookieResolver.checkLoginRequired(httpServletRequest);
+
+ String refreshToken = cookieResolver.extractRefreshToken(httpServletRequest);
+ String accessToken = authService.reissueAccessToken(refreshToken);
+
+ ResponseCookie accessTokenCookie = cookieProvider.createAccessTokenCookie(accessToken);
+ return ResponseEntity.ok()
+ .header(HttpHeaders.SET_COOKIE, accessTokenCookie.toString())
+ .build();
+ }
+
+ @GetMapping("/token-exist")
+ public ResponseEntity check(HttpServletRequest httpServletRequest) {
+ boolean isAccessTokenExist = !cookieResolver.isAccessTokenEmpty(httpServletRequest);
+ boolean isRefreshTokenExist = !cookieResolver.isRefreshTokenEmpty(httpServletRequest);
+
+ TokenExistResponse tokenExistResponse = TokenExistResponse.from(isAccessTokenExist, isRefreshTokenExist);
+ return ResponseEntity.ok(tokenExistResponse);
+ }
+
+ @DeleteMapping("/token")
+ public ResponseEntity deleteToken() {
+ ResponseCookie deletedAccessTokenCookie = cookieProvider.deleteAccessTokenCookie();
+ ResponseCookie deletedRefreshTokenCookie = cookieProvider.deleteRefreshTokenCookie();
+
+ return ResponseEntity.noContent()
+ .header(HttpHeaders.SET_COOKIE, deletedAccessTokenCookie.toString())
+ .header(HttpHeaders.SET_COOKIE, deletedRefreshTokenCookie.toString())
+ .build();
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/controller/cookie/CookieProvider.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/controller/cookie/CookieProvider.java
new file mode 100644
index 000000000..623a66394
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/controller/cookie/CookieProvider.java
@@ -0,0 +1,69 @@
+package com.bang_ggood.auth.controller.cookie;
+
+import com.bang_ggood.auth.service.jwt.JwtTokenProperties;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.ResponseCookie;
+import org.springframework.stereotype.Component;
+import java.time.Duration;
+
+@Component
+public class CookieProvider {
+
+ protected static final String ACCESS_TOKEN_COOKIE_NAME = "accessToken";
+ protected static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken";
+
+ private final JwtTokenProperties jwtTokenProperties;
+ private final String domain;
+
+ public CookieProvider(@Value("${domain}") String domain,
+ JwtTokenProperties jwtTokenProperties) {
+ this.domain = domain;
+ this.jwtTokenProperties = jwtTokenProperties;
+ }
+
+ public ResponseCookie createAccessTokenCookie(String token) {
+ return createCookie(
+ ACCESS_TOKEN_COOKIE_NAME,
+ token,
+ jwtTokenProperties.getAccessTokenExpirationMillis());
+ }
+
+ public ResponseCookie createRefreshTokenCookie(String token) {
+ return createCookie(
+ REFRESH_TOKEN_COOKIE_NAME,
+ token,
+ jwtTokenProperties.getRefreshTokenExpirationMillis());
+ }
+
+ private ResponseCookie createCookie(String tokenName, String token, long expiredMillis) {
+ return ResponseCookie
+ .from(tokenName, token)
+ .domain(domain)
+ .httpOnly(true)
+ .secure(true)
+ .sameSite("None")
+ .maxAge(Duration.ofMillis(expiredMillis))
+ .path("/")
+ .build();
+ }
+
+ public ResponseCookie deleteAccessTokenCookie() {
+ return deleteCookie(ACCESS_TOKEN_COOKIE_NAME);
+ }
+
+ public ResponseCookie deleteRefreshTokenCookie() {
+ return deleteCookie(REFRESH_TOKEN_COOKIE_NAME);
+ }
+
+ private ResponseCookie deleteCookie(String tokenName) {
+ return ResponseCookie
+ .from(tokenName, "")
+ .domain(domain)
+ .httpOnly(true)
+ .secure(true)
+ .sameSite("None")
+ .maxAge(0)
+ .path("/")
+ .build();
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/controller/cookie/CookieResolver.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/controller/cookie/CookieResolver.java
new file mode 100644
index 000000000..5d69df020
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/controller/cookie/CookieResolver.java
@@ -0,0 +1,65 @@
+package com.bang_ggood.auth.controller.cookie;
+
+import com.bang_ggood.global.exception.BangggoodException;
+import com.bang_ggood.global.exception.ExceptionCode;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Component;
+import java.util.Arrays;
+import java.util.Optional;
+
+@Component
+public class CookieResolver {
+
+ public void checkLoginRequired(HttpServletRequest request) {
+ if (isTokenEmpty(request)) {
+ throw new BangggoodException(ExceptionCode.AUTHENTICATION_TOKEN_EMPTY);
+ }
+
+ if (isRefreshTokenEmpty(request)) {
+ throw new BangggoodException(ExceptionCode.AUTHENTICATION_REFRESH_TOKEN_EMPTY);
+ }
+ }
+
+ public boolean isAccessTokenEmpty(HttpServletRequest request) {
+ return isTokenEmpty(request, CookieProvider.ACCESS_TOKEN_COOKIE_NAME);
+ }
+
+ public boolean isRefreshTokenEmpty(HttpServletRequest request) {
+ return isTokenEmpty(request, CookieProvider.REFRESH_TOKEN_COOKIE_NAME);
+ }
+
+ private boolean isTokenEmpty(HttpServletRequest request, String cookieName) {
+ if (request.getCookies() == null) {
+ return true;
+ }
+
+ return Arrays.stream(request.getCookies())
+ .noneMatch(cookie -> cookie.getName().equals(cookieName));
+ }
+
+ public String extractAccessToken(HttpServletRequest request) {
+ return extractToken(request, CookieProvider.ACCESS_TOKEN_COOKIE_NAME)
+ .orElseThrow(() -> new BangggoodException(ExceptionCode.AUTHENTICATION_ACCESS_TOKEN_EMPTY));
+ }
+
+ public String extractRefreshToken(HttpServletRequest request) {
+ return extractToken(request, CookieProvider.REFRESH_TOKEN_COOKIE_NAME)
+ .orElseThrow(() -> new BangggoodException(ExceptionCode.AUTHENTICATION_REFRESH_TOKEN_EMPTY));
+ }
+
+ private Optional extractToken(HttpServletRequest request, String cookieName) {
+ if (isTokenEmpty(request)) {
+ return Optional.empty();
+ }
+
+ return Arrays.stream(request.getCookies())
+ .filter(cookie -> cookie.getName().equals(cookieName))
+ .findAny()
+ .map(Cookie::getValue);
+ }
+
+ public boolean isTokenEmpty(HttpServletRequest request) {
+ return isAccessTokenEmpty(request) && isRefreshTokenEmpty(request);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/request/LocalLoginRequestV1.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/request/LocalLoginRequestV1.java
new file mode 100644
index 000000000..82d558aa6
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/request/LocalLoginRequestV1.java
@@ -0,0 +1,7 @@
+package com.bang_ggood.auth.dto.request;
+
+import jakarta.validation.constraints.NotBlank;
+
+public record LocalLoginRequestV1(@NotBlank(message = "이메일이 존재하지 않습니다.") String email,
+ @NotBlank(message = "비밀번호가 존재하지 않습니다.") String password) {
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/request/OauthLoginRequest.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/request/OauthLoginRequest.java
new file mode 100644
index 000000000..d7d0107de
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/request/OauthLoginRequest.java
@@ -0,0 +1,7 @@
+package com.bang_ggood.auth.dto.request;
+
+import jakarta.validation.constraints.NotBlank;
+
+public record OauthLoginRequest(@NotBlank(message = "인가 코드가 존재하지 않습니다.") String code,
+ @NotBlank(message = "Redirect Uri가 존재하지 않습니다.") String redirectUri) {
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/request/OauthTokenRequest.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/request/OauthTokenRequest.java
new file mode 100644
index 000000000..c386463f3
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/request/OauthTokenRequest.java
@@ -0,0 +1,10 @@
+package com.bang_ggood.auth.dto.request;
+
+public record OauthTokenRequest(String grantType, String clientId,
+ String redirectUri, String code, String clientSecret) {
+
+ public static OauthTokenRequest of(String grantType, String clientId,
+ String redirectUri, String code, String clientSecret) {
+ return new OauthTokenRequest(grantType, clientId, redirectUri, code, clientSecret);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/request/RegisterRequestV1.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/request/RegisterRequestV1.java
new file mode 100644
index 000000000..c82381d0d
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/request/RegisterRequestV1.java
@@ -0,0 +1,15 @@
+package com.bang_ggood.auth.dto.request;
+
+import com.bang_ggood.user.domain.LoginType;
+import com.bang_ggood.user.domain.User;
+import com.bang_ggood.user.domain.UserType;
+import jakarta.validation.constraints.NotBlank;
+
+public record RegisterRequestV1(@NotBlank(message = "이름이 존재하지 않습니다.") String name,
+ @NotBlank(message = "이메일이 존재하지 않습니다.") String email,
+ @NotBlank(message = "비밀번호가 존재하지 않습니다.") String password) {
+
+ public User toUserEntity() {
+ return new User(name(), email(), password(), UserType.USER, LoginType.LOCAL);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/AuthTokenResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/AuthTokenResponse.java
new file mode 100644
index 000000000..aa8613b1d
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/AuthTokenResponse.java
@@ -0,0 +1,8 @@
+package com.bang_ggood.auth.dto.response;
+
+public record AuthTokenResponse(String accessToken, String refreshToken) {
+
+ public static AuthTokenResponse of(String accessToken, String refreshToken) {
+ return new AuthTokenResponse(accessToken, refreshToken);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/KakaoAccountResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/KakaoAccountResponse.java
new file mode 100644
index 000000000..bd086067f
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/KakaoAccountResponse.java
@@ -0,0 +1,7 @@
+package com.bang_ggood.auth.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record KakaoAccountResponse(String email, String name, ProfileResponse profile) {
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/OauthInfoApiResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/OauthInfoApiResponse.java
new file mode 100644
index 000000000..a001283c6
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/OauthInfoApiResponse.java
@@ -0,0 +1,14 @@
+package com.bang_ggood.auth.dto.response;
+
+import com.bang_ggood.user.domain.LoginType;
+import com.bang_ggood.user.domain.User;
+import com.bang_ggood.user.domain.UserType;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record OauthInfoApiResponse(String id, String connected_at, KakaoAccountResponse kakao_account) {
+
+ public User toUserEntity() {
+ return new User(kakao_account.profile().nickname(), kakao_account.email(), UserType.USER, LoginType.KAKAO);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/OauthTokenApiResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/OauthTokenApiResponse.java
new file mode 100644
index 000000000..296ae39d1
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/OauthTokenApiResponse.java
@@ -0,0 +1,9 @@
+package com.bang_ggood.auth.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record OauthTokenApiResponse(String token_type, String access_token,
+ String expires_in, String refresh_token,
+ String refresh_token_expires_in, String scope) {
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/ProfileResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/ProfileResponse.java
new file mode 100644
index 000000000..1151418ee
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/ProfileResponse.java
@@ -0,0 +1,7 @@
+package com.bang_ggood.auth.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record ProfileResponse(String nickname, String thumbnail_image_url, String profile_image_url) {
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/TokenExistResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/TokenExistResponse.java
new file mode 100644
index 000000000..98cc46d6a
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/dto/response/TokenExistResponse.java
@@ -0,0 +1,8 @@
+package com.bang_ggood.auth.dto.response;
+
+public record TokenExistResponse(boolean isAccessTokenExist, boolean isRefreshTokenExist) {
+
+ public static TokenExistResponse from(boolean isAccessTokenExist, boolean isRefreshTokenExist) {
+ return new TokenExistResponse(isAccessTokenExist, isRefreshTokenExist);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/AuthService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/AuthService.java
new file mode 100644
index 000000000..be277db0e
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/AuthService.java
@@ -0,0 +1,143 @@
+package com.bang_ggood.auth.service;
+
+import com.bang_ggood.auth.dto.request.LocalLoginRequestV1;
+import com.bang_ggood.auth.dto.request.OauthLoginRequest;
+import com.bang_ggood.auth.dto.request.RegisterRequestV1;
+import com.bang_ggood.auth.dto.response.AuthTokenResponse;
+import com.bang_ggood.auth.dto.response.OauthInfoApiResponse;
+import com.bang_ggood.auth.service.jwt.JwtTokenProvider;
+import com.bang_ggood.auth.service.jwt.JwtTokenResolver;
+import com.bang_ggood.auth.service.oauth.OauthClient;
+import com.bang_ggood.global.DefaultChecklistService;
+import com.bang_ggood.global.exception.BangggoodException;
+import com.bang_ggood.global.exception.ExceptionCode;
+import com.bang_ggood.user.domain.Email;
+import com.bang_ggood.user.domain.LoginType;
+import com.bang_ggood.user.domain.User;
+import com.bang_ggood.user.domain.UserType;
+import com.bang_ggood.user.repository.UserRepository;
+import lombok.RequiredArgsConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.List;
+
+@RequiredArgsConstructor
+@Service
+public class AuthService {
+
+ private static final Logger log = LoggerFactory.getLogger(AuthService.class);
+ private static final int GUEST_USER_LIMIT = 1;
+
+ private final OauthClient oauthClient;
+ private final JwtTokenProvider jwtTokenProvider;
+ private final JwtTokenResolver jwtTokenResolver;
+ private final DefaultChecklistService defaultChecklistService;
+ private final UserRepository userRepository;
+
+ @Transactional
+ public Long register(RegisterRequestV1 request) {
+ User user = processUser(LoginType.LOCAL, request.toUserEntity(), true);
+ return user.getId();
+ }
+
+ @Transactional
+ public AuthTokenResponse oauthLogin(OauthLoginRequest request) {
+ OauthInfoApiResponse oauthInfo = oauthClient.requestOauthInfo(request);
+ User user = processUser(LoginType.KAKAO, oauthInfo.toUserEntity(), false);
+ return createAuthTokenResponse(user);
+ }
+
+ private User processUser(LoginType loginType, User user, boolean isRegistration) {
+ return userRepository.findByEmailAndLoginTypeWithDeleted(user.getEmail(), loginType)
+ .map(savedUser -> handleExistingUser(savedUser, loginType, isRegistration))
+ .orElseGet(() -> signUp(user));
+ }
+
+ private User handleExistingUser(User user, LoginType loginType, boolean isRegistrationRequest) {
+ validateRegister(user, isRegistrationRequest);
+ restoreUser(user, loginType);
+ return user;
+ }
+
+ private void validateRegister(User user, boolean isRegistrationRequest) {
+ if (!user.isDeleted() && isRegistrationRequest) {
+ throw new BangggoodException(ExceptionCode.USER_EMAIL_ALREADY_USED);
+ }
+ }
+
+ private void restoreUser(User user, LoginType loginType) {
+ if (user.isDeleted()) {
+ userRepository.resaveByEmailAndLoginType(user.getEmail(), loginType);
+ }
+ }
+
+ private AuthTokenResponse createAuthTokenResponse(User user) {
+ String accessToken = jwtTokenProvider.createAccessToken(user);
+ String refreshToken = jwtTokenProvider.createRefreshToken(user);
+ return AuthTokenResponse.of(accessToken, refreshToken);
+ }
+
+
+ @Transactional
+ public void withdraw(User user) {
+ userRepository.deleteById(user.getId());
+ }
+
+ @Transactional(readOnly = true)
+ public AuthTokenResponse localLogin(LocalLoginRequestV1 request) {
+ User user = userRepository.findByEmailAndLoginType(new Email(request.email()), LoginType.LOCAL)
+ .orElseThrow(() -> new BangggoodException(ExceptionCode.USER_NOT_FOUND));
+ checkPassword(request, user);
+
+ return createAuthTokenResponse(user);
+ }
+
+
+ private void checkPassword(LocalLoginRequestV1 request, User user) {
+ if (user.isDifferent(request.password())) {
+ throw new BangggoodException(ExceptionCode.USER_INVALID_PASSWORD);
+ }
+ }
+
+ private User signUp(User user) {
+ User savedUser = userRepository.save(user);
+ defaultChecklistService.createDefaultChecklistAndQuestions(savedUser);
+ return savedUser;
+ }
+
+ @Transactional(readOnly = true)
+ public User assignGuestUser() {
+ List foundGuestUser = userRepository.findUserByUserType(UserType.GUEST);
+
+ if (foundGuestUser.size() > GUEST_USER_LIMIT) {
+ throw new BangggoodException(ExceptionCode.GUEST_USER_UNEXPECTED_EXIST);
+ }
+
+ return foundGuestUser.stream()
+ .findFirst()
+ .orElseThrow(() -> new BangggoodException(ExceptionCode.GUEST_USER_NOT_FOUND));
+ }
+
+ public void logout(String accessToken, String refreshToken) {
+ AuthUser accessAuthUser = jwtTokenResolver.resolveAccessToken(accessToken);
+ AuthUser refreshAuthUser = jwtTokenResolver.resolveRefreshToken(refreshToken);
+ if (!accessAuthUser.id().equals(refreshAuthUser.id())) {
+ throw new BangggoodException(ExceptionCode.AUTHENTICATION_TOKEN_USER_MISMATCH);
+ }
+ }
+
+ @Transactional(readOnly = true)
+ public User getAuthUser(String token) {
+ AuthUser authUser = jwtTokenResolver.resolveAccessToken(token);
+ return userRepository.getUserById(authUser.id());
+ }
+
+ @Transactional(readOnly = true)
+ public String reissueAccessToken(String refreshToken) {
+ AuthUser authUser = jwtTokenResolver.resolveRefreshToken(refreshToken);
+ User user = userRepository.getUserById(authUser.id());
+ return jwtTokenProvider.createAccessToken(user);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/AuthUser.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/AuthUser.java
new file mode 100644
index 000000000..494c32666
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/AuthUser.java
@@ -0,0 +1,10 @@
+package com.bang_ggood.auth.service;
+
+import jakarta.validation.constraints.NotNull;
+
+public record AuthUser(@NotNull Long id) {
+
+ public static AuthUser from(Long id) {
+ return new AuthUser(id);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/PasswordEncoder.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/PasswordEncoder.java
new file mode 100644
index 000000000..8996642b3
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/PasswordEncoder.java
@@ -0,0 +1,58 @@
+package com.bang_ggood.auth.service;
+
+import com.bang_ggood.global.exception.BangggoodException;
+import com.bang_ggood.global.exception.ExceptionCode;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.util.Base64;
+
+public class PasswordEncoder {
+
+ private static final String DELIMITER = ":";
+ private static final int PASSWORD_AND_SALT_LENGTH = 2;
+
+ private PasswordEncoder() {
+ }
+
+ public static String encodeWithGeneralSalt(String password) {
+ return encode(password, getSalt());
+ }
+
+ public static String encodeWithSpecificSalt(String password, String passwordWithSalt) {
+ return encode(password, extractSaltByPassword(passwordWithSalt));
+ }
+
+ private static String encode(String password, byte[] salt) {
+ try {
+ KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, 512);
+ SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
+ byte[] hash = factory.generateSecret(spec).getEncoded();
+ String encodedPassword = Base64.getEncoder().encodeToString(hash);
+ String encodedSalt = Base64.getEncoder().encodeToString(salt);
+ return encodedPassword + DELIMITER + encodedSalt;
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+ throw new BangggoodException(ExceptionCode.PASSWORD_HASHING_ERROR);
+ }
+ }
+
+ private static byte[] getSalt() {
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] salt = new byte[64];
+ secureRandom.nextBytes(salt);
+ return salt;
+ }
+
+ private static byte[] extractSaltByPassword(String encodedPassword) {
+ String[] parts = encodedPassword.split(DELIMITER);
+ if (parts.length != PASSWORD_AND_SALT_LENGTH) {
+ throw new BangggoodException(ExceptionCode.PASSWORD_HASHING_ERROR);
+ }
+
+ String encodedSalt = parts[1];
+ return Base64.getDecoder().decode(encodedSalt);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/TokenType.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/TokenType.java
new file mode 100644
index 000000000..99e3ad747
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/TokenType.java
@@ -0,0 +1,6 @@
+package com.bang_ggood.auth.service;
+
+public enum TokenType {
+
+ ACCESS_TOKEN, REFRESH_TOKEN;
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/jwt/JwtTokenProperties.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/jwt/JwtTokenProperties.java
new file mode 100644
index 000000000..dbca1ad1a
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/jwt/JwtTokenProperties.java
@@ -0,0 +1,37 @@
+package com.bang_ggood.auth.service.jwt;
+
+import io.jsonwebtoken.security.Keys;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import javax.crypto.SecretKey;
+
+@Component
+public class JwtTokenProperties {
+
+ protected static final String TOKEN_TYPE = "type";
+
+ private final String secretKey;
+ private final long accessTokenExpirationMillis;
+ private final long refreshTokenExpirationMillis;
+
+ public JwtTokenProperties(
+ @Value("${jwt.secret-key}") String secretKey,
+ @Value("${jwt.accessToken-expiration-millis}") long accessTokenExpirationMillis,
+ @Value("${jwt.refreshToken-expiration-millis}") long refreshTokenExpirationMillis) {
+ this.secretKey = secretKey;
+ this.accessTokenExpirationMillis = accessTokenExpirationMillis;
+ this.refreshTokenExpirationMillis = refreshTokenExpirationMillis;
+ }
+
+ public long getAccessTokenExpirationMillis() {
+ return accessTokenExpirationMillis;
+ }
+
+ public long getRefreshTokenExpirationMillis() {
+ return refreshTokenExpirationMillis;
+ }
+
+ public SecretKey getSecretKey() {
+ return Keys.hmacShaKeyFor(secretKey.getBytes());
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/jwt/JwtTokenProvider.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/jwt/JwtTokenProvider.java
new file mode 100644
index 000000000..44db0bf47
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/jwt/JwtTokenProvider.java
@@ -0,0 +1,38 @@
+package com.bang_ggood.auth.service.jwt;
+
+import com.bang_ggood.auth.service.TokenType;
+import com.bang_ggood.user.domain.User;
+import io.jsonwebtoken.Jwts;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import java.util.Date;
+
+@RequiredArgsConstructor
+@Component
+public class JwtTokenProvider {
+
+ private final JwtTokenProperties jwtTokenProperties;
+
+ public String createAccessToken(User user) {
+ long accessTokenExpirationMillis = jwtTokenProperties.getAccessTokenExpirationMillis();
+ return createToken(user, accessTokenExpirationMillis, TokenType.ACCESS_TOKEN);
+ }
+
+ public String createRefreshToken(User user) {
+ long refreshTokenExpirationMillis = jwtTokenProperties.getRefreshTokenExpirationMillis();
+ return createToken(user, refreshTokenExpirationMillis, TokenType.REFRESH_TOKEN);
+ }
+
+ private String createToken(User user, long expirationMillis, TokenType tokenType) {
+ Date now = new Date();
+ Date expiredDate = new Date(now.getTime() + expirationMillis);
+
+ return Jwts.builder()
+ .setSubject(user.getId().toString())
+ .setIssuedAt(now)
+ .setExpiration(expiredDate)
+ .claim(JwtTokenProperties.TOKEN_TYPE, tokenType.name())
+ .signWith(jwtTokenProperties.getSecretKey())
+ .compact();
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/jwt/JwtTokenResolver.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/jwt/JwtTokenResolver.java
new file mode 100644
index 000000000..cb38bb13c
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/jwt/JwtTokenResolver.java
@@ -0,0 +1,53 @@
+package com.bang_ggood.auth.service.jwt;
+
+import com.bang_ggood.auth.service.AuthUser;
+import com.bang_ggood.auth.service.TokenType;
+import com.bang_ggood.global.exception.BangggoodException;
+import com.bang_ggood.global.exception.ExceptionCode;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.Jwts;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+@RequiredArgsConstructor
+@Component
+public class JwtTokenResolver {
+
+ private final JwtTokenProperties jwtTokenProperties;
+
+ public AuthUser resolveAccessToken(String token) {
+ return resolveTokenByType(token, TokenType.ACCESS_TOKEN);
+ }
+
+ public AuthUser resolveRefreshToken(String token) {
+ return resolveTokenByType(token, TokenType.REFRESH_TOKEN);
+ }
+
+ private AuthUser resolveTokenByType(String token, TokenType tokenType) {
+ try {
+ Claims claims = Jwts.parserBuilder()
+ .setSigningKey(jwtTokenProperties.getSecretKey())
+ .build()
+ .parseClaimsJws(token)
+ .getBody();
+
+ validateTokenType(claims, tokenType);
+
+ Long id = Long.valueOf(claims.getSubject());
+ return AuthUser.from(id);
+ } catch (ExpiredJwtException exception) {
+ throw new BangggoodException(ExceptionCode.AUTHENTICATION_TOKEN_EXPIRED);
+ } catch (JwtException exception) {
+ throw new BangggoodException(ExceptionCode.AUTHENTICATION_TOKEN_INVALID);
+ }
+ }
+
+ private void validateTokenType(Claims claims, TokenType tokenType) {
+ String extractTokenType = claims.get(JwtTokenProperties.TOKEN_TYPE, String.class);
+ if (!extractTokenType.equals(tokenType.name())) {
+ throw new BangggoodException(ExceptionCode.AUTHENTICATION_TOKEN_TYPE_MISMATCH);
+ }
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/oauth/OauthClient.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/oauth/OauthClient.java
new file mode 100644
index 000000000..2383df596
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/oauth/OauthClient.java
@@ -0,0 +1,49 @@
+package com.bang_ggood.auth.service.oauth;
+
+import com.bang_ggood.auth.dto.request.OauthLoginRequest;
+import com.bang_ggood.auth.dto.response.OauthInfoApiResponse;
+import com.bang_ggood.auth.dto.response.OauthTokenApiResponse;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestClient;
+
+@Component
+public class OauthClient {
+
+ private final RestClient restClient;
+ private final OauthRequestProperties oauthRequestProperties;
+
+ public OauthClient(
+ RestClient restClient,
+ OauthRequestProperties oauthRequestProperties) {
+ this.restClient = restClient;
+ this.oauthRequestProperties = oauthRequestProperties;
+ }
+
+ public OauthInfoApiResponse requestOauthInfo(OauthLoginRequest request) {
+ OauthTokenApiResponse oauthTokenApiResponse = requestToken(request);
+
+ String userInfoRequestUri = oauthRequestProperties.getUserInfoRequestUri();
+ String headerName = "Authorization";
+ String headerValue = "Bearer " + oauthTokenApiResponse.access_token();
+
+ return restClient.get()
+ .uri(userInfoRequestUri)
+ .header(headerName, headerValue)
+ .retrieve()
+ .body(OauthInfoApiResponse.class);
+ }
+
+ private OauthTokenApiResponse requestToken(OauthLoginRequest request) {
+ String tokenRequestUri = oauthRequestProperties.getTokenRequestUri();
+ MultiValueMap tokenRequestBody = oauthRequestProperties.createTokenRequestBody(request);
+
+ return restClient.post()
+ .uri(tokenRequestUri)
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .body(tokenRequestBody)
+ .retrieve()
+ .body(OauthTokenApiResponse.class);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/oauth/OauthRequestProperties.java b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/oauth/OauthRequestProperties.java
new file mode 100644
index 000000000..bd55a12a8
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/auth/service/oauth/OauthRequestProperties.java
@@ -0,0 +1,76 @@
+package com.bang_ggood.auth.service.oauth;
+
+import com.bang_ggood.auth.dto.request.OauthLoginRequest;
+import com.bang_ggood.global.exception.BangggoodException;
+import com.bang_ggood.global.exception.ExceptionCode;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import java.util.Arrays;
+import java.util.List;
+
+@Component
+public class OauthRequestProperties {
+
+ private final String tokenRequestUri;
+ private final String userInfoRequestUri;
+ private final String grantType;
+ private final String clientId;
+ private final String clientSecret;
+ private final List registeredRedirectUris;
+
+ public OauthRequestProperties(
+ @Value("${kakao.token_post_uri}") String tokenPostUri,
+ @Value("${kakao.user_get_uri}") String userInfoRequestUri,
+ @Value("${kakao.grant_type}") String grantType,
+ @Value("${kakao.client_id}") String clientId,
+ @Value("${kakao.redirect_uris}") String registeredRedirectUris,
+ @Value("${kakao.client_secret}") String clientSecret) {
+ this.tokenRequestUri = tokenPostUri;
+ this.userInfoRequestUri = userInfoRequestUri;
+ this.grantType = grantType;
+ this.clientId = clientId;
+ this.registeredRedirectUris = convertToList(registeredRedirectUris);
+ this.clientSecret = clientSecret;
+ }
+
+ private List convertToList(String registeredRedirectUris) {
+ return Arrays.stream(registeredRedirectUris.split(","))
+ .map(String::trim)
+ .toList();
+ }
+
+ public MultiValueMap createTokenRequestBody(OauthLoginRequest request) {
+ String matchingRedirectUri = findMatchingRedirectUri(request.redirectUri());
+ String code = request.code();
+
+ MultiValueMap map = new LinkedMultiValueMap<>();
+ map.add("grant_type", grantType);
+ map.add("client_id", clientId);
+ map.add("redirect_uri", matchingRedirectUri);
+ map.add("code", code);
+ map.add("client_secret", clientSecret);
+
+ return map;
+ }
+
+ private String findMatchingRedirectUri(String redirectUri) {
+ return registeredRedirectUris.stream()
+ .filter(each -> each.equals(redirectUri))
+ .findAny()
+ .orElseThrow(() -> new BangggoodException(ExceptionCode.OAUTH_REDIRECT_URI_MISMATCH));
+ }
+
+ public String getTokenRequestUri() {
+ return tokenRequestUri;
+ }
+
+ public String getUserInfoRequestUri() {
+ return userInfoRequestUri;
+ }
+
+ public List getRegisteredRedirectUris() {
+ return registeredRedirectUris;
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/controller/ChecklistController.java b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/controller/ChecklistController.java
new file mode 100644
index 000000000..693e10947
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/controller/ChecklistController.java
@@ -0,0 +1,100 @@
+package com.bang_ggood.checklist.controller;
+
+import com.bang_ggood.auth.config.AuthRequiredPrincipal;
+import com.bang_ggood.auth.config.UserPrincipal;
+import com.bang_ggood.checklist.dto.request.ChecklistRequest;
+import com.bang_ggood.checklist.dto.request.ChecklistRequestV1;
+import com.bang_ggood.checklist.dto.response.ChecklistsPreviewResponse;
+import com.bang_ggood.checklist.dto.response.ChecklistsPreviewResponseV1;
+import com.bang_ggood.checklist.dto.response.SelectedChecklistResponse;
+import com.bang_ggood.checklist.dto.response.SelectedChecklistResponseV1;
+import com.bang_ggood.checklist.service.ChecklistManageService;
+import com.bang_ggood.user.domain.User;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+import java.net.URI;
+
+@RequiredArgsConstructor
+@RestController
+public class ChecklistController {
+
+ private final ChecklistManageService checklistManageService;
+
+ @PostMapping("/checklists")
+ public ResponseEntity createChecklist(@AuthRequiredPrincipal User user,
+ @Valid @RequestBody ChecklistRequest checklistRequest) {
+ long checklistId = checklistManageService.createChecklist(user, checklistRequest);
+ return ResponseEntity.created(URI.create("/checklist/" + checklistId)).build();
+ }
+
+ @PostMapping("/v1/checklists")
+ public ResponseEntity createChecklistV1(@AuthRequiredPrincipal User user,
+ @Valid @RequestBody ChecklistRequestV1 checklistRequestV1) {
+ long checklistId = checklistManageService.createChecklistV1(user, checklistRequestV1);
+ return ResponseEntity.created(URI.create("/checklist/" + checklistId)).build();
+ }
+
+ @GetMapping("/checklists/{id}")
+ public ResponseEntity readChecklistById(@UserPrincipal User user,
+ @PathVariable("id") Long checklistId) {
+ return ResponseEntity.ok(checklistManageService.readChecklist(user, checklistId));
+ }
+
+ @GetMapping("v1/checklists/{id}")
+ public ResponseEntity readChecklistByIdV1(@UserPrincipal User user,
+ @PathVariable("id") Long checklistId) {
+ return ResponseEntity.ok(checklistManageService.readChecklistV1(user, checklistId));
+ }
+
+ @GetMapping("/checklists")
+ public ResponseEntity readChecklistsPreview(@UserPrincipal User user) {
+ return ResponseEntity.ok(checklistManageService.readAllChecklistsPreview(user));
+ }
+
+ @GetMapping("/v1/checklists")
+ public ResponseEntity readChecklistsPreviewV1(@UserPrincipal User user) {
+ return ResponseEntity.ok(checklistManageService.readAllChecklistsPreviewV1(user));
+ }
+
+ @GetMapping("/checklists/like")
+ public ResponseEntity readLikedChecklistsPreview(@AuthRequiredPrincipal User user) {
+ return ResponseEntity.ok(checklistManageService.readLikedChecklistsPreview(user));
+ }
+
+ @GetMapping("/v1/checklists/like")
+ public ResponseEntity readLikedChecklistsPreviewV1(@AuthRequiredPrincipal User user) {
+ return ResponseEntity.ok(checklistManageService.readLikedChecklistsPreviewV1(user));
+ }
+
+ @PutMapping("/checklists/{id}")
+ public ResponseEntity updateChecklistById(
+ @AuthRequiredPrincipal User user,
+ @PathVariable("id") long id,
+ @Valid @RequestBody ChecklistRequest checklistRequest) {
+ checklistManageService.updateChecklistById(user, id, checklistRequest);
+ return ResponseEntity.noContent().build();
+ }
+
+ @PutMapping("/v1/checklists/{id}")
+ public ResponseEntity updateChecklistByIdV1(
+ @AuthRequiredPrincipal User user,
+ @PathVariable("id") long id,
+ @Valid @RequestBody ChecklistRequestV1 checklistRequestV1) {
+ checklistManageService.updateChecklistByIdV1(user, id, checklistRequestV1);
+ return ResponseEntity.noContent().build();
+ }
+
+ @DeleteMapping("/checklists/{id}")
+ public ResponseEntity deleteChecklistById(@AuthRequiredPrincipal User user, @PathVariable("id") long id) {
+ checklistManageService.deleteChecklistById(user, id);
+ return ResponseEntity.noContent().build();
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/domain/Checklist.java b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/domain/Checklist.java
new file mode 100644
index 000000000..4c88eac62
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/domain/Checklist.java
@@ -0,0 +1,194 @@
+package com.bang_ggood.checklist.domain;
+
+import com.bang_ggood.BaseEntity;
+import com.bang_ggood.contract.domain.OccupancyMonth;
+import com.bang_ggood.contract.domain.OccupancyPeriod;
+import com.bang_ggood.global.exception.BangggoodException;
+import com.bang_ggood.global.exception.ExceptionCode;
+import com.bang_ggood.question.domain.ChecklistQuestion;
+import com.bang_ggood.room.domain.FloorLevel;
+import com.bang_ggood.room.domain.Room;
+import com.bang_ggood.room.domain.Structure;
+import com.bang_ggood.user.domain.User;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.OneToOne;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import java.util.List;
+import java.util.Objects;
+
+import static lombok.AccessLevel.PROTECTED;
+
+@Getter
+@NoArgsConstructor(access = PROTECTED)
+@Entity
+public class Checklist extends BaseEntity {
+
+ private static final int MEMO_MAX_LENGTH = 1000;
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @OneToOne(fetch = FetchType.LAZY)
+ private Room room;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ private User user;
+
+ private Integer deposit;
+
+ private Integer rent;
+
+ private Integer maintenanceFee;
+
+ private Integer contractTerm;
+
+ @Enumerated(EnumType.STRING)
+ private OccupancyMonth occupancyMonth;
+
+ @Enumerated(EnumType.STRING)
+ private OccupancyPeriod occupancyPeriod;
+
+ private String realEstate;
+
+ private String memo;
+
+ private String summary;
+
+ @OneToMany(mappedBy = "checklist")
+ private List questions;
+
+ public Checklist(Room room, User user, Integer deposit, Integer rent, Integer maintenanceFee,
+ Integer contractTerm, OccupancyMonth occupancyMonth, OccupancyPeriod occupancyPeriod,
+ String realEstate, String memo, String summary) {
+ this.room = room;
+ this.user = user;
+ this.deposit = deposit;
+ this.rent = rent;
+ this.maintenanceFee = maintenanceFee;
+ this.contractTerm = contractTerm;
+ this.occupancyMonth = occupancyMonth;
+ this.occupancyPeriod = occupancyPeriod;
+ this.realEstate = realEstate;
+ this.memo = memo;
+ this.summary = summary;
+ validateMemoLength();
+ }
+
+ public boolean isOwnedBy(User user) {
+ return this.user.equals(user);
+ }
+
+ public void change(Checklist updateChecklist) {
+ this.room = updateChecklist.room;
+ this.deposit = updateChecklist.deposit;
+ this.rent = updateChecklist.rent;
+ this.maintenanceFee = updateChecklist.maintenanceFee;
+ this.contractTerm = updateChecklist.contractTerm;
+ this.occupancyMonth = updateChecklist.occupancyMonth;
+ this.occupancyPeriod = updateChecklist.occupancyPeriod;
+ this.realEstate = updateChecklist.realEstate;
+ this.memo = updateChecklist.memo;
+ this.summary = updateChecklist.summary;
+ validateMemoLength();
+ }
+
+ private void validateMemoLength() {
+ if (memo != null && memo.length() > MEMO_MAX_LENGTH) {
+ throw new BangggoodException(ExceptionCode.CHECKLIST_MEMO_INVALID_LENGTH);
+ }
+ }
+
+ public Long getRoomId() {
+ return room.getId();
+ }
+
+ public String getRoomName() {
+ return room.getName();
+ }
+
+ public String getRoomAddress() {
+ return room.getAddress();
+ }
+
+ public String getRoomBuildingName() {
+ return room.getBuildingName();
+ }
+
+ public Integer getRoomFloor() {
+ return room.getFloor();
+ }
+
+ public String getRoomStation() {
+ return room.getStation();
+ }
+
+ public Integer getRoomWalkingTime() {
+ return room.getWalkingTime();
+ }
+
+ public Double getRoomSize() {
+ return room.getSize();
+ }
+
+ public FloorLevel getRoomFloorLevel() {
+ return room.getFloorLevel();
+ }
+
+ public Structure getRoomStructure() {
+ return room.getStructure();
+ }
+
+ public Integer getOccupancyMonth() {
+ return occupancyMonth.getMonth();
+ }
+
+ public String getOccupancyPeriod() {
+ return occupancyPeriod.getPeriod();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Checklist checklist = (Checklist) o;
+ return Objects.equals(id, checklist.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(id);
+ }
+
+ @Override
+ public String toString() {
+ return "Checklist{" +
+ "id=" + id +
+ ", room=" + room +
+ ", user=" + user +
+ ", deposit=" + deposit +
+ ", rent=" + rent +
+ ", maintenanceFee=" + maintenanceFee +
+ ", contractTerm=" + contractTerm +
+ ", occupancyMonth=" + occupancyMonth +
+ ", occupancyPeriod=" + occupancyPeriod +
+ ", realEstate='" + realEstate + '\'' +
+ ", memo='" + memo + '\'' +
+ ", summary='" + summary + '\'' +
+ ", questions=" + questions +
+ '}';
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/request/ChecklistRequest.java b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/request/ChecklistRequest.java
new file mode 100644
index 000000000..515f72079
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/request/ChecklistRequest.java
@@ -0,0 +1,25 @@
+package com.bang_ggood.checklist.dto.request;
+
+import com.bang_ggood.checklist.domain.Checklist;
+import com.bang_ggood.contract.domain.OccupancyMonth;
+import com.bang_ggood.contract.domain.OccupancyPeriod;
+import com.bang_ggood.question.dto.request.QuestionRequest;
+import com.bang_ggood.room.domain.Room;
+import com.bang_ggood.room.dto.request.RoomRequest;
+import com.bang_ggood.user.domain.User;
+import jakarta.validation.Valid;
+import java.util.List;
+
+public record ChecklistRequest(@Valid RoomRequest room, List options,
+ @Valid List questions) {
+
+ public Room toRoomEntity() {
+ return room.toRoomEntity();
+ }
+
+ public Checklist toChecklistEntity(Room roomEntity, User user) {
+ return new Checklist(roomEntity, user, room.deposit(), room.rent(), room.maintenanceFee(), room.contractTerm(),
+ OccupancyMonth.from(room.occupancyMonth()), OccupancyPeriod.from(room.occupancyPeriod()),
+ room.realEstate(), room.memo(), room.summary());
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/request/ChecklistRequestV1.java b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/request/ChecklistRequestV1.java
new file mode 100644
index 000000000..03edc011b
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/request/ChecklistRequestV1.java
@@ -0,0 +1,16 @@
+package com.bang_ggood.checklist.dto.request;
+
+import com.bang_ggood.question.dto.request.QuestionRequest;
+import com.bang_ggood.room.dto.request.RoomRequest;
+import com.bang_ggood.station.dto.request.ChecklistStationRequest;
+import jakarta.validation.Valid;
+import java.util.List;
+
+public record ChecklistRequestV1(@Valid RoomRequest room, List options,
+ @Valid List questions,
+ ChecklistStationRequest geolocation) {
+
+ public ChecklistRequest toChecklistRequest() {
+ return new ChecklistRequest(room, options, questions);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/ChecklistPreviewResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/ChecklistPreviewResponse.java
new file mode 100644
index 000000000..c7e71d9fb
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/ChecklistPreviewResponse.java
@@ -0,0 +1,26 @@
+package com.bang_ggood.checklist.dto.response;
+
+import com.bang_ggood.checklist.domain.Checklist;
+import java.time.LocalDateTime;
+
+public record ChecklistPreviewResponse(
+ Long checklistId, String roomName, String address, String buildingName,
+ String station, Integer walkingTime,
+ Integer deposit, Integer rent, LocalDateTime createdAt,
+ String summary, boolean isLiked) {
+
+ public static ChecklistPreviewResponse of(Checklist checklist, boolean isLiked) {
+ return new ChecklistPreviewResponse(
+ checklist.getId(),
+ checklist.getRoomName(),
+ checklist.getRoomAddress(),
+ checklist.getRoomBuildingName(),
+ checklist.getRoomStation(),
+ checklist.getRoomWalkingTime(),
+ checklist.getDeposit(),
+ checklist.getRent(),
+ checklist.getCreatedAt(),
+ checklist.getSummary(),
+ isLiked);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/ChecklistPreviewResponseV1.java b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/ChecklistPreviewResponseV1.java
new file mode 100644
index 000000000..1803003d8
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/ChecklistPreviewResponseV1.java
@@ -0,0 +1,27 @@
+package com.bang_ggood.checklist.dto.response;
+
+import com.bang_ggood.checklist.domain.Checklist;
+import com.bang_ggood.station.dto.response.SubwayStationResponse;
+import java.time.LocalDateTime;
+
+public record ChecklistPreviewResponseV1(
+ Long checklistId, String roomName, String address, String buildingName,
+ SubwayStationResponse station, Integer walkingTime,
+ Integer deposit, Integer rent, LocalDateTime createdAt,
+ String summary, boolean isLiked) {
+
+ public static ChecklistPreviewResponseV1 of(Checklist checklist, SubwayStationResponse station, boolean isLiked) {
+ return new ChecklistPreviewResponseV1(
+ checklist.getId(),
+ checklist.getRoomName(),
+ checklist.getRoomAddress(),
+ checklist.getRoomBuildingName(),
+ station,
+ checklist.getRoomWalkingTime(),
+ checklist.getDeposit(),
+ checklist.getRent(),
+ checklist.getCreatedAt(),
+ checklist.getSummary(),
+ isLiked);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/ChecklistsPreviewResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/ChecklistsPreviewResponse.java
new file mode 100644
index 000000000..a60dac618
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/ChecklistsPreviewResponse.java
@@ -0,0 +1,10 @@
+package com.bang_ggood.checklist.dto.response;
+
+import java.util.List;
+
+public record ChecklistsPreviewResponse(List checklists) {
+
+ public static ChecklistsPreviewResponse from(List checklists) {
+ return new ChecklistsPreviewResponse(checklists);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/ChecklistsPreviewResponseV1.java b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/ChecklistsPreviewResponseV1.java
new file mode 100644
index 000000000..d736420c9
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/ChecklistsPreviewResponseV1.java
@@ -0,0 +1,10 @@
+package com.bang_ggood.checklist.dto.response;
+
+import java.util.List;
+
+public record ChecklistsPreviewResponseV1(List checklists) {
+
+ public static ChecklistsPreviewResponseV1 from(List checklists) {
+ return new ChecklistsPreviewResponseV1(checklists);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/SelectedChecklistResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/SelectedChecklistResponse.java
new file mode 100644
index 000000000..bc26a0582
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/SelectedChecklistResponse.java
@@ -0,0 +1,16 @@
+package com.bang_ggood.checklist.dto.response;
+
+import com.bang_ggood.option.dto.response.SelectedOptionResponse;
+import com.bang_ggood.question.dto.response.SelectedCategoryQuestionsResponse;
+import com.bang_ggood.room.dto.response.SelectedRoomResponse;
+import java.util.List;
+
+public record SelectedChecklistResponse(SelectedRoomResponse room,
+ List options,
+ List categories,
+ boolean isLiked) {
+ public static SelectedChecklistResponse of(SelectedRoomResponse room, List options,
+ List categories, boolean isLiked) {
+ return new SelectedChecklistResponse(room, options, categories, isLiked);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/SelectedChecklistResponseV1.java b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/SelectedChecklistResponseV1.java
new file mode 100644
index 000000000..4f2ba0a46
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/dto/response/SelectedChecklistResponseV1.java
@@ -0,0 +1,23 @@
+package com.bang_ggood.checklist.dto.response;
+
+import com.bang_ggood.option.dto.response.SelectedOptionResponse;
+import com.bang_ggood.question.dto.response.SelectedCategoryQuestionsResponse;
+import com.bang_ggood.room.dto.response.SelectedRoomResponse;
+import com.bang_ggood.room.dto.response.SelectedRoomResponseV1;
+import com.bang_ggood.station.dto.response.SubwayStationResponse;
+import com.bang_ggood.station.dto.response.SubwayStationResponses;
+import java.util.List;
+
+public record SelectedChecklistResponseV1(SelectedRoomResponseV1 room,
+ List options,
+ List categories,
+ boolean isLiked,
+ List stations) {
+
+ public static SelectedChecklistResponseV1 of(SelectedRoomResponse room, List options,
+ List categories, boolean isLiked,
+ SubwayStationResponses stations) {
+ return new SelectedChecklistResponseV1(SelectedRoomResponseV1.from(room), options, categories, isLiked,
+ stations.getStations());
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/repository/ChecklistRepository.java b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/repository/ChecklistRepository.java
new file mode 100644
index 000000000..31ea2b04c
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/repository/ChecklistRepository.java
@@ -0,0 +1,62 @@
+package com.bang_ggood.checklist.repository;
+
+import com.bang_ggood.checklist.domain.Checklist;
+import com.bang_ggood.global.exception.BangggoodException;
+import com.bang_ggood.global.exception.ExceptionCode;
+import com.bang_ggood.user.domain.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.List;
+import java.util.Optional;
+
+public interface ChecklistRepository extends JpaRepository {
+
+ @Query("SELECT c FROM Checklist c "
+ + "JOIN FETCH c.room r "
+ + "LEFT JOIN FETCH c.questions q "
+ + "WHERE c.id = :id "
+ + "AND c.deleted = false")
+ Optional findById(@Param("id") Long id);
+
+ default Checklist getById(@Param("id") Long id) {
+ return findById(id).orElseThrow(() -> new BangggoodException(ExceptionCode.CHECKLIST_NOT_FOUND));
+ }
+
+ @Query("SELECT c FROM Checklist c "
+ + "WHERE c.user = :user "
+ + "AND c.deleted = false "
+ + "ORDER BY c.createdAt DESC, c.id DESC ")
+ List findAllByUserOrderByLatest(@Param("user") User user);
+
+ @Query("SELECT c FROM Checklist c "
+ + "JOIN ChecklistLike cl "
+ + "on cl.checklist = c "
+ + "WHERE c.user = :user "
+ + "AND c.deleted = false "
+ + "ORDER BY c.createdAt DESC, c.id DESC")
+ List findAllByUserAndIsLiked(@Param("user") User user);
+
+ @Query("SELECT c FROM Checklist c "
+ + "JOIN FETCH c.user u "
+ + "JOIN FETCH c.room r "
+ + "WHERE u = :user "
+ + "AND c.id IN :checklistIds "
+ + "AND c.deleted = false")
+ List findByUserAndIdIn(@Param("user") User user,
+ @Param("checklistIds") List checklistIds);
+
+ @Query("SELECT COUNT(c) > 0 FROM Checklist c "
+ + "WHERE c.id = :id "
+ + "AND c.deleted = false")
+ boolean existsById(@Param("id") Long id);
+
+ @Transactional
+ @Modifying(flushAutomatically = true, clearAutomatically = true)
+ @Query("UPDATE Checklist c "
+ + "SET c.deleted = true "
+ + "WHERE c.id = :id")
+ void deleteById(@Param("id") Long id);
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/service/ChecklistManageService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/service/ChecklistManageService.java
new file mode 100644
index 000000000..ec5b126a6
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/service/ChecklistManageService.java
@@ -0,0 +1,309 @@
+package com.bang_ggood.checklist.service;
+
+import com.bang_ggood.checklist.domain.Checklist;
+import com.bang_ggood.checklist.dto.request.ChecklistRequest;
+import com.bang_ggood.checklist.dto.request.ChecklistRequestV1;
+import com.bang_ggood.checklist.dto.response.ChecklistPreviewResponse;
+import com.bang_ggood.checklist.dto.response.ChecklistPreviewResponseV1;
+import com.bang_ggood.checklist.dto.response.ChecklistsPreviewResponse;
+import com.bang_ggood.checklist.dto.response.ChecklistsPreviewResponseV1;
+import com.bang_ggood.checklist.dto.response.SelectedChecklistResponse;
+import com.bang_ggood.checklist.dto.response.SelectedChecklistResponseV1;
+import com.bang_ggood.like.service.ChecklistLikeService;
+import com.bang_ggood.maintenance.domain.ChecklistMaintenance;
+import com.bang_ggood.maintenance.domain.MaintenanceItem;
+import com.bang_ggood.maintenance.service.ChecklistMaintenanceService;
+import com.bang_ggood.option.domain.ChecklistOption;
+import com.bang_ggood.option.dto.response.SelectedOptionResponse;
+import com.bang_ggood.option.service.ChecklistOptionService;
+import com.bang_ggood.question.domain.Answer;
+import com.bang_ggood.question.domain.Category;
+import com.bang_ggood.question.domain.ChecklistQuestion;
+import com.bang_ggood.question.dto.response.SelectedCategoryQuestionsResponse;
+import com.bang_ggood.question.dto.response.SelectedQuestionResponse;
+import com.bang_ggood.question.service.ChecklistQuestionService;
+import com.bang_ggood.question.service.QuestionService;
+import com.bang_ggood.room.domain.Room;
+import com.bang_ggood.room.dto.response.SelectedRoomResponse;
+import com.bang_ggood.room.service.RoomService;
+import com.bang_ggood.station.domain.ChecklistStation;
+import com.bang_ggood.station.dto.request.ChecklistStationRequest;
+import com.bang_ggood.station.dto.response.SubwayStationResponse;
+import com.bang_ggood.station.dto.response.SubwayStationResponses;
+import com.bang_ggood.station.service.ChecklistStationService;
+import com.bang_ggood.user.domain.User;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.List;
+
+@RequiredArgsConstructor
+@Service
+public class ChecklistManageService {
+
+ private final RoomService roomService;
+ private final ChecklistService checklistService;
+ private final ChecklistOptionService checklistOptionService;
+ private final ChecklistQuestionService checklistQuestionService;
+ private final ChecklistMaintenanceService checklistMaintenanceService;
+ private final ChecklistLikeService checklistLikeService;
+ private final ChecklistStationService checklistStationService;
+ private final QuestionService questionService;
+
+ @Transactional
+ public Long createChecklist(User user, ChecklistRequest checklistRequest) {
+ Room room = roomService.createRoom(checklistRequest.toRoomEntity());
+ Checklist checklist = checklistService.createChecklist(checklistRequest.toChecklistEntity(room, user));
+ createChecklistOptions(checklistRequest, checklist);
+ createChecklistQuestions(checklistRequest, checklist);
+ createChecklistMaintenances(checklistRequest, checklist);
+ return checklist.getId();
+ }
+
+ @Transactional
+ public Long createChecklistV1(User user, ChecklistRequestV1 checklistRequestV1) {
+ ChecklistRequest checklistRequest = checklistRequestV1.toChecklistRequest();
+
+ Room room = roomService.createRoom(checklistRequest.toRoomEntity());
+ Checklist checklist = checklistService.createChecklist(checklistRequest.toChecklistEntity(room, user));
+ createChecklistOptions(checklistRequest, checklist);
+ createChecklistQuestions(checklistRequest, checklist);
+ createChecklistMaintenances(checklistRequest, checklist);
+ createChecklistStation(checklistRequestV1, checklist);
+ return checklist.getId();
+ }
+
+ private void createChecklistOptions(ChecklistRequest checklistRequest, Checklist checklist) {
+ List checklistOptions = checklistRequest.options().stream()
+ .map(option -> new ChecklistOption(checklist, option))
+ .toList();
+ checklistOptionService.createOptions(checklistOptions);
+ }
+
+ private void createChecklistQuestions(ChecklistRequest checklistRequest, Checklist checklist) {
+ List checklistQuestions = checklistRequest.questions().stream()
+ .map(question -> new ChecklistQuestion(
+ checklist,
+ questionService.readQuestion(question.questionId()),
+ Answer.from(question.answer())))
+ .toList();
+ checklistQuestionService.createQuestions(checklistQuestions);
+ }
+
+ private void createChecklistMaintenances(ChecklistRequest checklistRequest, Checklist checklist) {
+ List checklistMaintenances =
+ checklistRequest.room().includedMaintenances().stream()
+ .map(maintenanceId -> new ChecklistMaintenance(checklist,
+ MaintenanceItem.fromId(maintenanceId)))
+ .toList();
+ checklistMaintenanceService.createMaintenances(checklistMaintenances);
+ }
+
+ private void createChecklistStation(ChecklistRequestV1 checklistRequestV1, Checklist checklist) {
+ ChecklistStationRequest geolocation = checklistRequestV1.geolocation();
+ checklistStationService.createChecklistStations(checklist, geolocation.latitude(), geolocation.longitude());
+ }
+
+ @Transactional(readOnly = true)
+ public SelectedChecklistResponse readChecklist(User user, Long checklistId) {
+ Checklist checklist = checklistService.readChecklist(user, checklistId);
+
+ List maintenances = readChecklistMaintenances(checklist);
+ List options = readChecklistOptions(checklist);
+ List questions = readChecklistQuestions(checklist);
+ SelectedRoomResponse room = SelectedRoomResponse.of(checklist, maintenances);
+ boolean isLiked = checklistLikeService.isLikedChecklist(checklist);
+
+ return SelectedChecklistResponse.of(room, options, questions, isLiked);
+ }
+
+ @Transactional(readOnly = true)
+ public SelectedChecklistResponseV1 readChecklistV1(User user, Long checklistId) {
+ Checklist checklist = checklistService.readChecklist(user, checklistId);
+
+ List maintenances = readChecklistMaintenances(checklist);
+ List options = readChecklistOptions(checklist);
+ List questions = readChecklistQuestions(checklist);
+ SelectedRoomResponse room = SelectedRoomResponse.of(checklist, maintenances);
+ boolean isLiked = checklistLikeService.isLikedChecklist(checklist);
+ SubwayStationResponses stations = readChecklistStations(checklist);
+
+ return SelectedChecklistResponseV1.of(room, options, questions, isLiked, stations);
+ }
+
+ private List readChecklistMaintenances(Checklist checklist) {
+ return checklistMaintenanceService.readChecklistMaintenances(checklist)
+ .stream()
+ .map(ChecklistMaintenance::getMaintenanceItemId)
+ .toList();
+ }
+
+ private List readChecklistOptions(Checklist checklist) {
+ return checklistOptionService.readChecklistOptions(checklist)
+ .stream()
+ .map(SelectedOptionResponse::from)
+ .toList();
+ }
+
+ private List readChecklistQuestions(Checklist checklist) {
+ List checklistQuestions = checklistQuestionService.readChecklistQuestions(checklist);
+
+ return questionService.findAllCategories().stream()
+ .map(category -> categorizeChecklistQuestions(category, checklistQuestions))
+ .filter(selectedCategoryQuestionsResponse -> !selectedCategoryQuestionsResponse.questions().isEmpty())
+ .toList();
+ }
+
+ private SelectedCategoryQuestionsResponse categorizeChecklistQuestions(Category category,
+ List checklistQuestions) {
+ List selectedQuestionResponse = checklistQuestionService.categorizeChecklistQuestions(category, checklistQuestions)
+ .stream()
+ .map(checklistQuestion -> new SelectedQuestionResponse(checklistQuestion, questionService.readHighlights(checklistQuestion.getQuestionId())))
+ .toList();
+
+ return SelectedCategoryQuestionsResponse.of(category, selectedQuestionResponse);
+ }
+
+ private SubwayStationResponses readChecklistStations(Checklist checklist) {
+ List checklistStations = checklistStationService.readChecklistStationsByChecklist(checklist);
+ List stations = checklistStations.stream()
+ .map(SubwayStationResponse::from)
+ .toList();
+
+ return SubwayStationResponses.from(stations);
+ }
+
+ @Transactional(readOnly = true)
+ public ChecklistsPreviewResponse readLikedChecklistsPreview(User user) {
+ List likedChecklists = checklistService.readLikedChecklistsPreview(user);
+ List responses = mapToChecklistPreviewResponses(
+ likedChecklists);
+ return ChecklistsPreviewResponse.from(responses);
+ }
+
+ private List mapToChecklistPreviewResponses(List likedChecklists) {
+ return likedChecklists.stream()
+ .map(checklist -> ChecklistPreviewResponse.of(checklist, true))
+ .toList();
+ }
+
+ @Transactional(readOnly = true)
+ public ChecklistsPreviewResponseV1 readLikedChecklistsPreviewV1(User user) {
+ List likedChecklists = checklistService.readLikedChecklistsPreview(user);
+ List responses = likedChecklists.stream()
+ .map(this::mapToChecklistPreviewV1)
+ .toList();
+ return ChecklistsPreviewResponseV1.from(responses);
+ }
+
+ @Transactional
+ public void deleteChecklistById(User user, Long id) {
+ Checklist checklist = checklistService.readChecklist(user, id);
+ checklistQuestionService.deleteAllByChecklistId(checklist.getId());
+ checklistOptionService.deleteAllByChecklistId(checklist.getId());
+ checklistMaintenanceService.deleteAllByChecklistId(checklist.getId());
+ checklistService.deleteById(id);
+ roomService.deleteById(checklist.getRoomId());
+ }
+
+ @Transactional(readOnly = true)
+ public ChecklistsPreviewResponse readAllChecklistsPreview(User user) {
+ List checklists = checklistService.readAllChecklistsOrderByLatest(user);
+
+ List responses = checklists.stream()
+ .map(this::mapToChecklistPreview)
+ .toList();
+ return ChecklistsPreviewResponse.from(responses);
+ }
+
+ private ChecklistPreviewResponse mapToChecklistPreview(Checklist checklist) {
+ boolean isLiked = checklistLikeService.isLikedChecklist(checklist);
+ return ChecklistPreviewResponse.of(checklist, isLiked);
+ }
+
+ @Transactional(readOnly = true)
+ public ChecklistsPreviewResponseV1 readAllChecklistsPreviewV1(User user) {
+ List checklists = checklistService.readAllChecklistsOrderByLatest(user);
+ List responses = checklists.stream()
+ .map(this::mapToChecklistPreviewV1)
+ .toList();
+
+ return ChecklistsPreviewResponseV1.from(responses);
+ }
+
+ private ChecklistPreviewResponseV1 mapToChecklistPreviewV1(Checklist checklist) {
+ boolean isLiked = checklistLikeService.isLikedChecklist(checklist);
+ SubwayStationResponse stationResponse = readNearestStation(checklist);
+ return ChecklistPreviewResponseV1.of(checklist, stationResponse, isLiked);
+ }
+
+ private SubwayStationResponse readNearestStation(Checklist checklist) {
+ List checklistStations = checklistStationService.readChecklistStationsByChecklist(checklist);
+ List stationResponses = checklistStations.stream()
+ .map(SubwayStationResponse::from)
+ .toList();
+ SubwayStationResponses subwayStationResponses = SubwayStationResponses.from(stationResponses);
+
+ return subwayStationResponses.getNearestStation();
+ }
+
+ @Transactional
+ public void updateChecklistById(User user, Long checklistId, ChecklistRequest checklistRequest) {
+ Checklist checklist = checklistService.readChecklist(user, checklistId);
+
+ roomService.updateRoom(checklist.getRoom(), checklistRequest.toRoomEntity());
+ checklistService.updateChecklist(checklist, checklistRequest.toChecklistEntity(checklist.getRoom(), user));
+
+ updateChecklistOptions(checklistRequest, checklist);
+ updateChecklistQuestions(checklistRequest, checklist);
+ updateChecklistMaintenances(checklistRequest, checklist);
+ }
+
+ @Transactional
+ public void updateChecklistByIdV1(User user, Long checklistId, ChecklistRequestV1 checklistRequestV1) {
+ Checklist checklist = checklistService.readChecklist(user, checklistId);
+
+ ChecklistRequest checklistRequest = checklistRequestV1.toChecklistRequest();
+ roomService.updateRoom(checklist.getRoom(), checklistRequest.toRoomEntity());
+ checklistService.updateChecklist(checklist, checklistRequest.toChecklistEntity(checklist.getRoom(), user));
+
+ updateChecklistOptions(checklistRequest, checklist);
+ updateChecklistQuestions(checklistRequest, checklist);
+ updateChecklistMaintenances(checklistRequest, checklist);
+ updateChecklistStations(checklistRequestV1, checklist);
+ }
+
+ private void updateChecklistOptions(ChecklistRequest checklistRequest, Checklist checklist) {
+ List checklistOptions = checklistRequest.options().stream()
+ .map(option -> new ChecklistOption(checklist, option))
+ .toList();
+ checklistOptionService.updateOptions(checklist.getId(), checklistOptions);
+ }
+
+ private void updateChecklistQuestions(ChecklistRequest checklistRequest, Checklist checklist) {
+ List questions = checklist.getQuestions();
+ List updateQuestions = checklistRequest.questions().stream()
+ .map(question -> new ChecklistQuestion(
+ checklist,
+ questionService.readQuestion(question.questionId()),
+ Answer.from(question.answer())))
+ .toList();
+ checklistQuestionService.updateQuestions(questions, updateQuestions);
+ }
+
+ private void updateChecklistMaintenances(ChecklistRequest checklistRequest, Checklist checklist) {
+
+ List checklistMaintenances =
+ checklistRequest.room().includedMaintenances().stream()
+ .map(maintenanceId -> new ChecklistMaintenance(checklist,
+ MaintenanceItem.fromId(maintenanceId)))
+ .toList();
+ checklistMaintenanceService.updateMaintenances(checklist.getId(), checklistMaintenances);
+ }
+
+ private void updateChecklistStations(ChecklistRequestV1 checklistRequestV1, Checklist checklist) {
+ double latitude = checklistRequestV1.geolocation().latitude();
+ double longitude = checklistRequestV1.geolocation().longitude();
+ checklistStationService.updateChecklistStation(checklist, latitude, longitude);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/service/ChecklistService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/service/ChecklistService.java
new file mode 100644
index 000000000..7bb755b36
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/checklist/service/ChecklistService.java
@@ -0,0 +1,57 @@
+package com.bang_ggood.checklist.service;
+
+import com.bang_ggood.checklist.domain.Checklist;
+import com.bang_ggood.checklist.repository.ChecklistRepository;
+import com.bang_ggood.global.exception.BangggoodException;
+import com.bang_ggood.global.exception.ExceptionCode;
+import com.bang_ggood.user.domain.User;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.List;
+
+@RequiredArgsConstructor
+@Service
+public class ChecklistService {
+
+ private final ChecklistRepository checklistRepository;
+
+ @Transactional
+ public Checklist createChecklist(Checklist checklist) {
+ return checklistRepository.save(checklist);
+ }
+
+ @Transactional(readOnly = true)
+ public Checklist readChecklist(User user, Long checklistId) {
+ Checklist checklist = checklistRepository.getById(checklistId);
+ validateChecklistOwnership(user, checklist);
+
+ return checklist;
+ }
+
+ private void validateChecklistOwnership(User user, Checklist checklist) {
+ if (!checklist.isOwnedBy(user)) {
+ throw new BangggoodException(ExceptionCode.CHECKLIST_NOT_OWNED_BY_USER);
+ }
+ }
+
+ @Transactional(readOnly = true)
+ public List readAllChecklistsOrderByLatest(User user) {
+ return checklistRepository.findAllByUserOrderByLatest(user);
+ }
+
+ @Transactional(readOnly = true)
+ public List readLikedChecklistsPreview(User user) {
+ return checklistRepository.findAllByUserAndIsLiked(user);
+ }
+
+ @Transactional
+ public void updateChecklist(Checklist checklist, Checklist updateChecklist) {
+ checklist.change(updateChecklist);
+ }
+
+ @Transactional
+ public void deleteById(Long id) {
+ checklistRepository.deleteById(id);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/contract/domain/OccupancyMonth.java b/backend/bang-ggood/src/main/java/com/bang_ggood/contract/domain/OccupancyMonth.java
new file mode 100644
index 000000000..6d19eaad3
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/contract/domain/OccupancyMonth.java
@@ -0,0 +1,43 @@
+package com.bang_ggood.contract.domain;
+
+import com.bang_ggood.global.exception.BangggoodException;
+import com.bang_ggood.global.exception.ExceptionCode;
+import java.util.Arrays;
+
+public enum OccupancyMonth {
+
+ JANUARY(1),
+ FEBRUARY(2),
+ MARCH(3),
+ APRIL(4),
+ MAY(5),
+ JUNE(6),
+ JULY(7),
+ AUGUST(8),
+ SEPTEMBER(9),
+ OCTOBER(10),
+ NOVEMBER(11),
+ DECEMBER(12),
+ NONE(null);
+
+ private final Integer month;
+
+ OccupancyMonth(Integer month) {
+ this.month = month;
+ }
+
+ public static OccupancyMonth from(Integer month) {
+ if (month == null) {
+ return NONE;
+ }
+ return Arrays.stream(OccupancyMonth.values())
+ .filter(value -> value.month != null && value.month.equals(month))
+ .findFirst()
+ .orElseThrow(() -> new BangggoodException(ExceptionCode.OCCUPANCY_MONTH_INVALID));
+ }
+
+ public Integer getMonth() {
+ return month;
+ }
+}
+
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/contract/domain/OccupancyPeriod.java b/backend/bang-ggood/src/main/java/com/bang_ggood/contract/domain/OccupancyPeriod.java
new file mode 100644
index 000000000..d46112d6b
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/contract/domain/OccupancyPeriod.java
@@ -0,0 +1,33 @@
+package com.bang_ggood.contract.domain;
+
+import com.bang_ggood.global.exception.BangggoodException;
+import com.bang_ggood.global.exception.ExceptionCode;
+import java.util.Arrays;
+
+public enum OccupancyPeriod {
+
+ EARLY("초"),
+ MIDDLE("중순"),
+ LATE("말"),
+ NONE(null);
+
+ private final String period;
+
+ OccupancyPeriod(String period) {
+ this.period = period;
+ }
+
+ public static OccupancyPeriod from(String period) {
+ if (period == null) {
+ return NONE;
+ }
+ return Arrays.stream(OccupancyPeriod.values())
+ .filter(value -> value.period != null && value.period.equals(period))
+ .findFirst()
+ .orElseThrow(() -> new BangggoodException(ExceptionCode.OCCUPANCY_PERIOD_INVALID));
+ }
+
+ public String getPeriod() {
+ return period;
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/DBInitializer.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/DBInitializer.java
new file mode 100644
index 000000000..bfa06e0d1
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/DBInitializer.java
@@ -0,0 +1,36 @@
+package com.bang_ggood.global;
+
+import com.bang_ggood.user.domain.LoginType;
+import com.bang_ggood.user.domain.User;
+import com.bang_ggood.user.domain.UserType;
+import com.bang_ggood.user.service.UserService;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+import java.util.List;
+
+@Component
+public class DBInitializer implements CommandLineRunner {
+
+ private final UserService userService;
+ private final DefaultChecklistService defaultChecklistService;
+
+ public DBInitializer(UserService userService, DefaultChecklistService defaultChecklistService) {
+ this.userService = userService;
+ this.defaultChecklistService = defaultChecklistService;
+ }
+
+ @Override
+ public void run(String... args) {
+ createGuestUser();
+ }
+
+ public void createGuestUser() {
+ List foundGuestUser = userService.readUser(UserType.GUEST);
+
+ if (foundGuestUser.isEmpty()) {
+ User guestUser = new User("방끗", "bang-ggood1@gmail.com", UserType.GUEST, LoginType.LOCAL);
+ userService.createUser(guestUser);
+ defaultChecklistService.createDefaultChecklistAndQuestions(guestUser);
+ }
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/DefaultChecklistService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/DefaultChecklistService.java
new file mode 100644
index 000000000..bd4733a07
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/DefaultChecklistService.java
@@ -0,0 +1,66 @@
+package com.bang_ggood.global;
+
+import com.bang_ggood.checklist.dto.request.ChecklistRequest;
+import com.bang_ggood.checklist.service.ChecklistManageService;
+import com.bang_ggood.option.domain.Option;
+import com.bang_ggood.question.domain.Answer;
+import com.bang_ggood.question.dto.request.QuestionRequest;
+import com.bang_ggood.question.service.QuestionManageService;
+import com.bang_ggood.room.dto.request.RoomRequest;
+import com.bang_ggood.user.domain.User;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.List;
+
+@Service
+public class DefaultChecklistService {
+
+ private static final RoomRequest DEFAULT_ROOM_REQUEST = createDefaultRoomRequest();
+ private static final List DEFAULT_OPTIONS = createDefaultOptions();
+ private static final List DEFAULT_QUESTION_REQUEST = createDefaultQuestionRequest();
+
+ private final QuestionManageService questionManageService;
+ private final ChecklistManageService checklistManageService;
+
+ public DefaultChecklistService(QuestionManageService questionManageService,
+ ChecklistManageService checklistManageService) {
+ this.questionManageService = questionManageService;
+ this.checklistManageService = checklistManageService;
+ }
+
+ private static RoomRequest createDefaultRoomRequest() {
+ return new RoomRequest(
+ "예시용 체크리스트", "방끗시 집잘구하구 행복하동", "방방하우스", "잠실", 10,
+ 3000, 60, 5, List.of(1, 3), "지상", 14, "분리형 원룸", 9.5,
+ 12, 9, "초", "방끗공인중개사",
+ "이곳에 필요한 메모를 작성하세요.", "이곳에 한줄평을 남겨 보세요.");
+ }
+
+ private static List createDefaultOptions() {
+ return List.of(
+ Option.INDUCTION.getId(),
+ Option.AIR_CONDITIONER.getId(),
+ Option.SINK.getId(),
+ Option.BED.getId());
+ }
+
+ private static List createDefaultQuestionRequest() { // TODO 수정 필요
+ return List.of(
+ new QuestionRequest(1, Answer.GOOD.name()),
+ new QuestionRequest(2, Answer.BAD.name()),
+ new QuestionRequest(3, Answer.GOOD.name()),
+ new QuestionRequest(10, Answer.GOOD.name()),
+ new QuestionRequest(11, Answer.BAD.name()),
+ new QuestionRequest(16, Answer.GOOD.name()),
+ new QuestionRequest(17, Answer.GOOD.name()));
+ }
+
+ @Transactional
+ public void createDefaultChecklistAndQuestions(User user) {
+ ChecklistRequest checklistRequest = new ChecklistRequest(
+ DEFAULT_ROOM_REQUEST, DEFAULT_OPTIONS, DEFAULT_QUESTION_REQUEST);
+
+ questionManageService.createDefaultCustomChecklistQuestions(user);
+ checklistManageService.createChecklist(user, checklistRequest);
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/CorsConfig.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/CorsConfig.java
new file mode 100644
index 000000000..8cdcdc547
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/CorsConfig.java
@@ -0,0 +1,40 @@
+package com.bang_ggood.global.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+import java.util.List;
+
+@Configuration
+@ConfigurationProperties(prefix = "cors")
+public class CorsConfig {
+
+ private List allowOrigins;
+
+ @Bean
+ public CorsFilter corsFilter() {
+ CorsConfiguration configuration = new CorsConfiguration();
+ configuration.setAllowedOrigins(allowOrigins);
+ configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
+ configuration.addAllowedHeader("*");
+ configuration.setAllowCredentials(true);
+ configuration.addExposedHeader(HttpHeaders.LOCATION);
+
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", configuration);
+
+ return new CorsFilter(source);
+ }
+
+ public List getAllowOrigins() {
+ return allowOrigins;
+ }
+
+ public void setAllowOrigins(List allowOrigins) {
+ this.allowOrigins = allowOrigins;
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/OauthClientConfig.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/OauthClientConfig.java
new file mode 100644
index 000000000..25d34d2c9
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/OauthClientConfig.java
@@ -0,0 +1,17 @@
+package com.bang_ggood.global.config;
+
+import com.bang_ggood.global.handler.OauthClientExceptionHandler;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestClient;
+
+@Configuration
+public class OauthClientConfig {
+
+ @Bean
+ RestClient restClient(OauthClientExceptionHandler handler) {
+ return RestClient.builder()
+ .defaultStatusHandler(handler)
+ .build();
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/SwaggerConfig.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/SwaggerConfig.java
new file mode 100644
index 000000000..7da543d10
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/SwaggerConfig.java
@@ -0,0 +1,23 @@
+package com.bang_ggood.global.config;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class SwaggerConfig {
+
+ @Bean
+ public OpenAPI openAPI() {
+ return new OpenAPI()
+ .components(new Components())
+ .info(apiInfo());
+ }
+
+ private Info apiInfo() {
+ return new Info()
+ .title("방끗 API 문서");
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/WebMvcConfig.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/WebMvcConfig.java
new file mode 100644
index 000000000..2caa09c3e
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/WebMvcConfig.java
@@ -0,0 +1,28 @@
+package com.bang_ggood.global.config;
+
+import com.bang_ggood.auth.config.AuthRequiredPrincipalArgumentResolver;
+import com.bang_ggood.auth.config.UserPrincipalArgumentResolver;
+import com.bang_ggood.auth.controller.cookie.CookieResolver;
+import com.bang_ggood.auth.service.AuthService;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import java.util.List;
+
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+ private final CookieResolver cookieResolver;
+ private final AuthService authService;
+
+ public WebMvcConfig(CookieResolver cookieResolver, AuthService authService) {
+ this.cookieResolver = cookieResolver;
+ this.authService = authService;
+ }
+
+ @Override
+ public void addArgumentResolvers(List resolvers) {
+ resolvers.add(new AuthRequiredPrincipalArgumentResolver(cookieResolver, authService));
+ resolvers.add(new UserPrincipalArgumentResolver(cookieResolver, authService));
+ }
+}
diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/datasource/DataSourceConfig.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/datasource/DataSourceConfig.java
new file mode 100644
index 000000000..9fa138638
--- /dev/null
+++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/datasource/DataSourceConfig.java
@@ -0,0 +1,55 @@
+package com.bang_ggood.global.config.datasource;
+
+import com.zaxxer.hikari.HikariDataSource;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.jdbc.DataSourceBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.DependsOn;
+import org.springframework.context.annotation.Primary;
+import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
+import javax.sql.DataSource;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.bang_ggood.global.config.datasource.DataSourceRouter.readKey;
+import static com.bang_ggood.global.config.datasource.DataSourceRouter.writeKey;
+
+@Configuration
+public class DataSourceConfig {
+
+ @Bean
+ @ConfigurationProperties(prefix = "spring.datasource.write")
+ public DataSource writeDataSource() {
+ return DataSourceBuilder.create().type(HikariDataSource.class).build();
+ }
+
+ @Bean
+ @ConfigurationProperties(prefix = "spring.datasource.read")
+ public DataSource readDataSource() {
+ return DataSourceBuilder.create().type(HikariDataSource.class).build();
+ }
+
+ @Bean
+ @DependsOn({"writeDataSource", "readDataSource"})
+ public DataSource routeDataSource() {
+ DataSourceRouter dataSourceRouter = new DataSourceRouter();
+ DataSource writeDataSource = writeDataSource();
+ DataSource readDataSource = readDataSource();
+
+ Map