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 dataSourceMap = new HashMap<>(); + dataSourceMap.put(writeKey, writeDataSource); + dataSourceMap.put(readKey, readDataSource); + dataSourceRouter.setTargetDataSources(dataSourceMap); + dataSourceRouter.setDefaultTargetDataSource(writeDataSource); + + return dataSourceRouter; + } + + @Bean + @Primary + @DependsOn({"routeDataSource"}) + public DataSource dataSource() { + return new LazyConnectionDataSourceProxy(routeDataSource()); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/datasource/DataSourceRouter.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/datasource/DataSourceRouter.java new file mode 100644 index 000000000..35764c16f --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/config/datasource/DataSourceRouter.java @@ -0,0 +1,18 @@ +package com.bang_ggood.global.config.datasource; + +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +public class DataSourceRouter extends AbstractRoutingDataSource { + + static String readKey = "read"; + static String writeKey = "write"; + + @Override + protected Object determineCurrentLookupKey() { + if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { + return readKey; + } + return writeKey; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/controller/HealthCheckController.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/controller/HealthCheckController.java new file mode 100644 index 000000000..1f82e3ba4 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/controller/HealthCheckController.java @@ -0,0 +1,12 @@ +package com.bang_ggood.global.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HealthCheckController { + + @GetMapping("/health-check") + public void checkHealth() { + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/BangggoodException.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/BangggoodException.java new file mode 100644 index 000000000..d6bc74dbc --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/BangggoodException.java @@ -0,0 +1,25 @@ +package com.bang_ggood.global.exception; + +import org.springframework.http.HttpStatusCode; + +public class BangggoodException extends RuntimeException { + + private final ExceptionCode exceptionCode; + + public BangggoodException(ExceptionCode exceptionCode) { + this.exceptionCode = exceptionCode; + } + + @Override + public String getMessage() { + return exceptionCode.getMessage(); + } + + public HttpStatusCode getHttpStatusCode() { + return exceptionCode.getHttpStatus(); + } + + public String getClientExceptionCodeName() { + return exceptionCode.getClientExceptionCode().name(); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/ClientExceptionCode.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/ClientExceptionCode.java new file mode 100644 index 000000000..f3ba35200 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/ClientExceptionCode.java @@ -0,0 +1,32 @@ +package com.bang_ggood.global.exception; + +public enum ClientExceptionCode { + + ARTICLE_NOT_FOUND, + AUTH_ACCESS_TOKEN_EMPTY, + AUTH_TOKEN_EMPTY, + AUTH_TOKEN_INVALID, + CHECKLIST_ERROR, + CHECKLIST_NOT_FOUND, + CHECKLIST_SERVER_ERROR, + CUSTOM_ERROR, + INTERNAL_SERVER_ERROR, + OAUTH_SERVER_ERROR, + PASSWORD_HASHING_ERROR, + QUESTION_ERROR, + STATION_SERVER_ERROR, + UNAUTH_ERROR, + USER_EMAIL_ALREADY_USED, + USER_INVALID_FORMAT, + USER_NOT_FOUND, + LOGIN_ERROR, + INVALID_PARAMETER, + + // TODO: 임의 사용 지워질 코드 + AUTH_TOKEN_NOT_OWNED_BY_USER, + AUTH_TOKEN_USER_MISMATCH, + GUEST_USER_NOT_FOUND, + GUEST_USER_UNEXPECTED_EXIST, + LIKE_ALREADY_EXISTS, + LIKE_NOT_EXISTS, +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/ExceptionCode.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/ExceptionCode.java new file mode 100644 index 000000000..0af933495 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/ExceptionCode.java @@ -0,0 +1,98 @@ +package com.bang_ggood.global.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum ExceptionCode { + + // 전체 + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, ClientExceptionCode.INTERNAL_SERVER_ERROR, "예상치 못한 서버에러가 발생했습니다"), + INVALID_PARAMETER(HttpStatus.BAD_REQUEST, ClientExceptionCode.INVALID_PARAMETER, "잘못된 인자입니다."), + + // Option + OPTION_INVALID(HttpStatus.BAD_REQUEST, ClientExceptionCode.CHECKLIST_ERROR, "잘못된 옵션 ID입니다."), + OPTION_DUPLICATED(HttpStatus.BAD_REQUEST, ClientExceptionCode.CHECKLIST_ERROR, "중복된 옵션이 존재합니다."), + + // Question + QUESTION_ID_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, ClientExceptionCode.QUESTION_ERROR, "중복된 질문 ID가 존재해 질문을 생성할 수 없습니다."), + QUESTION_HIGHLIGHT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, ClientExceptionCode.CHECKLIST_SERVER_ERROR, "잘못된 하이라이트 키워드가 존재해 질문을 생성할 수 없습니다."), + QUESTION_INVALID(HttpStatus.BAD_REQUEST, ClientExceptionCode.QUESTION_ERROR, "잘못된 질문 ID입니다."), + QUESTION_DUPLICATED(HttpStatus.BAD_REQUEST, ClientExceptionCode.QUESTION_ERROR, "중복된 질문이 존재합니다."), + QUESTION_DIFFERENT(HttpStatus.BAD_REQUEST, ClientExceptionCode.QUESTION_ERROR, "수정할 질문 목록이 기존의 질문 목록과 동일하지 않습니다."), + + // User + USER_NOT_FOUND(HttpStatus.UNAUTHORIZED, ClientExceptionCode.USER_NOT_FOUND, "유저가 존재하지 않습니다."), + USER_INVALID_PASSWORD(HttpStatus.BAD_REQUEST, ClientExceptionCode.LOGIN_ERROR, "비밀번호가 일치하지 않습니다."), + USER_EMAIL_ALREADY_USED(HttpStatus.CONFLICT, ClientExceptionCode.USER_EMAIL_ALREADY_USED, "이미 해당 이메일을 사용하는 유저가 존재합니다."), + GUEST_USER_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, ClientExceptionCode.GUEST_USER_NOT_FOUND, "게스트 유저가 존재하지 않습니다."), + GUEST_USER_UNEXPECTED_EXIST(HttpStatus.INTERNAL_SERVER_ERROR, ClientExceptionCode.GUEST_USER_UNEXPECTED_EXIST, "예상치 못한 게스트 유저가 존재합니다. 데이터베이스를 확인해주세요."), + + //Email + EMAIL_INVALID_FORMAT(HttpStatus.BAD_REQUEST, ClientExceptionCode.USER_INVALID_FORMAT, "유효하지 않은 이메일 형식입니다."), + + //Password + PASSWORD_INVALID_FORMAT(HttpStatus.BAD_REQUEST, ClientExceptionCode.USER_INVALID_FORMAT, "유효하지 않은 비밀번호 형식입니다."), + PASSWORD_HASHING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, ClientExceptionCode.PASSWORD_HASHING_ERROR, "비밀번호 해싱 중 오류가 발생했습니다."), + + // Answer + ANSWER_INVALID(HttpStatus.BAD_REQUEST, ClientExceptionCode.CHECKLIST_ERROR, "답변이 유효하지 않습니다."), + + // Checklist + CHECKLIST_NOT_FOUND(HttpStatus.BAD_REQUEST, ClientExceptionCode.CHECKLIST_NOT_FOUND, "체크리스트가 존재하지 않습니다."), + CHECKLIST_MEMO_INVALID_LENGTH(HttpStatus.BAD_REQUEST, ClientExceptionCode.CHECKLIST_ERROR, "체크리스트 메모는 1000자 이하여야 합니다."), + CHECKLIST_NOT_OWNED_BY_USER(HttpStatus.UNAUTHORIZED, ClientExceptionCode.UNAUTH_ERROR, "유저의 체크리스트가 아닙니다."), + + // CustomChecklist + CUSTOM_CHECKLIST_QUESTION_EMPTY(HttpStatus.BAD_REQUEST, ClientExceptionCode.CUSTOM_ERROR, "커스텀 질문 개수가 유효하지 않습니다."), + + // FloorLevel + FLOOR_LEVEL_INVALID(HttpStatus.BAD_REQUEST, ClientExceptionCode.CHECKLIST_ERROR, "층 종류가 유효하지 않습니다."), + + // Structure + STRUCTURE_INVALID(HttpStatus.BAD_REQUEST, ClientExceptionCode.CHECKLIST_ERROR, "방 구조가 유효하지 않습니다."), + + // Room + ROOM_FLOOR_AND_LEVEL_INVALID(HttpStatus.BAD_REQUEST, ClientExceptionCode.CHECKLIST_ERROR, "방이 지상층일 경우에만 층수를 입력할 수 있습니다."), + + + // OccupancyMonth + OCCUPANCY_MONTH_INVALID(HttpStatus.BAD_REQUEST, ClientExceptionCode.CHECKLIST_ERROR, "입주 가능월은 1부터 12 사이 혹은 null 값만 가능합니다."), + + // OccupancyPeriod + OCCUPANCY_PERIOD_INVALID(HttpStatus.BAD_REQUEST, ClientExceptionCode.CHECKLIST_ERROR, "입주 가능 기간은 초, 중, 말 혹은 null 값만 가능합니다."), + + // MaintenanceItem + MAINTENANCE_ITEM_DUPLICATE(HttpStatus.BAD_REQUEST, ClientExceptionCode.CHECKLIST_ERROR, "중복된 관리비 항목이 존재합니다."), + MAINTENANCE_ITEM_INVALID(HttpStatus.BAD_REQUEST, ClientExceptionCode.CHECKLIST_ERROR, "유효하지 않은 관리비 항목이 입력되었습니다."), + + // Auth + AUTHENTICATION_ACCESS_TOKEN_EMPTY(HttpStatus.UNAUTHORIZED, ClientExceptionCode.AUTH_ACCESS_TOKEN_EMPTY, "액세스 토큰이 존재하지 않습니다. 액세스 토큰을 발급해주세요."), + AUTHENTICATION_REFRESH_TOKEN_EMPTY(HttpStatus.UNAUTHORIZED, ClientExceptionCode.AUTH_TOKEN_EMPTY, "리프레시 토큰이 존재하지 않습니다. 다시 로그인해주세요."), + AUTHENTICATION_TOKEN_EMPTY(HttpStatus.UNAUTHORIZED, ClientExceptionCode.AUTH_TOKEN_EMPTY, "로그인이 필요한 사용자입니다."), + AUTHENTICATION_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, ClientExceptionCode.AUTH_TOKEN_INVALID, "토큰이 만료되었습니다."), + AUTHENTICATION_TOKEN_INVALID(HttpStatus.UNAUTHORIZED, ClientExceptionCode.AUTH_TOKEN_INVALID, "토큰 정보가 올바르지 않습니다."), + AUTHENTICATION_TOKEN_NOT_OWNED_BY_USER(HttpStatus.UNAUTHORIZED, ClientExceptionCode.AUTH_TOKEN_NOT_OWNED_BY_USER, "해당 유저의 토큰이 아닙니다."), + AUTHENTICATION_TOKEN_USER_MISMATCH(HttpStatus.UNAUTHORIZED, ClientExceptionCode.AUTH_TOKEN_USER_MISMATCH, "엑세스 토큰과 리프레시 토큰의 소유자가 다릅니다."), + AUTHENTICATION_TOKEN_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, ClientExceptionCode.AUTH_TOKEN_INVALID, "토큰 타입이 올바르지 않습니다."), + OAUTH_TOKEN_INTERNAL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, ClientExceptionCode.OAUTH_SERVER_ERROR, "카카오 서버와 통신하는 과정 중 예상치 못한 예외가 발생했습니다."), + OAUTH_REDIRECT_URI_MISMATCH(HttpStatus.BAD_REQUEST, ClientExceptionCode.OAUTH_SERVER_ERROR, "일치하는 Redirect URI가 존재하지 않습니다."), + + + // Article + ARTICLE_NOT_FOUND(HttpStatus.BAD_REQUEST, ClientExceptionCode.ARTICLE_NOT_FOUND, "해당 아티클이 존재하지 않습니다."), + + // Station + STATION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, ClientExceptionCode.STATION_SERVER_ERROR, "지하철 역을 찾을 수 없습니다."), + STATION_NAME_NOT_SAME(HttpStatus.INTERNAL_SERVER_ERROR, ClientExceptionCode.STATION_SERVER_ERROR, "지하철 역을 찾을 수 없습니다."); + + private final HttpStatus httpStatus; + private final ClientExceptionCode clientExceptionCode; + private final String message; + + ExceptionCode(HttpStatus httpStatus, ClientExceptionCode clientExceptionCode, String message) { + this.httpStatus = httpStatus; + this.clientExceptionCode = clientExceptionCode; + this.message = message; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/OauthException.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/OauthException.java new file mode 100644 index 000000000..2bb3ac0a8 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/OauthException.java @@ -0,0 +1,16 @@ +package com.bang_ggood.global.exception; + +import com.bang_ggood.global.exception.dto.OauthExceptionResponse; + +public class OauthException extends RuntimeException { + + private OauthExceptionResponse response; + + public OauthException(OauthExceptionResponse response) { + this.response = response; + } + + public OauthExceptionResponse getResponse() { + return response; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/dto/ExceptionResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/dto/ExceptionResponse.java new file mode 100644 index 000000000..94d221cf0 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/dto/ExceptionResponse.java @@ -0,0 +1,4 @@ +package com.bang_ggood.global.exception.dto; + +public record ExceptionResponse(String httpMethod, String path, String bangggoodCode, String message) { +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/dto/OauthExceptionResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/dto/OauthExceptionResponse.java new file mode 100644 index 000000000..0e5b14b44 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/exception/dto/OauthExceptionResponse.java @@ -0,0 +1,4 @@ +package com.bang_ggood.global.exception.dto; + +public record OauthExceptionResponse(String error, String error_description, String error_code) { +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/handler/GlobalExceptionHandler.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/handler/GlobalExceptionHandler.java new file mode 100644 index 000000000..2eb472690 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/handler/GlobalExceptionHandler.java @@ -0,0 +1,68 @@ +package com.bang_ggood.global.handler; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import com.bang_ggood.global.exception.OauthException; +import com.bang_ggood.global.exception.dto.ExceptionResponse; +import com.bang_ggood.global.exception.dto.OauthExceptionResponse; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BangggoodException.class) + public ResponseEntity handleBangggoodException(BangggoodException exception, + HttpServletRequest request) { + ExceptionResponse response = new ExceptionResponse( + request.getMethod(), + request.getRequestURI(), + exception.getClientExceptionCodeName(), + exception.getMessage()); + + return ResponseEntity.status(exception.getHttpStatusCode()) + .body(response); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntimeException(RuntimeException runtimeException, + HttpServletRequest request) { + ExceptionResponse response = new ExceptionResponse( + request.getMethod(), + request.getRequestURI(), + ExceptionCode.INTERNAL_SERVER_ERROR.getClientExceptionCode().name(), + ExceptionCode.INTERNAL_SERVER_ERROR.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(response); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException( + MethodArgumentNotValidException exception, + HttpServletRequest request) { + FieldError fieldError = exception.getFieldError(); + String errorMessage = ExceptionCode.INVALID_PARAMETER.getMessage(); // TODO 리팩터링 필요 + if (fieldError != null) { + errorMessage = fieldError.getDefaultMessage(); + } + + ExceptionResponse response = new ExceptionResponse( + request.getMethod(), + request.getRequestURI(), + ExceptionCode.INVALID_PARAMETER.getClientExceptionCode().name(), + errorMessage); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(response); + } + + @ExceptionHandler(OauthException.class) + public ResponseEntity handleOauthException(OauthException exception) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(exception.getResponse()); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/handler/OauthClientExceptionHandler.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/handler/OauthClientExceptionHandler.java new file mode 100644 index 000000000..5c49d189d --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/handler/OauthClientExceptionHandler.java @@ -0,0 +1,42 @@ +package com.bang_ggood.global.handler; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import com.bang_ggood.global.exception.OauthException; +import com.bang_ggood.global.exception.dto.OauthExceptionResponse; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.client.ResponseErrorHandler; +import java.io.IOException; + +@Component +public class OauthClientExceptionHandler implements ResponseErrorHandler { + + @Override + public boolean hasError(ClientHttpResponse response) { + try { + return response.getStatusCode().is4xxClientError(); + } catch (IOException exception) { + throw new BangggoodException(ExceptionCode.OAUTH_TOKEN_INTERNAL_EXCEPTION); + } + } + + @Override + public void handleError(ClientHttpResponse response) { + throw new OauthException(getResponseBody(response)); + } + + private OauthExceptionResponse getResponseBody(ClientHttpResponse response) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + + return objectMapper + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .readValue(response.getBody(), OauthExceptionResponse.class); + } catch (IOException exception) { + throw new BangggoodException(ExceptionCode.OAUTH_TOKEN_INTERNAL_EXCEPTION); + } + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/logging/LoggingAspect.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/logging/LoggingAspect.java new file mode 100644 index 000000000..6e3cc7f0e --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/logging/LoggingAspect.java @@ -0,0 +1,65 @@ +package com.bang_ggood.global.logging; + +import com.bang_ggood.global.logging.dto.ErrorLog; +import com.bang_ggood.global.logging.dto.InfoLog; +import com.bang_ggood.global.logging.dto.WarnLog; +import jakarta.servlet.http.HttpServletRequest; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import java.util.Arrays; +import java.util.Optional; + +@Aspect +@Component +public class LoggingAspect { + + private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class); + + @AfterReturning("execution(public * com.bang_ggood..*Service.*(..)))") + public void loggingInfo(JoinPoint joinPoint) { + if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes) { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + String methodName = joinPoint.getSignature().getName(); + + log.info(InfoLog.of(request, methodName).toString()); + } + } + + @Before("execution(* com.bang_ggood.global.handler.GlobalExceptionHandler.*(..)) &&" + + "!execution(* com.bang_ggood.global.handler.GlobalExceptionHandler.handleRuntimeException(..)))") + public void loggingWarn(JoinPoint joinPoint) { + Optional exceptionOptional = Arrays.stream(joinPoint.getArgs()) + .filter(arg -> arg instanceof Exception) + .map(arg -> (Exception) arg) + .findFirst(); + + if (exceptionOptional.isPresent() && RequestContextHolder.getRequestAttributes() != null) { + Exception exception = exceptionOptional.get(); + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + + log.warn(WarnLog.of(exception, request).toString()); + } + } + + @Before("execution(* com.bang_ggood.global.handler.GlobalExceptionHandler.handleRuntimeException(..)))") + public void loggingError(JoinPoint joinPoint) { + Optional exceptionOptional = Arrays.stream(joinPoint.getArgs()) + .filter(arg -> arg instanceof Exception) + .map(arg -> (Exception) arg) + .findFirst(); + + if (exceptionOptional.isPresent() && RequestContextHolder.getRequestAttributes() != null) { + Exception exception = exceptionOptional.get(); + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + + log.error(ErrorLog.of(exception, request).toString()); + } + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/logging/dto/BaseLog.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/logging/dto/BaseLog.java new file mode 100644 index 000000000..f69316062 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/logging/dto/BaseLog.java @@ -0,0 +1,35 @@ +package com.bang_ggood.global.logging.dto; + +import java.time.LocalDateTime; + +public abstract class BaseLog { + + private final LocalDateTime requestTime; + private final String requestUrl; + private final String uuid; + + public BaseLog(LocalDateTime requestTime, String requestUrl, String uuid) { + this.requestTime = requestTime; + this.requestUrl = requestUrl; + this.uuid = uuid; + } + + public LocalDateTime getRequestTime() { + return requestTime; + } + + public String getRequestUrl() { + return requestUrl; + } + + public String getUuid() { + return uuid; + } + + @Override + public String toString() { + return "requestTime=" + requestTime + + ", requestUrl='" + requestUrl + + ", uuid='" + uuid; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/logging/dto/ErrorLog.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/logging/dto/ErrorLog.java new file mode 100644 index 000000000..0af868e6e --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/logging/dto/ErrorLog.java @@ -0,0 +1,44 @@ +package com.bang_ggood.global.logging.dto; + +import jakarta.servlet.http.HttpServletRequest; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.UUID; + +public class ErrorLog extends BaseLog { + + private final String errorMessage; + private final String stackTrace; + + public ErrorLog(LocalDateTime requestTime, String requestUrl, String uuid, + String errorMessage, String stackTrace) { + super(requestTime, requestUrl, uuid); + this.errorMessage = errorMessage; + this.stackTrace = stackTrace; + } + + public static ErrorLog of(Exception exception, HttpServletRequest request) { + return new ErrorLog( + LocalDateTime.now(), + request.getMethod() + ' ' + request.getRequestURI(), + UUID.randomUUID().toString(), + exception.getMessage(), + Arrays.toString(exception.getStackTrace()) + ); + } + + public String getErrorMessage() { + return errorMessage; + } + + public String getStackTrace() { + return stackTrace; + } + + @Override + public String toString() { + return super.toString() + '\'' + + ", errorMessage='" + errorMessage + '\'' + + ", stackTrace='" + stackTrace + '\''; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/logging/dto/InfoLog.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/logging/dto/InfoLog.java new file mode 100644 index 000000000..0048bf2d7 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/logging/dto/InfoLog.java @@ -0,0 +1,34 @@ +package com.bang_ggood.global.logging.dto; + +import jakarta.servlet.http.HttpServletRequest; +import java.time.LocalDateTime; +import java.util.UUID; + +public class InfoLog extends BaseLog { + + private final String infoMethodName; + + public InfoLog(LocalDateTime requestTime, String requestUrl, String uuid, String infoMethodName) { + super(requestTime, requestUrl, uuid); + this.infoMethodName = infoMethodName; + } + + public static InfoLog of(HttpServletRequest request, String methodName) { + return new InfoLog( + LocalDateTime.now(), + request.getMethod() + ' ' + request.getRequestURI(), + UUID.randomUUID().toString(), + methodName + ); + } + + public String getInfoMethodName() { + return infoMethodName; + } + + @Override + public String toString() { + return super.toString() + '\'' + + ", infoMessage='" + infoMethodName + '\''; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/global/logging/dto/WarnLog.java b/backend/bang-ggood/src/main/java/com/bang_ggood/global/logging/dto/WarnLog.java new file mode 100644 index 000000000..da89bd9b2 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/global/logging/dto/WarnLog.java @@ -0,0 +1,34 @@ +package com.bang_ggood.global.logging.dto; + +import jakarta.servlet.http.HttpServletRequest; +import java.time.LocalDateTime; +import java.util.UUID; + +public class WarnLog extends BaseLog { + + private final String warnMessage; + + public WarnLog(LocalDateTime requestTime, String requestUrl, String uuid, String warnMessage) { + super(requestTime, requestUrl, uuid); + this.warnMessage = warnMessage; + } + + public static WarnLog of(Exception exception, HttpServletRequest request) { + return new WarnLog( + LocalDateTime.now(), + request.getMethod() + ' ' + request.getRequestURI(), + UUID.randomUUID().toString(), + exception.getMessage() + ); + } + + public String getWarnMessage() { + return warnMessage; + } + + @Override + public String toString() { + return super.toString() + '\'' + + ", warnMessage='" + warnMessage + '\''; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/like/controller/ChecklistLikeController.java b/backend/bang-ggood/src/main/java/com/bang_ggood/like/controller/ChecklistLikeController.java new file mode 100644 index 000000000..221f9b29e --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/like/controller/ChecklistLikeController.java @@ -0,0 +1,33 @@ +package com.bang_ggood.like.controller; + +import com.bang_ggood.auth.config.AuthRequiredPrincipal; +import com.bang_ggood.like.service.ChecklistLikeManageService; +import com.bang_ggood.user.domain.User; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ChecklistLikeController { + + private final ChecklistLikeManageService checklistLikeManageService; + + public ChecklistLikeController(ChecklistLikeManageService checklistLikeManageService) { + this.checklistLikeManageService = checklistLikeManageService; + } + + @PostMapping("/checklists/{id}/like") + public ResponseEntity createChecklistLike(@AuthRequiredPrincipal User user, @PathVariable("id") Long id) { + checklistLikeManageService.createLike(user, id); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/checklists/{id}/like") + public ResponseEntity deleteChecklistLikeByChecklistId(@AuthRequiredPrincipal User user, + @PathVariable("id") long id) { + checklistLikeManageService.deleteLike(user, id); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/like/domain/ChecklistLike.java b/backend/bang-ggood/src/main/java/com/bang_ggood/like/domain/ChecklistLike.java new file mode 100644 index 000000000..ff4af65b3 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/like/domain/ChecklistLike.java @@ -0,0 +1,57 @@ +package com.bang_ggood.like.domain; + +import com.bang_ggood.BaseEntity; +import com.bang_ggood.checklist.domain.Checklist; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class ChecklistLike extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + private Checklist checklist; + + public ChecklistLike(Checklist checklist) { + this.checklist = checklist; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ChecklistLike that = (ChecklistLike) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } + + @Override + public String toString() { + return "ChecklistLike{" + + "id=" + id + + ", checklist=" + checklist + + '}'; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/like/repository/ChecklistLikeRepository.java b/backend/bang-ggood/src/main/java/com/bang_ggood/like/repository/ChecklistLikeRepository.java new file mode 100644 index 000000000..2dc4f65e6 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/like/repository/ChecklistLikeRepository.java @@ -0,0 +1,15 @@ +package com.bang_ggood.like.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.like.domain.ChecklistLike; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface ChecklistLikeRepository extends JpaRepository { + + boolean existsByChecklist(Checklist checklist); + + Optional findByChecklistId(Long checklistId); +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/like/service/ChecklistLikeManageService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/like/service/ChecklistLikeManageService.java new file mode 100644 index 000000000..9d9419fb5 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/like/service/ChecklistLikeManageService.java @@ -0,0 +1,28 @@ +package com.bang_ggood.like.service; + +import com.bang_ggood.checklist.domain.Checklist; +import com.bang_ggood.checklist.service.ChecklistService; +import com.bang_ggood.user.domain.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class ChecklistLikeManageService { + + private final ChecklistService checklistService; + private final ChecklistLikeService checklistLikeService; + + @Transactional + public void createLike(User user, Long checklistId) { + Checklist checklist = checklistService.readChecklist(user, checklistId); + checklistLikeService.createLike(user, checklist); + } + + @Transactional + public void deleteLike(User user, Long checklistId) { + Checklist checklist = checklistService.readChecklist(user, checklistId); + checklistLikeService.deleteLike(user, checklist); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/like/service/ChecklistLikeService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/like/service/ChecklistLikeService.java new file mode 100644 index 000000000..464af3202 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/like/service/ChecklistLikeService.java @@ -0,0 +1,57 @@ +package com.bang_ggood.like.service; + +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.like.domain.ChecklistLike; +import com.bang_ggood.like.repository.ChecklistLikeRepository; +import com.bang_ggood.user.domain.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + +@RequiredArgsConstructor +@Service +public class ChecklistLikeService { + + private final ChecklistLikeRepository checklistLikeRepository; + + @Transactional + public void createLike(User user, Checklist checklist) { + validateChecklistOwnership(user, checklist); + + if (isChecklistAlreadyLiked(checklist)) { + return; + } + + checklistLikeRepository.save(new ChecklistLike(checklist)); + } + + private boolean isChecklistAlreadyLiked(Checklist checklist) { + return checklistLikeRepository.existsByChecklist(checklist); + } + + private void validateChecklistOwnership(User user, Checklist checklist) { + if (!checklist.isOwnedBy(user)) { + throw new BangggoodException(ExceptionCode.CHECKLIST_NOT_OWNED_BY_USER); + } + } + + @Transactional + public boolean isLikedChecklist(Checklist checklist) { + return checklistLikeRepository.existsByChecklist(checklist); + } + + @Transactional + public void deleteLike(User user, Checklist checklist) { + validateChecklistOwnership(user, checklist); + Optional checklistLike = checklistLikeRepository.findByChecklistId(checklist.getId()); + + if (checklistLike.isEmpty()) { + return; + } + + checklistLikeRepository.deleteById(checklistLike.get().getId()); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/maintenance/domain/ChecklistMaintenance.java b/backend/bang-ggood/src/main/java/com/bang_ggood/maintenance/domain/ChecklistMaintenance.java new file mode 100644 index 000000000..b7ee32f0c --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/maintenance/domain/ChecklistMaintenance.java @@ -0,0 +1,68 @@ +package com.bang_ggood.maintenance.domain; + +import com.bang_ggood.BaseEntity; +import com.bang_ggood.checklist.domain.Checklist; +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 lombok.Getter; +import lombok.NoArgsConstructor; +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class ChecklistMaintenance extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Checklist checklist; + + @Enumerated(EnumType.STRING) + private MaintenanceItem maintenanceItem; + + public ChecklistMaintenance(Checklist checklist, MaintenanceItem maintenanceItem) { + this.checklist = checklist; + this.maintenanceItem = maintenanceItem; + } + + public Integer getMaintenanceItemId() { + return maintenanceItem.getId(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ChecklistMaintenance that = (ChecklistMaintenance) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } + + @Override + public String toString() { + return "ChecklistMaintenance{" + + "id=" + id + + ", checklist=" + checklist + + ", maintenanceItem='" + maintenanceItem + '\'' + + '}'; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/maintenance/domain/MaintenanceItem.java b/backend/bang-ggood/src/main/java/com/bang_ggood/maintenance/domain/MaintenanceItem.java new file mode 100644 index 000000000..838798272 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/maintenance/domain/MaintenanceItem.java @@ -0,0 +1,42 @@ +package com.bang_ggood.maintenance.domain; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import java.util.Arrays; + +public enum MaintenanceItem { + WATERWORKS(1, "수도"), + INTERNET(2, "인터넷"), + ELECTRICITY(3, "전기"), + GAS(4, "가스"); + + private final int id; + private final String name; + + MaintenanceItem(int id, String name) { + this.id = id; + this.name = name; + } + + public static boolean contains(int id) { + return Arrays.stream(MaintenanceItem.values()) + .anyMatch(maintenanceItem -> maintenanceItem.id == id); + } + + public static MaintenanceItem fromId(int id) { + for (MaintenanceItem maintenanceItem : values()) { + if (maintenanceItem.id == id) { + return maintenanceItem; + } + } + throw new BangggoodException(ExceptionCode.MAINTENANCE_ITEM_INVALID); + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/maintenance/repository/ChecklistMaintenanceRepository.java b/backend/bang-ggood/src/main/java/com/bang_ggood/maintenance/repository/ChecklistMaintenanceRepository.java new file mode 100644 index 000000000..76eb85280 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/maintenance/repository/ChecklistMaintenanceRepository.java @@ -0,0 +1,24 @@ +package com.bang_ggood.maintenance.repository; + +import com.bang_ggood.maintenance.domain.ChecklistMaintenance; +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; + +public interface ChecklistMaintenanceRepository extends JpaRepository { + + @Query("SELECT cm FROM ChecklistMaintenance cm " + + "WHERE cm.checklist.id =:checklistId and cm.deleted = false ") + List findAllByChecklistId(@Param("checklistId") Long checklistId); + + + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Transactional + @Query("UPDATE ChecklistMaintenance cm " + + "SET cm.deleted = true " + + "WHERE cm.checklist.id = :checklistId") + void deleteAllByChecklistId(@Param("checklistId") Long checklistId); +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/maintenance/service/ChecklistMaintenanceService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/maintenance/service/ChecklistMaintenanceService.java new file mode 100644 index 000000000..e13f51509 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/maintenance/service/ChecklistMaintenanceService.java @@ -0,0 +1,53 @@ +package com.bang_ggood.maintenance.service; + +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.maintenance.domain.ChecklistMaintenance; +import com.bang_ggood.maintenance.repository.ChecklistMaintenanceRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@RequiredArgsConstructor +@Service +public class ChecklistMaintenanceService { + + private final ChecklistMaintenanceRepository checklistMaintenanceRepository; + + @Transactional + public void createMaintenances(List maintenances) { + validateMaintenancesDuplicate(maintenances); + checklistMaintenanceRepository.saveAll(maintenances); + } + + private void validateMaintenancesDuplicate(List maintenances) { + Set set = new HashSet<>(); + maintenances.forEach(maintenance -> { + Integer id = maintenance.getMaintenanceItemId(); + if (!set.add(id)) { + throw new BangggoodException(ExceptionCode.MAINTENANCE_ITEM_DUPLICATE); + } + }); + } + + @Transactional(readOnly = true) + public List readChecklistMaintenances(Checklist checklist) { + return checklistMaintenanceRepository.findAllByChecklistId(checklist.getId()); + } + + @Transactional + public void deleteAllByChecklistId(Long id) { + checklistMaintenanceRepository.deleteAllByChecklistId(id); + } + + @Transactional + public void updateMaintenances(Long checklistId, List checklistMaintenances) { + validateMaintenancesDuplicate(checklistMaintenances); + checklistMaintenanceRepository.deleteAllByChecklistId(checklistId); + checklistMaintenanceRepository.saveAll(checklistMaintenances); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/option/domain/ChecklistOption.java b/backend/bang-ggood/src/main/java/com/bang_ggood/option/domain/ChecklistOption.java new file mode 100644 index 000000000..e66d9d162 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/option/domain/ChecklistOption.java @@ -0,0 +1,63 @@ +package com.bang_ggood.option.domain; + +import com.bang_ggood.BaseEntity; +import com.bang_ggood.checklist.domain.Checklist; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class ChecklistOption extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Checklist checklist; + + @Column(nullable = false) + private Integer optionId; + + public ChecklistOption(Checklist checklist, Integer optionId) { + this.checklist = checklist; + this.optionId = optionId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ChecklistOption that = (ChecklistOption) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } + + @Override + public String toString() { + return "ChecklistOption{" + + "id=" + id + + ", checklist=" + checklist + + ", optionId=" + optionId + + '}'; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/option/domain/Option.java b/backend/bang-ggood/src/main/java/com/bang_ggood/option/domain/Option.java new file mode 100644 index 000000000..cd06be56c --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/option/domain/Option.java @@ -0,0 +1,48 @@ +package com.bang_ggood.option.domain; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import java.util.Arrays; + +public enum Option { + + REFRIGERATOR(1, "냉장고"), + SINK(2, "싱크대"), + INDUCTION(3, "인덕션"), + MICROWAVE_OVEN(4, "전자레인지"), + AIR_CONDITIONER(5, "에어컨"), + WASHING_MACHINE(6, "세탁기"), + CLOSET(7, "옷장"), + DESK(8, "책상"), + BED(9, "침대"), + SHOE_RACK(10, "신발장"), + ELEVATOR(11, "엘리베이터"); + + private final int id; + private final String name; + + Option(int id, String name) { + this.id = id; + this.name = name; + } + + public static boolean contains(int id) { + return Arrays.stream(Option.values()) + .anyMatch(option -> option.id == id); + } + + public static Option from(ChecklistOption checklistOption) { + return Arrays.stream(values()) + .filter(option -> option.id == checklistOption.getOptionId()) + .findAny() + .orElseThrow(() -> new BangggoodException(ExceptionCode.OPTION_INVALID)); + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/option/dto/response/SelectedOptionResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/option/dto/response/SelectedOptionResponse.java new file mode 100644 index 000000000..949979ae4 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/option/dto/response/SelectedOptionResponse.java @@ -0,0 +1,11 @@ +package com.bang_ggood.option.dto.response; + +import com.bang_ggood.option.domain.ChecklistOption; +import com.bang_ggood.option.domain.Option; + +public record SelectedOptionResponse(Integer optionId, String optionName) { + + public static SelectedOptionResponse from(ChecklistOption checklistOption) { + return new SelectedOptionResponse(checklistOption.getOptionId(), Option.from(checklistOption).getName()); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/option/repository/ChecklistOptionRepository.java b/backend/bang-ggood/src/main/java/com/bang_ggood/option/repository/ChecklistOptionRepository.java new file mode 100644 index 000000000..4a7bdd6f7 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/option/repository/ChecklistOptionRepository.java @@ -0,0 +1,30 @@ +package com.bang_ggood.option.repository; + +import com.bang_ggood.checklist.domain.Checklist; +import com.bang_ggood.option.domain.ChecklistOption; +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; + +public interface ChecklistOptionRepository extends JpaRepository { + + @Query("SELECT co FROM ChecklistOption co " + + "WHERE co.checklist.id = :checklistId " + + "AND co.deleted = false") + List findAllByChecklistId(@Param("checklistId") Long checklistId); + + @Query("SELECT COUNT(co) FROM ChecklistOption co " + + "WHERE co.checklist = :checklist " + + "AND co.deleted = false") + Integer countByChecklist(@Param("checklist") Checklist checklist); + + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Transactional + @Query("UPDATE ChecklistOption co " + + "SET co.deleted = true " + + "WHERE co.checklist.id = :checklistId") + void deleteAllByChecklistId(@Param("checklistId") Long checklistId); +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/option/service/ChecklistOptionService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/option/service/ChecklistOptionService.java new file mode 100644 index 000000000..cc069c025 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/option/service/ChecklistOptionService.java @@ -0,0 +1,53 @@ +package com.bang_ggood.option.service; + +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.option.domain.ChecklistOption; +import com.bang_ggood.option.repository.ChecklistOptionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@RequiredArgsConstructor +@Service +public class ChecklistOptionService { + + private final ChecklistOptionRepository checklistOptionRepository; + + @Transactional + public void createOptions(List options) { + validateOptionDuplicate(options); + checklistOptionRepository.saveAll(options); + } + + private void validateOptionDuplicate(List options) { + Set set = new HashSet<>(); + options.forEach(option -> { + Integer id = option.getOptionId(); + if (!set.add(id)) { + throw new BangggoodException(ExceptionCode.OPTION_DUPLICATED); + } + }); + } + + @Transactional(readOnly = true) + public List readChecklistOptions(Checklist checklist) { + return checklistOptionRepository.findAllByChecklistId(checklist.getId()); + } + + @Transactional + public void deleteAllByChecklistId(Long id) { + checklistOptionRepository.deleteAllByChecklistId(id); + } + + @Transactional + public void updateOptions(Long checklistId, List checklistOptions) { + validateOptionDuplicate(checklistOptions); + checklistOptionRepository.deleteAllByChecklistId(checklistId); + checklistOptionRepository.saveAll(checklistOptions); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/controller/QuestionController.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/controller/QuestionController.java new file mode 100644 index 000000000..2dd070087 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/controller/QuestionController.java @@ -0,0 +1,43 @@ +package com.bang_ggood.question.controller; + +import com.bang_ggood.auth.config.AuthRequiredPrincipal; +import com.bang_ggood.auth.config.UserPrincipal; +import com.bang_ggood.question.dto.request.CustomChecklistUpdateRequest; +import com.bang_ggood.question.dto.response.CategoryCustomChecklistQuestionsResponse; +import com.bang_ggood.question.dto.response.CustomChecklistQuestionsResponse; +import com.bang_ggood.question.service.QuestionManageService; +import com.bang_ggood.user.domain.User; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class QuestionController { + + private final QuestionManageService questionManageService; + + public QuestionController(QuestionManageService questionManageService) { + this.questionManageService = questionManageService; + } + + // TODO : 엔드포인트 통일 with CustomChecklist + @GetMapping("/checklists/questions") + public ResponseEntity readCustomChecklistQuestions(@UserPrincipal User user) { + return ResponseEntity.ok(questionManageService.readCustomChecklistQuestions(user)); + } + + @GetMapping("/custom-checklist/all") + public ResponseEntity readAllCustomChecklistQuestions( + @UserPrincipal User user) { + return ResponseEntity.ok(questionManageService.readAllCustomChecklistQuestions(user)); + } + + @PutMapping("/custom-checklist") + public ResponseEntity updateCustomChecklist(@AuthRequiredPrincipal User user, + @RequestBody CustomChecklistUpdateRequest request) { + questionManageService.updateCustomChecklist(user, request); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/Answer.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/Answer.java new file mode 100644 index 000000000..4e1910509 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/Answer.java @@ -0,0 +1,17 @@ +package com.bang_ggood.question.domain; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import java.util.Arrays; + +public enum Answer { + + GOOD, BAD, NONE; + + public static Answer from(String grade) { + return Arrays.stream(Answer.values()) + .filter(value -> value.name().equals(grade)) + .findFirst() + .orElseThrow(() -> new BangggoodException(ExceptionCode.ANSWER_INVALID)); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/Category.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/Category.java new file mode 100644 index 000000000..957c68520 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/Category.java @@ -0,0 +1,46 @@ +package com.bang_ggood.question.domain; + +import jakarta.persistence.Column; +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 Category { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(nullable = false) + private String name; + + public Category(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Category that = (Category) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/ChecklistQuestion.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/ChecklistQuestion.java new file mode 100644 index 000000000..ca5501869 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/ChecklistQuestion.java @@ -0,0 +1,91 @@ +package com.bang_ggood.question.domain; + +import com.bang_ggood.BaseEntity; +import com.bang_ggood.checklist.domain.Checklist; +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.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class ChecklistQuestion extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Checklist checklist; + + @JoinColumn(name = "question_id") + @ManyToOne(fetch = FetchType.LAZY) + private Question question; + + @Enumerated(EnumType.STRING) + private Answer answer; + + public ChecklistQuestion(Checklist checklist, Question question, Answer answer) { + this.checklist = checklist; + this.answer = answer; + this.question = question; + } + + public void change(ChecklistQuestion checklistQuestion) { + this.answer = checklistQuestion.answer; + } + + public boolean isDifferentQuestionId(ChecklistQuestion checklistQuestion) { // TODO 리팩토링 + return !getQuestionId().equals(checklistQuestion.getQuestionId()); + } + + public Long getChecklistId() { + return checklist.getId(); + } + + public Integer getQuestionId() { + return question.getId(); + } + + public boolean isCategory(Category category) { + return question.getCategory().equals(category); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ChecklistQuestion that = (ChecklistQuestion) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } + + @Override + public String toString() { + return "ChecklistQuestion{" + + "id=" + id + + ", checklist=" + checklist + + ", question=" + question + + ", answer=" + answer + + '}'; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/CustomChecklistQuestion.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/CustomChecklistQuestion.java new file mode 100644 index 000000000..c6852a3f3 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/CustomChecklistQuestion.java @@ -0,0 +1,45 @@ +package com.bang_ggood.question.domain; + +import com.bang_ggood.BaseEntity; +import com.bang_ggood.user.domain.User; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class CustomChecklistQuestion extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private User user; + + @JoinColumn(name = "question_id") + @ManyToOne(fetch = FetchType.LAZY) + private Question question; + + public CustomChecklistQuestion(User user, Question question) { + this.user = user; + this.question = question; + } + + public Integer getQuestionId() { + return question.getId(); + } + + public boolean isSameCategory(Category category) { + return this.question.getCategory().equals(category); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/Highlight.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/Highlight.java new file mode 100644 index 000000000..41af7c85b --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/Highlight.java @@ -0,0 +1,53 @@ +package com.bang_ggood.question.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class Highlight { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY) + private Question question; + + @Column(nullable = false) + private String name; + + public Highlight(Question question, String name) { + this.question = question; + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Highlight highlight = (Highlight) o; + return Objects.equals(id, highlight.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/Question.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/Question.java new file mode 100644 index 000000000..546988e5a --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/domain/Question.java @@ -0,0 +1,64 @@ +package com.bang_ggood.question.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +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 Question { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY) + private Category category; + + @Column(nullable = false) + private String title; + + private String subtitle; + + private boolean isDefault; + + public Question(Category category, String title, String subtitle, boolean isDefault) { + this.category = category; + this.title = title; + this.subtitle = subtitle; + this.isDefault = isDefault; + } + + public boolean isSelected(List questions) { + return questions.stream() + .anyMatch(question -> question.getQuestionId() == this.id); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Question that = (Question) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/request/CustomChecklistUpdateRequest.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/request/CustomChecklistUpdateRequest.java new file mode 100644 index 000000000..8cf415546 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/request/CustomChecklistUpdateRequest.java @@ -0,0 +1,6 @@ +package com.bang_ggood.question.dto.request; + +import java.util.List; + +public record CustomChecklistUpdateRequest(List questionIds) { +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/request/QuestionRequest.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/request/QuestionRequest.java new file mode 100644 index 000000000..c74c489aa --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/request/QuestionRequest.java @@ -0,0 +1,6 @@ +package com.bang_ggood.question.dto.request; + +import jakarta.validation.constraints.NotNull; + +public record QuestionRequest(@NotNull(message = "질문 아이디가 존재하지 않습니다.") Integer questionId, String answer) { +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/CategoryCustomChecklistQuestionResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/CategoryCustomChecklistQuestionResponse.java new file mode 100644 index 000000000..8952fcc5f --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/CategoryCustomChecklistQuestionResponse.java @@ -0,0 +1,13 @@ +package com.bang_ggood.question.dto.response; + +import com.bang_ggood.question.domain.Category; +import java.util.List; + +public record CategoryCustomChecklistQuestionResponse(Integer categoryId, String categoryName, + List questions) { + + public static CategoryCustomChecklistQuestionResponse of(Category category, + List questions) { + return new CategoryCustomChecklistQuestionResponse(category.getId(), category.getName(), questions); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/CategoryCustomChecklistQuestionsResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/CategoryCustomChecklistQuestionsResponse.java new file mode 100644 index 000000000..51473f2a7 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/CategoryCustomChecklistQuestionsResponse.java @@ -0,0 +1,11 @@ +package com.bang_ggood.question.dto.response; + +import java.util.List; + +public record CategoryCustomChecklistQuestionsResponse(List categories) { + + public static CategoryCustomChecklistQuestionsResponse from( + List categories) { + return new CategoryCustomChecklistQuestionsResponse(categories); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/CategoryQuestionsResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/CategoryQuestionsResponse.java new file mode 100644 index 000000000..73337c660 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/CategoryQuestionsResponse.java @@ -0,0 +1,11 @@ +package com.bang_ggood.question.dto.response; + +import com.bang_ggood.question.domain.Category; +import java.util.List; + +public record CategoryQuestionsResponse(Integer categoryId, String categoryName, List questions) { + + public static CategoryQuestionsResponse of(Category category, List questions) { + return new CategoryQuestionsResponse(category.getId(), category.getName(), questions); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/CustomChecklistQuestionResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/CustomChecklistQuestionResponse.java new file mode 100644 index 000000000..a43ab5176 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/CustomChecklistQuestionResponse.java @@ -0,0 +1,19 @@ +package com.bang_ggood.question.dto.response; + +import com.bang_ggood.question.domain.Highlight; +import com.bang_ggood.question.domain.Question; +import java.util.List; + +public class CustomChecklistQuestionResponse extends QuestionResponse { + + private final boolean isSelected; + + public CustomChecklistQuestionResponse(Question question, List highlights, boolean isSelected) { + super(question, highlights); + this.isSelected = isSelected; + } + + public boolean getIsSelected() { + return isSelected; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/CustomChecklistQuestionsResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/CustomChecklistQuestionsResponse.java new file mode 100644 index 000000000..1083b6e9a --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/CustomChecklistQuestionsResponse.java @@ -0,0 +1,6 @@ +package com.bang_ggood.question.dto.response; + +import java.util.List; + +public record CustomChecklistQuestionsResponse(List categories) { +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/QuestionResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/QuestionResponse.java new file mode 100644 index 000000000..47846205b --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/QuestionResponse.java @@ -0,0 +1,38 @@ +package com.bang_ggood.question.dto.response; + +import com.bang_ggood.question.domain.Highlight; +import com.bang_ggood.question.domain.Question; +import java.util.List; + +public class QuestionResponse { + + private final Integer questionId; + private final String title; + private final String subtitle; + private final List highlights; + + public QuestionResponse(Question question, List highlights) { + this.questionId = question.getId(); + this.title = question.getTitle(); + this.subtitle = question.getSubtitle(); + this.highlights = highlights.stream() + .map(Highlight::getName) + .toList(); + } + + public Integer getQuestionId() { + return questionId; + } + + public String getTitle() { + return title; + } + + public String getSubtitle() { + return subtitle; + } + + public List getHighlights() { + return highlights; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/SelectedCategoryQuestionsResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/SelectedCategoryQuestionsResponse.java new file mode 100644 index 000000000..3cb7f7568 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/SelectedCategoryQuestionsResponse.java @@ -0,0 +1,16 @@ +package com.bang_ggood.question.dto.response; + +import com.bang_ggood.question.domain.Category; +import java.util.List; + +public record SelectedCategoryQuestionsResponse(Integer categoryId, String categoryName, + List questions) { + + public static SelectedCategoryQuestionsResponse of(Category category, List questions) { + return new SelectedCategoryQuestionsResponse( + category.getId(), + category.getName(), + questions + ); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/SelectedQuestionResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/SelectedQuestionResponse.java new file mode 100644 index 000000000..ab7c8b74a --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/dto/response/SelectedQuestionResponse.java @@ -0,0 +1,19 @@ +package com.bang_ggood.question.dto.response; + +import com.bang_ggood.question.domain.ChecklistQuestion; +import com.bang_ggood.question.domain.Highlight; +import java.util.List; + +public class SelectedQuestionResponse extends QuestionResponse { + + private final String answer; + + public SelectedQuestionResponse(ChecklistQuestion checklistQuestion, List highlights) { + super(checklistQuestion.getQuestion(), highlights); + this.answer = checklistQuestion.getAnswer().name(); + } + + public String getAnswer() { + return answer; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/repository/CategoryRepository.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/repository/CategoryRepository.java new file mode 100644 index 000000000..a251136bf --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/repository/CategoryRepository.java @@ -0,0 +1,17 @@ +package com.bang_ggood.question.repository; + +import com.bang_ggood.question.domain.Category; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import java.util.List; + +public interface CategoryRepository extends JpaRepository { + + @Query(value = "SELECT distinct c.* FROM category c " + + "JOIN question q ON q.category_id = c.id " + + "JOIN custom_checklist_question ccq ON ccq.question_id = q.id " + + "WHERE ccq.user_id = :userId AND ccq.deleted = false ", + nativeQuery = true) + List findAllCustomQuestionCategoriesByUserId(@Param("userId") Long userId); +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/repository/ChecklistQuestionRepository.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/repository/ChecklistQuestionRepository.java new file mode 100644 index 000000000..ff505d864 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/repository/ChecklistQuestionRepository.java @@ -0,0 +1,24 @@ +package com.bang_ggood.question.repository; + +import com.bang_ggood.question.domain.ChecklistQuestion; +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; + +public interface ChecklistQuestionRepository extends JpaRepository { + + @Query("SELECT cq FROM ChecklistQuestion cq " + + "WHERE cq.checklist.id = :checklistId " + + "AND cq.deleted = false") + List findAllByChecklistId(@Param("checklistId") Long checklistId); + + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Transactional + @Query("UPDATE ChecklistQuestion cq " + + "SET cq.deleted = true " + + "WHERE cq.checklist.id = :checklistId") + void deleteAllByChecklistId(@Param("checklistId") Long checklistId); +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/repository/CustomChecklistQuestionRepository.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/repository/CustomChecklistQuestionRepository.java new file mode 100644 index 000000000..03d5964aa --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/repository/CustomChecklistQuestionRepository.java @@ -0,0 +1,19 @@ +package com.bang_ggood.question.repository; + +import com.bang_ggood.question.domain.CustomChecklistQuestion; +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 java.util.List; + +public interface CustomChecklistQuestionRepository extends JpaRepository { + + @Query("SELECT c FROM CustomChecklistQuestion c WHERE c.user.id = :#{#user.id} AND c.deleted = false ") + List findAllByUser(@Param("user") User user); + + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("UPDATE CustomChecklistQuestion SET deleted = true WHERE user.id = :#{#user.id}") + void deleteAllByUser(@Param("user") User user); +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/repository/HighlightRepository.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/repository/HighlightRepository.java new file mode 100644 index 000000000..838bf8d2b --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/repository/HighlightRepository.java @@ -0,0 +1,10 @@ +package com.bang_ggood.question.repository; + +import com.bang_ggood.question.domain.Highlight; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface HighlightRepository extends JpaRepository { + + List findAllByQuestionId(Integer id); +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/repository/QuestionRepository.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/repository/QuestionRepository.java new file mode 100644 index 000000000..e1de25d59 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/repository/QuestionRepository.java @@ -0,0 +1,20 @@ +package com.bang_ggood.question.repository; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import com.bang_ggood.question.domain.Question; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface QuestionRepository extends JpaRepository { + + default Question getById(Integer id) { + return findById(id).orElseThrow(() -> new BangggoodException(ExceptionCode.QUESTION_INVALID)); + } + + List findAllByCategoryId(Integer id); + + List findAllByIsDefaultTrue(); + + List findAllByIdIn(List questionIds); +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/service/ChecklistQuestionService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/service/ChecklistQuestionService.java new file mode 100644 index 000000000..91b0fbbbb --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/service/ChecklistQuestionService.java @@ -0,0 +1,116 @@ +package com.bang_ggood.question.service; + +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.question.domain.Category; +import com.bang_ggood.question.domain.ChecklistQuestion; +import com.bang_ggood.question.domain.CustomChecklistQuestion; +import com.bang_ggood.question.domain.Question; +import com.bang_ggood.question.repository.ChecklistQuestionRepository; +import com.bang_ggood.question.repository.CustomChecklistQuestionRepository; +import com.bang_ggood.user.domain.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@RequiredArgsConstructor +@Service +public class ChecklistQuestionService { + + private final ChecklistQuestionRepository checklistQuestionRepository; + private final CustomChecklistQuestionRepository customChecklistQuestionRepository; + private final QuestionService questionService; // TODO 리팩터링 + + @Transactional + public void createDefaultCustomQuestions(List customChecklistQuestions) { + customChecklistQuestionRepository.saveAll(customChecklistQuestions); + } + + @Transactional + public void createQuestions(List checklistQuestions) { + validateQuestionDuplicate(checklistQuestions); + checklistQuestionRepository.saveAll(checklistQuestions); + } + + private void validateQuestionDuplicate(List questions) { + Set set = new HashSet<>(); + questions.forEach(question -> { + if (!set.add(question.getQuestionId())) { + throw new BangggoodException(ExceptionCode.QUESTION_DUPLICATED); + } + }); + } + + @Transactional(readOnly = true) + public List readCustomChecklistQuestions(User user) { + return customChecklistQuestionRepository.findAllByUser(user); + } + + @Transactional + public void updateCustomChecklist(User user, List questions) { + validateCustomChecklistQuestionsIsNotEmpty(questions); + validateCustomChecklistQuestionsDuplication(questions); + + customChecklistQuestionRepository.deleteAllByUser(user); + + List customChecklistQuestions = questions.stream() + .map(question -> new CustomChecklistQuestion(user, questionService.readQuestion(question.getId()))) + .toList(); + customChecklistQuestionRepository.saveAll(customChecklistQuestions); + } + + private void validateCustomChecklistQuestionsIsNotEmpty(List questions) { + if (questions.isEmpty()) { + throw new BangggoodException(ExceptionCode.CUSTOM_CHECKLIST_QUESTION_EMPTY); + } + } + + private void validateCustomChecklistQuestionsDuplication(List questions) { + if (questions.size() != Set.copyOf(questions).size()) { + throw new BangggoodException(ExceptionCode.QUESTION_DUPLICATED); + } + } + + @Transactional(readOnly = true) + public List readChecklistQuestions(Checklist checklist) { + return checklistQuestionRepository.findAllByChecklistId(checklist.getId()); + } + + @Transactional(readOnly = true) + public List categorizeChecklistQuestions(Category category, List questions) { + return questions.stream() + .filter(question -> question.isCategory(category) && question.getAnswer() != null) + .toList(); + } + + @Transactional + public void deleteAllByChecklistId(Long id) { + checklistQuestionRepository.deleteAllByChecklistId(id); + } + + @Transactional + public void updateQuestions(List questions, List updateQuestions) { + validateQuestionDuplicate(updateQuestions); + validateSameQuestions(questions, updateQuestions); + for (int i = 0; i < questions.size(); i++) { + questions.get(i).change(updateQuestions.get(i)); + } + checklistQuestionRepository.saveAll(questions); + } + + private void validateSameQuestions(List questions, List updateQuestions) { + if (questions.size() != updateQuestions.size()) { + throw new BangggoodException(ExceptionCode.QUESTION_DIFFERENT); + } + + for (int i = 0; i < questions.size(); i++) { + if (questions.get(i).isDifferentQuestionId(updateQuestions.get(i))) { + throw new BangggoodException(ExceptionCode.QUESTION_DIFFERENT); + } + } + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/service/QuestionManageService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/service/QuestionManageService.java new file mode 100644 index 000000000..aa49e7b66 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/service/QuestionManageService.java @@ -0,0 +1,93 @@ +package com.bang_ggood.question.service; + +import com.bang_ggood.question.domain.Category; +import com.bang_ggood.question.domain.CustomChecklistQuestion; +import com.bang_ggood.question.domain.Question; +import com.bang_ggood.question.dto.request.CustomChecklistUpdateRequest; +import com.bang_ggood.question.dto.response.CategoryCustomChecklistQuestionResponse; +import com.bang_ggood.question.dto.response.CategoryCustomChecklistQuestionsResponse; +import com.bang_ggood.question.dto.response.CategoryQuestionsResponse; +import com.bang_ggood.question.dto.response.CustomChecklistQuestionResponse; +import com.bang_ggood.question.dto.response.CustomChecklistQuestionsResponse; +import com.bang_ggood.question.dto.response.QuestionResponse; +import com.bang_ggood.user.domain.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +@Service +public class QuestionManageService { + + private final ChecklistQuestionService checklistQuestionService; + private final QuestionService questionService; + + @Transactional + public void createDefaultCustomChecklistQuestions(User user) { + List customChecklistQuestions = questionService.findDefaultQuestions() + .stream() + .map(question -> new CustomChecklistQuestion(user, question)) + .toList(); + + checklistQuestionService.createDefaultCustomQuestions(customChecklistQuestions); + } + + @Transactional(readOnly = true) + public CustomChecklistQuestionsResponse readCustomChecklistQuestions(User user) { + List customChecklistQuestions = checklistQuestionService.readCustomChecklistQuestions( + user); + List categoryQuestionsResponses = categorizeCustomChecklistQuestions(user, customChecklistQuestions).stream() + .filter(categoryQuestionsResponse -> !categoryQuestionsResponse.questions().isEmpty()) + .toList(); + + return new CustomChecklistQuestionsResponse(categoryQuestionsResponses); + } + + private List categorizeCustomChecklistQuestions(User user, List customChecklistQuestions) { + List categoryQuestionsResponses = new ArrayList<>(); + + for (Category category : questionService.findAllCustomQuestionCategories(user)) { + List questionResponses = customChecklistQuestions.stream() + .filter(customChecklistQuestion -> customChecklistQuestion.isSameCategory(category)) + .map(customChecklistQuestion -> new QuestionResponse(customChecklistQuestion.getQuestion(), questionService.readHighlights(customChecklistQuestion.getQuestionId()))) + .toList(); + + categoryQuestionsResponses.add(CategoryQuestionsResponse.of(category, questionResponses)); + } + + return categoryQuestionsResponses; + } + + @Transactional(readOnly = true) + public CategoryCustomChecklistQuestionsResponse readAllCustomChecklistQuestions(User user) { + List customChecklistQuestions = checklistQuestionService.readCustomChecklistQuestions( + user); + return categorizeAllQuestionsWithSelected(customChecklistQuestions); + } + + private CategoryCustomChecklistQuestionsResponse categorizeAllQuestionsWithSelected( + List customChecklistQuestions) { + List response = new ArrayList<>(); + + for (Category category : questionService.findAllCategories()) { + List categoryQuestions = questionService.readQuestionsByCategory(category); + List questions = categoryQuestions.stream() + .map(question -> new CustomChecklistQuestionResponse( + question, + questionService.readHighlights(question.getId()), + question.isSelected(customChecklistQuestions))) + .toList(); + response.add(CategoryCustomChecklistQuestionResponse.of(category, questions)); + } + + return CategoryCustomChecklistQuestionsResponse.from(response); + } + + @Transactional + public void updateCustomChecklist(User user, CustomChecklistUpdateRequest request) { + List questions = questionService.readAllQuestionByIds(request.questionIds()); + checklistQuestionService.updateCustomChecklist(user, questions); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/question/service/QuestionService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/question/service/QuestionService.java new file mode 100644 index 000000000..f33fe09fd --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/question/service/QuestionService.java @@ -0,0 +1,83 @@ +package com.bang_ggood.question.service; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import com.bang_ggood.question.domain.Category; +import com.bang_ggood.question.domain.Highlight; +import com.bang_ggood.question.domain.Question; +import com.bang_ggood.question.repository.CategoryRepository; +import com.bang_ggood.question.repository.HighlightRepository; +import com.bang_ggood.question.repository.QuestionRepository; +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; +import java.util.Set; + +@RequiredArgsConstructor +@Service +public class QuestionService { + + private final CategoryRepository categoryRepository; + private final QuestionRepository questionRepository; + private final HighlightRepository highlightRepository; + + @Transactional(readOnly = true) + public List findAllCategories() { + return categoryRepository.findAll(); + } + + @Transactional(readOnly = true) + public List findAllCustomQuestionCategories(User user) { + return categoryRepository.findAllCustomQuestionCategoriesByUserId(user.getId()); + } + + @Transactional(readOnly = true) + public List findDefaultQuestions() { + return questionRepository.findAllByIsDefaultTrue(); + } + + @Transactional(readOnly = true) + public Question readQuestion(Integer questionId) { + return questionRepository.getById(questionId); + } + + @Transactional(readOnly = true) + public List readAllQuestionByIds(List questionIds) { + validateQuestionsEmpty(questionIds); + validateQuestionsDuplication(questionIds); + + List questions = questionRepository.findAllByIdIn(questionIds); + validateAllQuestionsSelected(questionIds, questions); + return questions; + } + + private void validateQuestionsEmpty(List ids) { + if (ids.isEmpty()) { + throw new BangggoodException(ExceptionCode.CUSTOM_CHECKLIST_QUESTION_EMPTY); // TODO 예외 메시지 변경 + } + } + + private void validateQuestionsDuplication(List ids) { + if (ids.size() != Set.copyOf(ids).size()) { + throw new BangggoodException(ExceptionCode.QUESTION_DUPLICATED); + } + } + + private void validateAllQuestionsSelected(List ids, List questions) { + if (ids.size() != questions.size()) { + throw new BangggoodException(ExceptionCode.QUESTION_INVALID); + } + } + + @Transactional(readOnly = true) + public List readHighlights(Integer questionId) { + return highlightRepository.findAllByQuestionId(questionId); + } + + @Transactional(readOnly = true) + public List readQuestionsByCategory(Category category) { + return questionRepository.findAllByCategoryId(category.getId()); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/room/domain/FloorLevel.java b/backend/bang-ggood/src/main/java/com/bang_ggood/room/domain/FloorLevel.java new file mode 100644 index 000000000..3ac1fc173 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/room/domain/FloorLevel.java @@ -0,0 +1,33 @@ +package com.bang_ggood.room.domain; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import java.util.Arrays; + +public enum FloorLevel { + + GROUND("지상"), + BASEMENT("반지하/지하"), + ROOFTOP("옥탑"), + NONE(null); + + private final String name; + + FloorLevel(String name) { + this.name = name; + } + + public static FloorLevel from(String name) { + if (name == null) { + return NONE; + } + return Arrays.stream(FloorLevel.values()) + .filter(value -> value.name != null && value.name.equals(name)) + .findFirst() + .orElseThrow(() -> new BangggoodException(ExceptionCode.FLOOR_LEVEL_INVALID)); + } + + public String getName() { + return name; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/room/domain/Room.java b/backend/bang-ggood/src/main/java/com/bang_ggood/room/domain/Room.java new file mode 100644 index 000000000..3ab48d5e1 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/room/domain/Room.java @@ -0,0 +1,112 @@ +package com.bang_ggood.room.domain; + +import com.bang_ggood.BaseEntity; +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +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 Room extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + private String address; + + private String buildingName; + + private String station; + + private Integer walkingTime; + + @Enumerated(EnumType.STRING) + private FloorLevel floorLevel; + + private Integer floor; + + @Enumerated(EnumType.STRING) + private Structure structure; + + private Double size; + + public Room(String name, String address, String buildingName, String station, Integer walkingTime, + FloorLevel floorLevel, Integer floor, Structure structure, Double size) { + this.name = name; + this.address = address; + this.buildingName = buildingName; + this.station = station; + this.walkingTime = walkingTime; + this.floorLevel = floorLevel; + this.floor = floor; + this.structure = structure; + this.size = size; + validateFloorAndLevel(); + } + + public void change(Room room) { + this.name = room.name; + this.address = room.address; + this.buildingName = room.buildingName; + this.station = room.station; + this.walkingTime = room.walkingTime; + this.floorLevel = room.floorLevel; + this.floor = room.floor; + this.structure = room.structure; + this.size = room.size; + validateFloorAndLevel(); + } + + private void validateFloorAndLevel() { + if (floorLevel != FloorLevel.GROUND && floor != null) { + throw new BangggoodException(ExceptionCode.ROOM_FLOOR_AND_LEVEL_INVALID); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Room room = (Room) o; + return Objects.equals(id, room.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } + + @Override + public String toString() { + return "Room{" + + "id=" + id + + ", name='" + name + '\'' + + ", address='" + address + '\'' + + ", buildingName='" + buildingName + '\'' + + ", station='" + station + '\'' + + ", walkingTime=" + walkingTime + + ", floorLevel=" + floorLevel + + ", floor=" + floor + + ", structure=" + structure + + ", size=" + size + + '}'; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/room/domain/Structure.java b/backend/bang-ggood/src/main/java/com/bang_ggood/room/domain/Structure.java new file mode 100644 index 000000000..23f323166 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/room/domain/Structure.java @@ -0,0 +1,35 @@ +package com.bang_ggood.room.domain; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import java.util.Arrays; + +public enum Structure { + + OPEN_ONE_ROOM("오픈형 원룸"), + DIVIDED_ONE_ROOM("분리형 원룸"), + TWO_ROOM("투룸"), + THREE_ROOM_OR_MORE("쓰리룸 이상"), + DUPLEX("복층"), + NONE(null); + + private final String name; + + Structure(String name) { + this.name = name; + } + + public static Structure from(String name) { + if (name == null) { + return NONE; + } + return Arrays.stream(Structure.values()) + .filter(value -> value.name != null && value.name.equals(name)) + .findFirst() + .orElseThrow(() -> new BangggoodException(ExceptionCode.STRUCTURE_INVALID)); + } + + public String getName() { + return name; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/room/dto/request/RoomRequest.java b/backend/bang-ggood/src/main/java/com/bang_ggood/room/dto/request/RoomRequest.java new file mode 100644 index 000000000..7672a05e4 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/room/dto/request/RoomRequest.java @@ -0,0 +1,22 @@ +package com.bang_ggood.room.dto.request; + +import com.bang_ggood.room.domain.FloorLevel; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.domain.Structure; +import jakarta.validation.constraints.NotBlank; +import java.util.List; + +public record RoomRequest(@NotBlank(message = "방 이름이 존재하지 않습니다.") String roomName, + String address, String buildingName, String station, + Integer walkingTime, Integer deposit, Integer rent, Integer maintenanceFee, + List includedMaintenances, String floorLevel, Integer floor, + String structure, Double size, Integer contractTerm, Integer occupancyMonth, + String occupancyPeriod, + String realEstate, String memo, String summary +) { + + public Room toRoomEntity() { + return new Room(roomName, address, buildingName, station, walkingTime, + FloorLevel.from(floorLevel), floor, Structure.from(structure), size); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/room/dto/response/SelectedRoomResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/room/dto/response/SelectedRoomResponse.java new file mode 100644 index 000000000..1ad7c1023 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/room/dto/response/SelectedRoomResponse.java @@ -0,0 +1,25 @@ +package com.bang_ggood.room.dto.response; + +import com.bang_ggood.checklist.domain.Checklist; +import java.time.LocalDateTime; +import java.util.List; + +public record SelectedRoomResponse(String roomName, Integer deposit, Integer rent, Integer contractTerm, Integer floor, + String address, String buildingName, String station, Integer walkingTime, + String realEstate, + Double size, String floorLevel, String structure, + Integer occupancyMonth, String occupancyPeriod, String memo, String summary, + List includedMaintenances, Integer maintenanceFee, + LocalDateTime createdAt) { + + public static SelectedRoomResponse of(Checklist checklist, List includedMaintenances) { + return new SelectedRoomResponse(checklist.getRoomName(), checklist.getDeposit(), checklist.getRent(), + checklist.getContractTerm(), checklist.getRoomFloor(), checklist.getRoomAddress(), + checklist.getRoomBuildingName(), + checklist.getRoomStation(), checklist.getRoomWalkingTime(), checklist.getRealEstate(), + checklist.getRoomSize(), checklist.getRoomFloorLevel().getName(), + checklist.getRoomStructure().getName(), checklist.getOccupancyMonth(), checklist.getOccupancyPeriod(), + checklist.getMemo(), checklist.getSummary(), includedMaintenances, checklist.getMaintenanceFee(), + checklist.getCreatedAt()); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/room/dto/response/SelectedRoomResponseV1.java b/backend/bang-ggood/src/main/java/com/bang_ggood/room/dto/response/SelectedRoomResponseV1.java new file mode 100644 index 000000000..73ba23eea --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/room/dto/response/SelectedRoomResponseV1.java @@ -0,0 +1,24 @@ +package com.bang_ggood.room.dto.response; + +import java.time.LocalDateTime; +import java.util.List; + +public record SelectedRoomResponseV1(String roomName, Integer deposit, Integer rent, Integer contractTerm, + Integer floor, + String address, String buildingName, String realEstate, + Double size, String floorLevel, String structure, + Integer occupancyMonth, String occupancyPeriod, String memo, String summary, + List includedMaintenances, Integer maintenanceFee, + LocalDateTime createdAt) { + + public static SelectedRoomResponseV1 from(SelectedRoomResponse response) { + return new SelectedRoomResponseV1( + response.roomName(), response.deposit(), response.rent(), + response.contractTerm(), response.floor(), response.address(), + response.buildingName(), response.realEstate(), + response.size(), response.floorLevel(), response.structure(), + response.occupancyMonth(), response.occupancyPeriod(), response.memo(), response.summary(), + response.includedMaintenances(), response.maintenanceFee(), response.createdAt() + ); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/room/repository/RoomRepository.java b/backend/bang-ggood/src/main/java/com/bang_ggood/room/repository/RoomRepository.java new file mode 100644 index 000000000..3a9d9b2bf --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/room/repository/RoomRepository.java @@ -0,0 +1,18 @@ +package com.bang_ggood.room.repository; + +import com.bang_ggood.room.domain.Room; +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; + +public interface RoomRepository extends JpaRepository { + + @Transactional + @Modifying + @Query("UPDATE Room r " + + "SET r.deleted = true " + + "WHERE r.id = :id") + void deleteById(@Param("id") Long id); +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/room/service/RoomService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/room/service/RoomService.java new file mode 100644 index 000000000..46e3634e9 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/room/service/RoomService.java @@ -0,0 +1,29 @@ +package com.bang_ggood.room.service; + +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.repository.RoomRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class RoomService { + + private final RoomRepository roomRepository; + + @Transactional + public Room createRoom(Room room) { + return roomRepository.save(room); + } + + @Transactional + public void deleteById(Long id) { + roomRepository.deleteById(id); + } + + @Transactional + public void updateRoom(Room room, Room updateRoom) { + room.change(updateRoom); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/station/SubwayReader.java b/backend/bang-ggood/src/main/java/com/bang_ggood/station/SubwayReader.java new file mode 100644 index 000000000..78d0186bd --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/station/SubwayReader.java @@ -0,0 +1,52 @@ +package com.bang_ggood.station; + +import com.bang_ggood.station.domain.SubwayStation; +import com.opencsv.CSVReader; +import com.opencsv.CSVReaderBuilder; +import com.opencsv.exceptions.CsvValidationException; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +public class SubwayReader { + + private static final String SUBWAY_DATA_PATH = "classpath*:seoul_stations*.csv"; + + public static List readSubwayStationData() { + List stations = new ArrayList<>(); + try (CSVReader csvReader = new CSVReaderBuilder( + new InputStreamReader(getSubwayStationResource().getInputStream(), + Charset.forName("EUC-KR"))).build()) { + String[] line = csvReader.readNext(); // drop first row + while ((line = csvReader.readNext()) != null) { + SubwayStation station = new SubwayStation( + Integer.parseInt(line[0]), + line[1], + line[2], + Double.parseDouble(line[3]), + Double.parseDouble(line[4])); + stations.add(station); + } + return stations; + } catch (IOException | CsvValidationException e) { + throw new RuntimeException("지하철 데이터 파일을 읽어오는데 실패했습니다."); + } + } + + private static Resource getSubwayStationResource() throws IOException { + ResourcePatternResolver patternResolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = patternResolver.getResources(SUBWAY_DATA_PATH); + + return Arrays.stream(resources) + .filter(resource -> resource.getFilename() != null) + .max(Comparator.comparing(Resource::getFilename)) + .orElseThrow(() -> new RuntimeException(SUBWAY_DATA_PATH + "를 읽어오는데 실패했습니다.")); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/station/controller/SubwayStationController.java b/backend/bang-ggood/src/main/java/com/bang_ggood/station/controller/SubwayStationController.java new file mode 100644 index 000000000..6549e461a --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/station/controller/SubwayStationController.java @@ -0,0 +1,26 @@ +package com.bang_ggood.station.controller; + +import com.bang_ggood.station.dto.response.SubwayStationResponses; +import com.bang_ggood.station.service.SubwayStationService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class SubwayStationController { + + private final SubwayStationService subwayStationService; + + public SubwayStationController(SubwayStationService subwayStationService) { + this.subwayStationService = subwayStationService; + } + + @GetMapping("/stations/nearest") + public ResponseEntity readNearestStation(@RequestParam("latitude") Double latitude, + @RequestParam("longitude") Double longitude) { + + SubwayStationResponses response = subwayStationService.readNearestStation(latitude, longitude); + return ResponseEntity.ok(response); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/station/domain/ChecklistStation.java b/backend/bang-ggood/src/main/java/com/bang_ggood/station/domain/ChecklistStation.java new file mode 100644 index 000000000..87029184f --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/station/domain/ChecklistStation.java @@ -0,0 +1,58 @@ +package com.bang_ggood.station.domain; + +import com.bang_ggood.BaseEntity; +import com.bang_ggood.checklist.domain.Checklist; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class ChecklistStation extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private Checklist checklist; + + private String stationName; + + private String stationLine; + + private int walkingTime; + + public ChecklistStation(Checklist checklist, String stationName, String stationLine, int walkingTime) { + this.checklist = checklist; + this.stationName = stationName; + this.stationLine = stationLine; + this.walkingTime = walkingTime; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ChecklistStation that = (ChecklistStation) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/station/domain/SubwayStation.java b/backend/bang-ggood/src/main/java/com/bang_ggood/station/domain/SubwayStation.java new file mode 100644 index 000000000..9559ea5ec --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/station/domain/SubwayStation.java @@ -0,0 +1,44 @@ +package com.bang_ggood.station.domain; + +import lombok.Getter; + +@Getter +public class SubwayStation { + + private static final int METER_PER_DEGREE = 111_320; + // meter per second * minute unit * decreasing speed on open street + private static final double AVERAGE_WALKING_SPEED = 1.3 * 60 * 0.4; + + private final Integer id; + private final String name; + private final String line; + private final double latitude; + private final double longitude; + + public SubwayStation(Integer id, String name, String line, double latitude, double longitude) { + this.id = id; + this.name = name; + this.line = line; + this.latitude = latitude; + this.longitude = longitude; + } + + public int calculateWalkingTime(double latitude, double longitude) { + double dx = (this.latitude - latitude) * METER_PER_DEGREE; + double dy = (this.longitude - longitude) * METER_PER_DEGREE * Math.cos(this.latitude); + double distance = Math.sqrt(dx * dx + dy * dy); + + return (int) (Math.round(distance) / AVERAGE_WALKING_SPEED); + } + + @Override + public String toString() { + return "Station{" + + "id=" + id + + ", name='" + name + '\'' + + ", line='" + line + '\'' + + ", latitude=" + latitude + + ", longitude=" + longitude + + '}'; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/station/dto/request/ChecklistStationRequest.java b/backend/bang-ggood/src/main/java/com/bang_ggood/station/dto/request/ChecklistStationRequest.java new file mode 100644 index 000000000..4b4db9c47 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/station/dto/request/ChecklistStationRequest.java @@ -0,0 +1,8 @@ +package com.bang_ggood.station.dto.request; + +public record ChecklistStationRequest(double latitude, double longitude) { + + public static ChecklistStationRequest of(double latitude, double longitude) { + return new ChecklistStationRequest(latitude, longitude); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/station/dto/response/SubwayStationResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/station/dto/response/SubwayStationResponse.java new file mode 100644 index 000000000..8227cd739 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/station/dto/response/SubwayStationResponse.java @@ -0,0 +1,43 @@ +package com.bang_ggood.station.dto.response; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import com.bang_ggood.station.domain.ChecklistStation; +import com.bang_ggood.station.domain.SubwayStation; +import lombok.AllArgsConstructor; +import lombok.Getter; +import java.util.ArrayList; +import java.util.List; + +@AllArgsConstructor +@Getter +public class SubwayStationResponse { + + private final String stationName; + private final List stationLine; + private Integer walkingTime; + + public static SubwayStationResponse of(SubwayStation station, double latitude, double longitude) { + List stationLine = new ArrayList<>(); + stationLine.add(station.getLine()); + return new SubwayStationResponse(station.getName(), stationLine, + station.calculateWalkingTime(latitude, longitude)); + } + + public static SubwayStationResponse from(ChecklistStation checklistStation) { + List stationLine = new ArrayList<>(); + stationLine.add(checklistStation.getStationLine()); + return new SubwayStationResponse(checklistStation.getStationName(), stationLine, + checklistStation.getWalkingTime()); + } + + public SubwayStationResponse merge(SubwayStationResponse response) { + if (!stationName.equals(response.stationName)) { + throw new BangggoodException(ExceptionCode.STATION_NAME_NOT_SAME); + } + + stationLine.addAll(response.stationLine); + walkingTime = Math.min(walkingTime, response.walkingTime); + return this; + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/station/dto/response/SubwayStationResponses.java b/backend/bang-ggood/src/main/java/com/bang_ggood/station/dto/response/SubwayStationResponses.java new file mode 100644 index 000000000..14c873808 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/station/dto/response/SubwayStationResponses.java @@ -0,0 +1,44 @@ +package com.bang_ggood.station.dto.response; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class SubwayStationResponses { + + private static final int REQUESTED_STATION_NUMBER = 2; + + private final List stations; + + public static SubwayStationResponses from(List stations) { + return new SubwayStationResponses(mergeTransferStations(stations)); + } + + private static List mergeTransferStations(List stations) { + return stations.stream() + .collect(Collectors.groupingBy( + SubwayStationResponse::getStationName, + Collectors.reducing(SubwayStationResponse::merge) + )) + .values() + .stream() + .map(optional -> optional.orElseThrow(() -> new BangggoodException(ExceptionCode.STATION_NOT_FOUND))) + .sorted(Comparator.comparing(SubwayStationResponse::getWalkingTime)) + .limit(REQUESTED_STATION_NUMBER) + .toList(); + } + + public SubwayStationResponse getNearestStation() { + if (stations.isEmpty()) { + return null; + } + return stations.get(0); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/station/repository/ChecklistStationRepository.java b/backend/bang-ggood/src/main/java/com/bang_ggood/station/repository/ChecklistStationRepository.java new file mode 100644 index 000000000..90857764f --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/station/repository/ChecklistStationRepository.java @@ -0,0 +1,25 @@ +package com.bang_ggood.station.repository; + +import com.bang_ggood.checklist.domain.Checklist; +import com.bang_ggood.station.domain.ChecklistStation; +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; + +public interface ChecklistStationRepository extends JpaRepository { + + @Query("SELECT cs FROM ChecklistStation cs " + + "where cs.checklist = :checklist " + + "and cs.deleted = false") + List findByChecklist(@Param("checklist") Checklist checklist); + + @Transactional + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("UPDATE ChecklistStation cs " + + "SET cs.deleted = true " + + "WHERE cs.checklist.id = :checklistId") + void deleteAllByChecklistId(@Param("checklistId") Long checklistId); +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/station/service/ChecklistStationService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/station/service/ChecklistStationService.java new file mode 100644 index 000000000..8a510cf84 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/station/service/ChecklistStationService.java @@ -0,0 +1,50 @@ +package com.bang_ggood.station.service; + +import com.bang_ggood.checklist.domain.Checklist; +import com.bang_ggood.station.domain.ChecklistStation; +import com.bang_ggood.station.dto.response.SubwayStationResponse; +import com.bang_ggood.station.repository.ChecklistStationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +@Service +public class ChecklistStationService { + + private final ChecklistStationRepository checklistStationRepository; + private final SubwayStationService subwayStationService; + + @Transactional + public void createChecklistStations(Checklist checklist, double latitude, double longitude) { + saveChecklistStations(checklist, latitude, longitude); + } + + @Transactional(readOnly = true) + public List readChecklistStationsByChecklist(Checklist checklist) { + return checklistStationRepository.findByChecklist(checklist); + } + + @Transactional + public void updateChecklistStation(Checklist checklist, double latitude, double longitude) { + checklistStationRepository.deleteAllByChecklistId(checklist.getId()); + saveChecklistStations(checklist, latitude, longitude); + } + + private void saveChecklistStations(Checklist checklist, double latitude, double longitude) { + List responses = subwayStationService.readNearestStation(latitude, longitude) + .getStations(); + List checklistStations = new ArrayList<>(); + + for (SubwayStationResponse response : responses) { + for (String stationLine : response.getStationLine()) { + checklistStations.add(new ChecklistStation(checklist, response.getStationName(), stationLine, + response.getWalkingTime())); + } + } + + checklistStationRepository.saveAll(checklistStations); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/station/service/SubwayStationService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/station/service/SubwayStationService.java new file mode 100644 index 000000000..6114a874e --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/station/service/SubwayStationService.java @@ -0,0 +1,22 @@ +package com.bang_ggood.station.service; + +import com.bang_ggood.station.SubwayReader; +import com.bang_ggood.station.domain.SubwayStation; +import com.bang_ggood.station.dto.response.SubwayStationResponse; +import com.bang_ggood.station.dto.response.SubwayStationResponses; +import org.springframework.stereotype.Service; +import java.util.List; + +@Service +public class SubwayStationService { + + private static final List SUBWAY_STATIONS = SubwayReader.readSubwayStationData(); + + public SubwayStationResponses readNearestStation(double latitude, double longitude) { + List stationResponses = SUBWAY_STATIONS.stream() + .map(station -> SubwayStationResponse.of(station, latitude, longitude)) + .toList(); + + return SubwayStationResponses.from(stationResponses); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/user/controller/UserController.java b/backend/bang-ggood/src/main/java/com/bang_ggood/user/controller/UserController.java new file mode 100644 index 000000000..3c9fc3650 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/user/controller/UserController.java @@ -0,0 +1,17 @@ +package com.bang_ggood.user.controller; + +import com.bang_ggood.auth.config.AuthRequiredPrincipal; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.dto.UserResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class UserController { + + @GetMapping("/user/me") + public ResponseEntity readUserInfo(@AuthRequiredPrincipal User user) { + return ResponseEntity.ok(UserResponse.from(user)); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/Email.java b/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/Email.java new file mode 100644 index 000000000..533d3abe6 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/Email.java @@ -0,0 +1,55 @@ +package com.bang_ggood.user.domain; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.Getter; +import lombok.NoArgsConstructor; +import java.util.Objects; +import java.util.regex.Pattern; + +import static lombok.AccessLevel.PROTECTED; + +@Embeddable +@Getter +@NoArgsConstructor(access = PROTECTED) +public class Email { + + //이메일은 영문 대소문자, 숫자, 점, 하이픈, 언더스코어, 플러스 기호를 포함할 수 있으며, + // "@" 기호 뒤에 도메인 이름이 필요하고, + // 마지막에는 최소 2글자의 영문자로 이루어진 최상위 도메인이 포함되어야 한다.. + private static final Pattern EMAIL_PATTERN = + Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"); + + @Column(name = "email") + private String value; + + public Email(String value) { + validateEmailPattern(value); + this.value = value; + } + + public void validateEmailPattern(String email) { + if (!EMAIL_PATTERN.matcher(email).matches()) { + throw new BangggoodException(ExceptionCode.EMAIL_INVALID_FORMAT); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Email email = (Email) o; + return Objects.equals(value, email.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/LoginType.java b/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/LoginType.java new file mode 100644 index 000000000..b076197b2 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/LoginType.java @@ -0,0 +1,6 @@ +package com.bang_ggood.user.domain; + +public enum LoginType { + + LOCAL, KAKAO +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/Password.java b/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/Password.java new file mode 100644 index 000000000..32a94986c --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/Password.java @@ -0,0 +1,60 @@ +package com.bang_ggood.user.domain; + +import com.bang_ggood.auth.service.PasswordEncoder; +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.Getter; +import lombok.NoArgsConstructor; +import java.util.Objects; +import java.util.regex.Pattern; + +import static lombok.AccessLevel.PROTECTED; + +@Getter +@Embeddable +@NoArgsConstructor(access = PROTECTED) +public class Password { + + //비밀번호는 최소 6자 이상이어야 하며, 영어 문자와 숫자를 각각 1개 이상 포함해야 한다. + private static final Pattern PASSWORD_PATTERN = + Pattern.compile("^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d~`!@#$%^&*()_\\-+=]{6,}$"); + + + @Column(name = "password") + private String value; + + public Password(String value) { + validatePasswordPattern(value); + this.value = PasswordEncoder.encodeWithGeneralSalt(value); + } + + public boolean isDifferent(String password) { + String targetPassword = PasswordEncoder.encodeWithSpecificSalt(password, value); + return !value.equals(targetPassword); + } + + private void validatePasswordPattern(String password) { + if (password == null || !PASSWORD_PATTERN.matcher(password).matches()) { + throw new BangggoodException(ExceptionCode.PASSWORD_INVALID_FORMAT); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Password targetPassword = (Password) o; + return Objects.equals(value, targetPassword.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/User.java b/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/User.java new file mode 100644 index 000000000..164733012 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/User.java @@ -0,0 +1,87 @@ +package com.bang_ggood.user.domain; + +import com.bang_ggood.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import java.util.Objects; + +import static lombok.AccessLevel.PROTECTED; + +@Table(name = "users") +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +public class User extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + @Column(nullable = false) + @Embedded + private Email email; + + @Embedded + private Password password; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private UserType userType; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private LoginType loginType; + + public User(String name, String email, UserType userType, LoginType loginType) { + this.name = name; + this.email = new Email(email); + this.userType = userType; + this.loginType = loginType; + } + + public User(String name, String email, String password, UserType userType, LoginType loginType) { + this.name = name; + this.email = new Email(email); + this.password = new Password(password); + this.userType = userType; + this.loginType = loginType; + } + + public User(Long id, String name, String email) { // TODO 테스트용 + this.id = id; + this.name = name; + this.email = new Email(email); + } + + public boolean isDifferent(String targetPassword) { + return password.isDifferent(targetPassword); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + User user = (User) o; + return Objects.equals(id, user.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/UserType.java b/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/UserType.java new file mode 100644 index 000000000..519caea39 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/user/domain/UserType.java @@ -0,0 +1,6 @@ +package com.bang_ggood.user.domain; + +public enum UserType { + + ADMIN, USER, GUEST; +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/user/dto/UserResponse.java b/backend/bang-ggood/src/main/java/com/bang_ggood/user/dto/UserResponse.java new file mode 100644 index 000000000..771112dff --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/user/dto/UserResponse.java @@ -0,0 +1,12 @@ +package com.bang_ggood.user.dto; + +import com.bang_ggood.user.domain.User; +import java.time.LocalDateTime; + +public record UserResponse(Long userId, String userName, String userEmail, LocalDateTime createdAt) { + public static UserResponse from(User user) { + return new UserResponse( + user.getId(), user.getName(), user.getEmail().getValue(), user.getCreatedAt() + ); + } +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/user/repository/UserRepository.java b/backend/bang-ggood/src/main/java/com/bang_ggood/user/repository/UserRepository.java new file mode 100644 index 000000000..54ee8acf0 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/user/repository/UserRepository.java @@ -0,0 +1,44 @@ +package com.bang_ggood.user.repository; + +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 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 UserRepository extends JpaRepository { + + default User getUserById(Long id) { + return findById(id).orElseThrow(() -> new BangggoodException(ExceptionCode.USER_NOT_FOUND)); + } + + @Query("SELECT u FROM User u WHERE u.id = :id AND u.deleted = false") + Optional findById(@Param("id") Long id); + + @Query("SELECT u FROM User u WHERE u.userType = :userType and u.deleted = false ") + List findUserByUserType(@Param("userType") UserType userType); + + @Query("SELECT u FROM User u WHERE u.email = :email and u.loginType = :loginType and u.deleted = false") + Optional findByEmailAndLoginType(@Param("email") Email email, @Param("loginType") LoginType loginType); + + @Transactional + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("UPDATE User u SET u.deleted = true WHERE u.id = :id") + void deleteById(@Param("id") Long id); + + @Transactional + @Modifying(flushAutomatically = true, clearAutomatically = true) + @Query("UPDATE User u SET u.deleted = false WHERE u.email = :email AND u.loginType = :loginType") + void resaveByEmailAndLoginType(@Param("email") Email email, @Param("loginType") LoginType loginType); + + @Query("SELECT u FROM User u WHERE u.email = :email and u.loginType = :loginType") + Optional findByEmailAndLoginTypeWithDeleted(@Param("email") Email email, @Param("loginType")LoginType loginType); +} diff --git a/backend/bang-ggood/src/main/java/com/bang_ggood/user/service/UserService.java b/backend/bang-ggood/src/main/java/com/bang_ggood/user/service/UserService.java new file mode 100644 index 000000000..63b03b2f9 --- /dev/null +++ b/backend/bang-ggood/src/main/java/com/bang_ggood/user/service/UserService.java @@ -0,0 +1,26 @@ +package com.bang_ggood.user.service; + +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.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.List; + +@RequiredArgsConstructor +@Service +public class UserService { + + private final UserRepository userRepository; + + @Transactional + public void createUser(User user) { + userRepository.save(user); + } + + @Transactional(readOnly = true) + public List readUser(UserType userType) { + return userRepository.findUserByUserType(userType); + } +} diff --git a/backend/bang-ggood/src/main/resources/.DS_Store b/backend/bang-ggood/src/main/resources/.DS_Store new file mode 100644 index 000000000..0e8448f03 Binary files /dev/null and b/backend/bang-ggood/src/main/resources/.DS_Store differ diff --git a/backend/bang-ggood/src/main/resources/data.sql b/backend/bang-ggood/src/main/resources/data.sql new file mode 100644 index 000000000..e49ba0d45 --- /dev/null +++ b/backend/bang-ggood/src/main/resources/data.sql @@ -0,0 +1,137 @@ +-- 비밀번호 : password1234 +INSERT INTO users(name, email, password, user_type, login_type, created_at, modified_at, deleted) +VALUES ('방방이', 'bang-ggood@gmail.com', + 'xDNYKEJqE/36U0Dt3nXRMFPNEMEgjCYM7R/A4B29baOsv4KYQ9MGgcO3HUa11sNKCFb9ZXyYBqJqxNglvBzFvg==:7yejAszEpxBb7AyZNKvAqpmMEJiKFXIa8JKwAx3n4loB2DRcAC2pfwkgo/dzKzRvBX4RbrATWaIlPYrgAhbHZQ==', + 'USER', 'LOCAL', '2024-07-22 07:56:42', '2024-07-22 07:56:42', false); + +INSERT INTO category (name) +VALUES + ('방 컨디션'), + ('창문'), + ('화장실'), + ('보안'), + ('외부'); + +INSERT INTO question (category_id, title, subtitle, is_default) +VALUES + (1, '곰팡이가 핀 곳 없이 깨끗한가요?', '천장, 벽면, 가구 뒤, 장판을 확인하세요.', true), + (1, '불쾌한 냄새 없이 쾌적한가요?', null, true), + (1, '벌레가 나온 흔적 없이 깔끔한가요?', '벌레 퇴치약이 부착되어 있는지 확인하세요.', true), + (1, '물건을 충분히 수납할 수 있는 공간이 있나요?', null, true), + (1, '방 인테리어는 괜찮나요?', null, true), + (1, '에어컨의 상태는 괜찮은가요?', '에어컨을 틀어서 불쾌한 냄새가 나진 않는지 확인하세요.', false), + (1, '보일러가 잘 동작하나요?', null, false), + (1, '콘센트 위치와 개수가 적절한가요?', null, false), + (1, '벽지 상태가 양호한가요?', null, false), + (2, '창 밖의 뷰가 가로막힘 없이 트여있나요?', null, true), + (2, '창문 상태가 괜찮나요?', null, true), + (2, '환기가 잘 되는 구조인가요?', '창문 크기와 방향을 확인하세요.', true), + (2, '햇빛이 잘 들어오나요?', null, true), + (2, '창문이 이중창인가요?', null, false), + (2, '창문 밖에 쓰레기통 등 냄새가 나는 요소가 있나요?', null, false), + (3, '화장실이 깨끗한가요?', '청소 가능한 얼룩인지 확인하세요.', true), + (3, '수압 및 물 빠짐이 괜찮은가요?', '화장실에서 수도와 변기를 동시에 사용해보세요.', true), + (3, '환기 시설이 있나요?', null, true), + (3, '내부에 창문이 있나요?', null, false), + (3, '온수가 잘 나오나요?', null, false), + (4, '잠금장치가 있는 공동 현관문이 있나요?', null, true), + (4, '출입구와 복도에 CCTV가 설치되어 있나요?', null, true), + (4, '관리자분이 함께 상주하시나요?', '관리자분이 24시간 상주하시는지 확인하세요.', true), + (4, '보안 시설이 잘 갖추어져 있나요?', '도어락, 창문 잠금장치 등이 있는지 확인하세요.', false), + (4, '화면이 달린 인터폰이 제공되나요?', null, false), + (4, '현관문에 걸쇠가 있나요?', null, false), + (5, '주변 도로가 밤에도 충분히 밝은가요?', null, false), + (5, '주변에 소음 시설이 있는지 확인했나요?', '유흥시설, 놀이터, 공사장이 있는지 확인하세요.', false), + (5, '1층에 음식점이 있는지 확인했나요?', null, false), + (5, '집 가는 길이 언덕 없이 완만한가요?', null, false), + (5, '옆 건물에서 보이는 구조인지 확인했나요?', null, false), + (5, '주차할 수 있는 시설이 있나요?', null, false) +; +INSERT INTO highlight (question_id, name) +VALUES + (1, '곰팡이'), + (2, '불쾌한 냄새'), + (3, '벌레'), + (4, '수납할 수 있는 공간'), + (5, '방 인테리어'), + (6, '에어컨'), + (7, '보일러'), + (8, '콘센트'), + (9, '벽지 상태'), + (10, '창 밖의 뷰'), + (11, '창문 상태'), + (12, '환기'), + (13, '햇빛'), + (14, '이중창'), + (15, '냄새가 나는 요소'), + (16, '깨끗'), + (17, '수압 및 물 빠짐'), + (18, '환기 시설'), + (19, '창문'), + (20, '온수'), + (21, '잠금장치'), + (21, '공동 현관문'), + (22, 'CCTV'), + (23, '관리자분'), + (24, '보안 시설'), + (25, '인터폰'), + (26, '걸쇠'), + (27, '주변 도로'), + (27, '밝은가요'), + (28, '소음 시설'), + (29, '음식점'), + (30, '언덕'), + (31, '보이는 구조'), + (32, '주차할 수 있는 시설') +; + +INSERT INTO custom_checklist_question(user_id, question, question_id, created_at, modified_at, deleted) +VALUES (1, 'ROOM_CONDITION_1',1, '2024-07-22 07:56:42', '2024-07-22 07:56:42', false), + (1, 'ROOM_CONDITION_2',2, '2024-07-22 07:56:43', '2024-07-22 07:56:43', false), + (1, 'ROOM_CONDITION_3',3, '2024-07-22 07:56:44', '2024-07-22 07:56:44', false), + (1, 'ROOM_CONDITION_4',4, '2024-07-22 07:56:45', '2024-07-22 07:56:45', false), + (1, 'ROOM_CONDITION_5',5, '2024-07-22 07:56:46', '2024-07-22 07:56:46', false), + (1, 'WINDOW_1',6, '2024-07-22 07:56:47', '2024-07-22 07:56:47', false), + (1, 'WINDOW_2',7, '2024-07-22 07:56:48', '2024-07-22 07:56:48', false), + (1, 'WINDOW_3',8, '2024-07-22 07:56:49', '2024-07-22 07:56:49', false), + (1, 'WINDOW_4',9, '2024-07-22 07:56:50', '2024-07-22 07:56:50', false), + (1, 'BATHROOM_1',10, '2024-07-22 07:56:51', '2024-07-22 07:56:51', false), + (1, 'BATHROOM_2',11, '2024-07-22 07:56:52', '2024-07-22 07:56:52', false), + (1, 'BATHROOM_3',12, '2024-07-22 07:56:53', '2024-07-22 07:56:53', false), + (1, 'SECURITY_1',13, '2024-07-22 07:56:54', '2024-07-22 07:56:54', false), + (1, 'SECURITY_2',14, '2024-07-22 07:56:55', '2024-07-22 07:56:55', false), + (1, 'SECURITY_3',15, '2024-07-22 07:56:56', '2024-07-22 07:56:56', false); + +INSERT INTO article(title, content, keyword, summary, thumbnail, created_at, modified_at, deleted) +VALUES ('자취방 이사만 5번 피셜! 원룸 구할 때 체크리스트 1탄', + '학교 주변 자취방을 구하는 **대학생 분들,** 회사 주변 방을 구하는 **직장인 분들!** 그리고 그외 전국의 예비 자취생 여러분 모두 잘 오셨습니다.

원래 **자취방 구하기는 대학생 종강 시즌이 제철**이신 거, 다들 아시죠? 👀 그래서 오늘은 자취방 이사만 5번 해본 **자취방 구하기 만렙 에디터**가 찐 노하우가 담겨 있는 **원룸 구할 때 확인해야 할 체크리스트**를 가져왔습니다.

한 번도 생각해 보지 못했지만 **보자마자 납득 100%인 꿀팁들**을 고르고 골라 왔으니, 모두 집중하고 따라오시지요!



🪟 창문

**✅ 옆 건물에서 잘 보이는 구조인지**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/projects/160436720332165459.jpg)
많은 분들이 잘 생각하지 못하시는 부분 중 하나죠! **옆 건물과 사생활 보호가 되는지 여부**입니다. 물론 블라인드를 쳐서 가릴 수는 있지만, **매번 블라인드를 쳐둬야 하는 건** 은근 불편한 일이거든요! (=자취방 3호 경험담)

그리고 자취의 특권은 **샤워 후 자연의 상태로 나와서** 옷을 갈아입을 수 있다는 것 아니겠습니까? 🛀 그러니 자취의 장점을 누리기 위해 꼭 한번 체크해 보시는 게 좋겠습니다.

**✅ 환기하기에 적합한 크기인지**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/projects/159505656294119930.jpg)
자취방의 경우 그 특성상 창문이 하나만 있는 경우가 많은데요. 하지만 창문 전체가 시원하게 열리는 형태가 아니라, 위 사진처럼 **틈새만 열리는 경우라면 피하시는 게 좋습니다.**

좁은 평수일수록 환기가 매우 중요한데, 요런 창문의 경우에는 **환기 능력**이 떨어질 수밖에 없기 때문이죠! 결정 요인은 아니지만 **의외로 삶의 질과 연결**되는 부분이기 때문에 체크해 보시길 권장합니다.

**✅ 방충망/방범창 이상 없는지**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/projects/164397041095518794.jpg)
창문 확인의 기본 of 기본! 바로 **방충망과 방범창 여부**입니다.

방충망은 구멍이 뚫려 **보수할 부분이 있는지**까지 체크해 주시는 게 좋고, 방범창은 **저층일수록 꼭** **확인**해 주시는 것이 좋습니다.

**✅ 햇빛 잘 들어오는지**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/snapshots/166038643972068590.jpeg)
물론 집을 보러 가기 전에 남향인지 북향인지 중개인 분께서 설명을 해주실 텐데요. 하지만 남향이어도 앞 건물에 가려져 빛이 잘 안 들어올 수 있으니, 꼭 확인하기로 합시다.

**🍯 여기서 작은 꿀팁!**
Q. 앞 건물에 막힌 남향과 일반 북향/서향이 고민이라면?
A. 그래도 남향을 추천해요! 빛이 들어오지 않는 남향이라도 북향이나 서향보다는 훨씬 쾌적하답니다.

🔐 보안

**✅ 관리자 분 상주하시는지**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/projects/167971937444371683.jpg)자취방 특성상 상시로 관리자 분이 계시는 경우가 많지는 않죠! 하지만 오피스텔의 경우에는 종종 관**리자분이 상주하고 계시는 경우**가 있는데요 👀

자취를 하다 보면 갑자기 불이 나가는 등 **전문가의 도움이 필요한 순간**들이 찾아오기 마련이죠. (그것의 자취의 맛..) 따라서 상주 관리자분이 계신 집이라면 **+15점** 해주시는 것이 좋겠습니다.

**✅ 현관문 잠금장치 있는지**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/snapshots/167894935043362031.jpeg)
요것도 쉽게 지나칠 수 있는 포인트! 하지만 잠금장치 여부는 꼭 확인해 주시는 게 좋습니다. 자취를 하다 보면 정말 가끔 **모르는 사람이 초인종을 누르는 경우**도 있거든요 🤔

**🙋‍♀️ 추가로 이것도 확인해 보세요!**
 ✅ 출입구와 복도에 CCTV 있는지
 ✅ 공동 현관 비밀번호 있는지

🏡 주변 환경

**✅ 무인 택배 보관함 있는지**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/snapshots/166618977771822482.jpeg)
처음엔 굳이 필요한가 싶지만, **여기저기 활용도 좋은 무인 택배함!** 상세 주소 노출 없이 택배를 보관하기 좋은 것은 물론, 물건을 집에 두고 와야 하는데 **올라가기 귀찮을 때나 중고거래할 때** 등 유용하게 사용할 수 있어요.

만약 무인 택배함이 있는 집이라면 **보너스 점수**를 주기로 합시다 🕵️‍♀️

**✅ 대중교통 이용 편리한지**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/168740647444591385.jpeg)
자취방을 구하는 목적은 바로 **학교나 직장 등에 편리하게 가기 위함**이 1번 아니겠습니까? 따라서 대중교통이 이용이 불편한 위치에 집이 있다면 **삶이 질이 수직 하락**할 수밖에 없죠!

지도상 시간도 체크하고, 실제로 자취방에서 대중교통 타는 곳까지 가보며 **경사나 주변 환경**도 체크해 보세요 🏃

**✅ 주변에 소음 시설 있는지**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/snapshots/164194588552065716.jpeg)
아무래도 자취방 중에는 소음에 약한 곳이 많은데요! 따라서 **주변에 소음을 유발하는 시설이 있는지** 꼭 확인하셔야 합니다. 대표적으로는 **큰 도로와 술집** 정도가 있어요.

거기에 더해 꼭 확인하셔야 할 것은 바로 **24시 해장국집**입니다. 대개 **새벽까지 시끌시끌한 경우**가 많으므로 꼭 한번 확인해 보시길 바랍니다. (저도 알고 싶지 않았습니다)

**🙋‍♀️ 추가로 이것도 확인해 보세요!**
 ✅ 편의점, 은행 등 편의시설 있는지
 ✅ 집 가는 길이 언덕인지
 ✅ 골목이라면 가로등 있는지

🛌 기본 옵션

**✅ 옵션 가구 치워줄 수 있는지 확인하기**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/snapshots/165794935274391835.jpeg)
멀쩡한 옵션 가구를 왜 치우느냐! 하실 수 있지만, 자취를 하다 보면 의외로 **옵션 가구가 짐이 되는 경우**들이 많더라고요 🤔

에디터 또한 어느 날부터 옵션 책상이 불필요하고 너무 거슬려서 결국 집주인 분께 양해를 구하고 수거해 주시길 부탁드린 경우가 있답니다. 경우에 따라 집 주인분께서 거절하실 수도 있을 수 있으니 **꼭 사전에 확인**해 보세요!

**✅ 화구 종류 체크하기**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/projects/167471560364252808.jpg) 집에서 요리를 종종 해 드시는 분이라면 꼭 **화구 종류를 확인**해 주세요! 요리를 즐겨 하시는 분일수록 가스레인지가 더 편리해요.

특히 하이라이트로 되어 있는 경우에는 **화력이 매우 약한 곳들**이 꽤나 있으니 ^^; 꼭 사전에 확인하시길 권합니다.

✅ **에어컨/냉장고 작동 점검하기**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/snapshots/168620136263807016.jpeg)
옵션 가구 중 **가장 고가인 에어컨과 냉장고**는 사전에 점검을 해두면 좋아요!

만약 집 구하는 날 체크하지 못했다면, 혹시라도 문제가 생길 경우를 대비하여 **이사 후 일주일 내로 확인**해 보시길 바랍니다.

**🙋‍♀️ 추가로 이것도 확인해 보세요!**
  ✅ 옵션 가구 필요 없다면 치워줄 수 있는지 확인하기
  ✅ 옵션 가구 종류 확인하기 (신발장, 블라인드 등)

지면 관계상 1탄은 여기까지~
2탄에서 이어서 만나요💟

출처 : 오늘의집', + '자취방 꿀팁', + '원룸 체크리스트', + 'https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/projects/160436720332165459.jpg', + '2024-07-22 07:56:42', + '2024-07-22 07:56:42', + false), + ('자취방 이사만 5번 피셜! 원룸 구할 때 체크리스트 2탄', + '안녕하세요~ **자취방 구하기 만렙 에디터**가 찐 노하우가 담겨 있는 **원룸 구할 때 확인해야 할 체크리스트 2탄**으로 돌아왔습니다!

1탄을 아직 못보신 분들은 먼저 확인하고 돌아와주세요. 다들 집중하시고 가봅시다!!

🕵️ 디테일

**✅ 인터폰 영상 지원되는지**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/snapshots/168217562696278839.jpeg)
이것도 앞서 이야기해 드린 **현관문 잠금장치와 비슷한 맥락**인데요! **배달 음식이나 등기 수령** 같은 경우에도 음성 인터폰만 듣는 것보다는 영상을 통해 방문자를 확인하는 것이 안전하죠.

잘 모르는 상대가 찾아왔을 때, **상대방의 모습을 확인**할 수 있다는 것만으로도 **마음의 안정에 큰 도움**이 되니 확인해 보시는 걸 추천드립니다. (방문 기록을 증거로 남길 수 있는 건 덤!)


**✅ 바퀴벌레 약 설치되어 있는지**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/snapshots/159404181119534587.jpeg)
만약 집을 보러 갔는데 가구 뒤, 신발장 옆, 화장실 변기 뒤쪽 등등 **바퀴벌레 약을 설치한 흔적**이 있다? **웬만하면 런하시기를 추천합니다** 🏃‍♂️

물론 현재 거주자분이 꼼꼼한 성격으로 사전 예방하신 경우일 수도 있지만, 대부분 높은 확률로 한차례 발견했기 때문에 설치하는 경우가 많기 때문이죠...😱 가능한 도망치시기를!


**✅ 벽지에 곰팡이 흔적 있는지**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/snapshots/168645987277346900.jpeg)
곰팡이가 잘 생기는 **창문 주변 벽지와 침대 뒤쪽** 등, **시커먼 곰팡이 흔적**이 있는지 확인해 주세요.

곰팡이 흔적이 심하게 남아 있다면 높은 확률로 **습도 관리가 어려운 집**이거나 **집 구조적으로 곰팡이가 생길 수밖에 없는 곳**이랍니다. 따라서 이 경우에도 빠르게 런하시길! 🏃‍♂️

**🙋‍♀️ 추가로 이것도 확인해 보세요!**
  ✅ 콘센트 개수는 충분한지
  ✅ 옆집 방음 잘 되는지


✍🏻 기타 사항

**✅ 건물에 집주인분 사시는지 확인하기**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/projects/156680003862457833.jpg)
사소하지만 은근 중요한 디테일! 바로 **집주인 분과 같은 건물에서 살게 되는지**를 체크해 주시면 좋습니다.

대학가 인근 빌라의 경우 집주인 분도 같은 건물에 거주하시는 경우가 종종 있는데요. **이게 은근 불편하고 머쓱하다는 사실..!** 물론 개인차는 있겠지만 본인이 에디터의 경우에 해당된다면? 은밀하게 체크해 주세요.


**✅ 분리수거 시스템 확인하기**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/projects/165413921573933477.jpeg)
**분리 수거 시스템이 체계적으로 운영**되고 있는지, **쓰레기 버리기에는 수월한지**도 확인해 주시면 좋습니다.

자취방 중에 간혹 굉장히 불편하고 비효율적인 분리수거 시스템을 가진 곳들이 있거든요! **배출 일자나 주기적으로 쓰레기 관리해 주시는 분이 계시는지 여부** 등등 면밀하게 체크하시는 게 좋겠습니다.

**🙋‍♀️ 추가로 이것도 확인해 보세요!**
  ✅ 관리비 포함 항목 확인하기
  ✅ 인테리어 가능 여부 체크하기

🚽 화장실

**✅ 배수구 냄새 올라오는지**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/projects/163777210096433394.jpg)
**화장실 숙적!** 바로 **배수구 냄새**죠.특히 더워지는 여름철엔 모두가 고민하는 부분이긴 하지만, **유달리 배수구 냄새가 지독하게 나는 집**이 있다는 사실!

이 부분은 정말 삶의 질과 직결되기 때문에, **화장실에 들어가 문을 닫고** 꼭 한번 확인해 주세요 😉


**✅ 화장실 내부에 창문 있는지**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/projects/161331910819056750.jpg)
흔한 조건은 아니지만 **있다면 무조건 플러스 오십 점인 화장실 창문!**

창문이 없다면 환풍기는 잘 되는지, 방문 당시 화장실 습도는 어느 정도인지 확인해 주시는 편이 좋습니다 🌬️

**🙋‍♀️ 추가로 이것도 확인해 보세요!**
  ✅ 샤워 여유 공간 충분한지
  ✅ 곰팡이 흔적 있는지


🚿 수도와 배수

**✅ 싱크대/화장실 배수구 잘 내려가는지**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/snapshots/167694880830634529.jpeg)
곳곳의 수전에서 물이 잘 나오는지를 확인하는 것과 더불어, 꼭 챙겨야 할 것은 **배수구로 물이 내려가는 속도**입니다. **특히 세면대의 경우**가 각종 이물질이 축적되어 물이 매우 늦게 내려가는 경우가 있어요! (= 2호 자취방)

빠르게 설거지나 빨래를 해야 하는데 물이 잘 안 내려가면 정말 성격 안 좋아지기 때문에 🤯 화장실과 싱크대 모두 꼼꼼히 체크하시길 권합니다.


**✅ 변기 물 잘 내려가는지**
![img](https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/projects/166970947851460229.jpg)
여기서 포인트는 **시원하고 우렁차게!** 입니다. 종종 변기 내려가는 힘이 매우 약한 곳들이 있는데요.

이 경우 삶의 질이 급격히 저하되므로, **변기 내릴 때 소리와 시원함**을 꼭! 체크하시길 바랍니다.

**🙋‍♀️ 추가로 이것도 확인해 보세요!**
 ✅ 싱크대/세면대/샤워기 물 잘 나오는지
 ✅ 싱크대/화장실 온수 잘 나오는지


오늘은 자취 이사만 5번 경력을 살려 고인물이 아니면 알 수 없는 원룸 구할 때 체크할 것들에 대해 소개해 드렸는데요!
자취방 구할 때 꼭 요긴하게 사용하시길 바랍니다 💟

출처 : 오늘의집', + '자취방 꿀팁', + '원룸 체크리스트', + 'https://image.ohou.se/i/bucketplace-v2-development/uploads/cards/snapshots/168217562696278839.jpeg', + '2024-07-22 07:56:42', + '2024-07-22 07:56:42', + false), + ('초보 세입자들에게 전하는 부동산 계약 팁', + '마음에 쏙 드는 방을 찾았다면 이젠 집을 계약할 단계겠죠? 이번 포스트에서는 초보 세입자분들을 위해서 방을 계약할 때 참고해야 할 사항들을 말씀드리도록 하겠습니다.

등기부등본, 건축물대장 열람은 필수!

등기부등본과 건축물대장은 해당 부동산, 건물에 대해서 소유권자에 대한 사항과 위치·면적·구조·용도·층수 등에 대한 사항을 알 수 있는 문서인데요. 등기부등본과 건축물대장을 통해서 **계약하려는 방, 건물의 소유주가 실제 계약을 진행하는 임대인과 동일한지, 건물 또한 등록된 내용과 다른 부분이 없는지** 확인해야 합니다.

특히, 등기부등본에는 부동산에 대해 설정된 융자, 경매, 저당권에 대해서도 확인할 수 있으니 반드시 확인하고 계약을 진행해야 합니다. 혹시나 등기부등본과 건축물대장에 표시된 내용이 다를 수 있는데 권리관계는 등기부등본을 기준으로 하며 부동산의 표시는 건축물대장을 기준으로 하게 되기 때문에 이를 참고하시고, 혹시 표시가 달라 걱정이 된다면 인터넷 등기소를 통해 수정을 하시면 됩니다.

계약은 집주인과 하는 것이다!

계약은 집주인이 임대인이고 방을 구하는 여러분이 임차인이 되어 진행되는 것입니다. **계약의 당사자는 반드시 본인과 집주인이 되어야 한다는 것**인데요. 보통은 방을 구하는 당사자와 집주인, 중개인 이렇게 계약을 진행하겠지만 가끔 특별한 사유로 집주인(임대인)이 직접 자리하여 계약을 진행하지 못하는 경우가 발생할 수 있습니다. 이때, 임대인은 대리인에게 본인의 직인과 대리인 임명장을 통해서 실제로 권한을 부여한 대리인임을 증명할 수 있고 대리인은 부여된 권한 안에서 대리계약을 진행할 수 있습니다.

주의할 것은, 대리인은 계약에서 집주인으로부터 권한을 부여받은 대리인일 뿐, 계약 당사자가 아니기 때문에 계약서상의 성명 직인은 모두 집주인인 임대인의 것이 들어가야 하며 계약금도 집주인이 별도로 말한 내용이 없다면 집주인 명의의 계좌로 입금해야 합니다.

협의된 내용은 특약사항에 명시할 것!

집을 계약할 때 집주인과 얘기하여 협의한 내용들(옵션사항, 월세 및 관리비 관련 사항, 기타 생활규칙, 내부 개조 등)은 계약서상에 명시가 안된 경우가 많기 때문에 이러한 내용들은 특약사항으로 따로 적어주는 것이 좋습니다.

집주인과 얘기가 다 되었다고 생각하여 이를 계약서 상에 기재하지 않았을 경우 문제가 발생했을 때, 이전에 집주인과 협의된 사항임을 본인이 직접 밝혀야 하기 때문에 **임차인이 적극적으로 특약사항을 확인하고 내용을 명시**해야 합니다.

전입신고 및 확정일자를 받을 것!

전입신고와 확정일자는 임차인이 임대차계약에서 법적으로 권리를 보호받을 수 있는 절차입니다. 전입신고는 새로운 거주지에 전입하여 주소지 변경 사항을 알리는 절차이며 이를 통해 주택임대차보호법의 보호를 받을 수 있고, 확정일자는 임대차계약을 맺은 날짜를 확인하는 것으로 경매 등의 상황에서 우선적으로 보증금을 변제받을 수 있게 됩니다.

전입신고와 확정일자는 주민센터에서 바로 민원이 가능하며 전입신고와 동시에 확정일자를 받는 것이 가장 편리한 방법이며 **신고는 전입한 날로부터 14일 이내로 완료**하면 됩니다.

출처:https://brunch.co.kr/@dprnrn234/124', + '등기부등본', + '방 계약 시 주의할 점', + null, + '2024-07-22 07:56:42', + '2024-07-22 07:56:42', + false), + ('전세 사기 피하는 법…계약 전 꼭 확인해야 할 8가지', + '![img](https://mediahub.seoul.go.kr/uploads/mediahub/2023/05/whHWpgBBQsZMIKotGenBwFIKcPERqYKu.png)
![img](https://mediahub.seoul.go.kr/uploads/mediahub/2023/05/yyxhNCPoGKUFhtSmnxCNAxFHovdrREfW.png)

전세사기 예방, 꼭! 확인해야 할 8가지!


압류 및 세금체납 등 권리제한사항이 있거나 매매가격보다 과도하게 높은 전세가격 등으로 **보증금을 돌려받지 못하는 전세사기!**

깡통전세·전세사기 피해자가 더 늘어나지 않도록 하기 위해 예방 대책 및 지원 방안을 마련하고 총력을 기울이고 있는데요. **전세사기 예방을 위해 꼭 확인해야 할 8가지, 함께 살펴보아요!** ![img](https://mediahub.seoul.go.kr/uploads/mediahub/2023/05/ZrgaSqFihDwrSiXzxxUVzIgjcohGoGCe.png)
![img](https://mediahub.seoul.go.kr/uploads/mediahub/2023/05/GYuqUxJtzxeFpHrWtBObYmzYPIqySThn.png)
![img](https://mediahub.seoul.go.kr/uploads/mediahub/2023/05/TFWMcSjDHnkEDiBGBmMcRVxpRhDPRkbP.png)
![img](https://mediahub.seoul.go.kr/uploads/mediahub/2023/05/cYHtopKhdHyBhbFWpNodygWStpjCVmUZ.png)

출처:https://mediahub.seoul.go.kr/archives/2007948', + '전세', + '전세 사기 피하는 법', + 'https://mediahub.seoul.go.kr/uploads/mediahub/2023/05/whHWpgBBQsZMIKotGenBwFIKcPERqYKu.png', + '2024-07-22 07:56:42', + '2024-07-22 07:56:42', + false); diff --git a/backend/bang-ggood/src/main/resources/logback.xml b/backend/bang-ggood/src/main/resources/logback.xml new file mode 100644 index 000000000..d5379b463 --- /dev/null +++ b/backend/bang-ggood/src/main/resources/logback.xml @@ -0,0 +1,44 @@ + + + + + + + + INFO + ACCEPT + DENY + + + %d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level){BLUE} [%thread] %logger{36} - %msg%n + + + + + + WARN + ACCEPT + DENY + + + %d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level){YELLOW} [%thread] %logger{36} - %msg%n + + + + + + ERROR + ACCEPT + DENY + + + %d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level){RED} [%thread] %logger{36} - %msg%n + + + + + + + + + diff --git a/backend/bang-ggood/src/main/resources/schema.sql b/backend/bang-ggood/src/main/resources/schema.sql new file mode 100644 index 000000000..3820826ab --- /dev/null +++ b/backend/bang-ggood/src/main/resources/schema.sql @@ -0,0 +1,177 @@ +-- Drop tables if they exist +DROP TABLE IF EXISTS checklist_station CASCADE; +DROP TABLE IF EXISTS checklist_like CASCADE; +DROP TABLE IF EXISTS custom_checklist_question CASCADE; +DROP TABLE IF EXISTS checklist_option CASCADE; +DROP TABLE IF EXISTS checklist_question CASCADE; +DROP TABLE IF EXISTS checklist_maintenance CASCADE; +DROP TABLE IF EXISTS checklist CASCADE; +DROP TABLE IF EXISTS article CASCADE; +DROP TABLE IF EXISTS users CASCADE; +DROP TABLE IF EXISTS room CASCADE; +DROP TABLE IF EXISTS highlight CASCADE; +DROP TABLE IF EXISTS question CASCADE; +DROP TABLE IF EXISTS category CASCADE; + +-- Create tables + +CREATE TABLE category +( + id INTEGER AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) +); + +CREATE TABLE question +( + id INTEGER AUTO_INCREMENT PRIMARY KEY, + category_id INTEGER, + title VARCHAR(255), + subtitle VARCHAR(255), + is_default BOOLEAN, + FOREIGN KEY (category_id) REFERENCES category (id) +); + +CREATE TABLE highlight +( + id INTEGER AUTO_INCREMENT PRIMARY KEY, + question_id INTEGER, + name VARCHAR(255), + FOREIGN KEY (question_id) REFERENCES question (id) +); + +CREATE TABLE room +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255), + address VARCHAR(255), + building_name VARCHAR(255), + station VARCHAR(255), + walking_time INTEGER, + floor_level VARCHAR(255), + floor INTEGER, + structure VARCHAR(255), + size DOUBLE, + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN +); + +CREATE TABLE users +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255), + email VARCHAR(255) NOT NULL, + password VARCHAR(255), + user_type VARCHAR(255) NOT NULL, + login_type VARCHAR(255) NOT NULL, + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN, + CONSTRAINT unique_email_login_type UNIQUE (email, login_type) +); + +CREATE TABLE checklist +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + room_id BIGINT NOT NULL UNIQUE, + user_id BIGINT NOT NULL, + deposit INTEGER, + rent INTEGER, + maintenance_fee INTEGER, + contract_term INTEGER, + occupancy_month VARCHAR(255), + occupancy_period VARCHAR(255), + real_estate VARCHAR(255), + memo VARCHAR(1000), + summary VARCHAR(255), + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN, + FOREIGN KEY (room_id) REFERENCES room (id), + FOREIGN KEY (user_id) REFERENCES users (id) +); + +CREATE TABLE checklist_maintenance +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + checklist_id BIGINT, + maintenance_item VARCHAR(255), + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN, + FOREIGN KEY (checklist_id) REFERENCES checklist (id) +); + +CREATE TABLE checklist_question +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + question VARCHAR(255), + question_id INTEGER NOT NULL, + checklist_id BIGINT NOT NULL, + answer VARCHAR(255), + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN, + FOREIGN KEY (checklist_id) REFERENCES checklist (id), + FOREIGN KEY (question_id) REFERENCES question (id) +); +CREATE TABLE checklist_option +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + option_id INTEGER NOT NULL, + checklist_id BIGINT NOT NULL, + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN, + FOREIGN KEY (checklist_id) REFERENCES checklist (id) +); + +CREATE TABLE custom_checklist_question +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT, + question VARCHAR(255), + question_id INTEGER, + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN, + FOREIGN KEY (user_id) references users (id), + FOREIGN KEY (question_id) references question (id) +); + +CREATE TABLE checklist_like +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + checklist_id BIGINT, + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN, + FOREIGN KEY (checklist_id) REFERENCES checklist (id) +); + +CREATE TABLE article +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255), + content TEXT, + keyword VARCHAR(255), + summary VARCHAR(255), + thumbnail VARCHAR(2500), + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN +); + +CREATE TABLE checklist_station +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + checklist_id BIGINT, + station_name VARCHAR(255), + station_line VARCHAR(255), + walking_time INTEGER, + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN, + FOREIGN KEY (checklist_id) REFERENCES checklist (id) +); + diff --git a/backend/bang-ggood/src/main/resources/seoul_stations_240819.csv b/backend/bang-ggood/src/main/resources/seoul_stations_240819.csv new file mode 100644 index 000000000..f6a384cd8 --- /dev/null +++ b/backend/bang-ggood/src/main/resources/seoul_stations_240819.csv @@ -0,0 +1,768 @@ +_ID,,ȣ,,浵 +4703,4.19ֹ,̽ż,37.649502,127.013684 +1907,,1ȣ,37.748577,127.044213 +340,,3ȣ,37.492245,127.117757 +2818,,8ȣ,37.492888,127.118398 +2748,д,7ȣ,37.480338,126.882656 +1702,д,1ȣ,37.481581,126.882581 +4107,,9ȣ,37.561391,126.854456 +4704,,̽ż,37.641537,127.016789 +3216,,õ2ȣ,37.484192,126.683673 +3211,(Ƽ),õ2ȣ,37.524649,126.675539 +3212,߾ӽ,õ2ȣ,37.517054,126.676672 +1265,,߾Ӽ,37.568491,126.915487 +1851,õ,д缱,37.448605,127.126697 +1323,,ἱ,37.814536,127.510739 +1816,,1ȣ,37.464737,126.694181 +3123,Ÿ,õ1ȣ,37.467048,126.707938 +1312,,ἱ,37.634118,127.114757 +3117,,õ1ȣ,37.517268,126.721514 +222,,2ȣ,37.49799,127.027912 +4307,,źд缱,37.496837,127.028104 +2732,û,7ȣ,37.517179,127.041255 +1849,û,д缱,37.517469,127.041151 +4502,,μ,37.270161,127.126033 +2549,,5ȣ,37.535804,127.132481 +2813,û,8ȣ,37.530341,127.120508 +1269,,߾Ӽ,37.612314,126.843223 +214,(͹̳),2ȣ,37.535095,127.094681 +9995,,5ȣ,37.55749,127.17593 +1326,,ἱ,37.805723,127.634146 +2559,,5ȣ,37.498079,127.13482 +1801,,1ȣ,37.494594,126.85968 +1027,,д缱,37.489116,127.06614 +4101,ȭ,9ȣ,37.578608,126.798153 +2512,ȭ,5ȣ,37.572399,126.806171 +2560,ſ,5ȣ,37.493105,127.14415 +212,ǴԱ,2ȣ,37.540373,127.069191 +2729,ǴԱ,7ȣ,37.540786,127.071011 +4925,Ϻ,,37.63165,126.705975 +3203,˴ܻŸ,õ2ȣ,37.60185,126.657108 +3201,˴ܿ(˴ܻ),õ2ȣ,37.594877,126.627178 +3208,˹,õ2ȣ,37.561405,126.677566 +4209,˾,ö1ȣ,37.569098,126.674007 +3207,˾,õ2ȣ,37.56866,126.675687 +1504,Ɽ,氭,37.399907,127.25263 +4610,⵵ûϺû,μ,37.75059,127.071495 +1451,渶,4ȣ,37.443885,127.007888 +317,溹(μû),3ȣ,37.575762,126.97353 +3115,αԱ,õ1ȣ,37.538157,126.722597 +4604,ö,μ,37.737202,127.043257 +341,,3ȣ,37.495918,127.12454 +3114,,õ1ȣ,37.543238,126.728128 +4208,,ö1ȣ,37.571662,126.7363 +3110,,õ1ȣ,37.571449,126.73578 +2553,,5ȣ,37.555004,127.154151 +2641,(),6ȣ,37.590508,127.036296 +1873,,μ,37.24963,126.980248 +329,͹̳,3ȣ,37.504891,127.004916 +2736,͹̳,7ȣ,37.503367,127.005068 +4123,͹̳,9ȣ,37.50598,127.004403 +1757,,4ȣ,37.316784,126.823144 +4513,,μ,37.24484,127.214251 +4928,,,37.601243,126.770345 +1272,,߾Ӽ,37.645676,126.801762 +4612,,μ,37.750471,127.083715 +1506,,氭,37.351315,127.34674 +2530,,5ȣ,37.544431,126.951372 +2627,,6ȣ,37.543555,126.951678 +1292,,߾Ӽ,37.542596,126.952099 +4202,,ö1ȣ,37.54253,126.952024 +2718,(б),7ȣ,37.625742,127.072896 +4103,׽,9ȣ,37.563726,126.810678 +4212,ȭû,ö1ȣ,37.459041,126.477516 +1453,õ,4ȣ,37.433021,126.996568 +1705,,1ȣ,37.419232,126.908706 +4411,ǻ(),Ÿ,37.4691018,126.9450639 +4319,(),źд缱,37.30211,127.044483 +4318,߾(ִ),źд缱,37.288617,127.051478 +2547,(Ŵ),5ȣ,37.545303,127.10357 +1750,,1ȣ,37.416182,126.884466 +2750,Ÿ,7ȣ,37.479252,126.854876 +1019,,1ȣ,37.623632,127.061835 +2534,ȭ(ȭȸ),5ȣ,37.571525,126.97717 +2625,â(),6ȣ,37.547456,126.931993 +223,(.û),2ȣ,37.493961,127.014667 +330,(.û),3ȣ,37.493025,127.013786 +4921,,,37.645384,126.628633 +1701,,1ȣ,37.503039,126.881966 +232,εд,2ȣ,37.485266,126.901401 +1026,,д缱,37.486839,127.058856 +1205,,߾Ӽ,37.603392,127.143869 +4121,,9ȣ,37.501364,126.987332 +2616,,6ȣ,37.611377,126.91727 +1863,,д缱,37.298969,127.105664 +9009,, ö,37.29913,127.10389 +213,(û),2ȣ,37.537077,127.085916 +1813,,1ȣ,37.496756,126.870793 +310,Ĺ,3ȣ,37.636763,126.918821 +1214,,߾Ӽ,37.516169,127.399367 +3138,,õ1ȣ,37.399907,126.630347 +4114,ȸǻ,9ȣ,37.528105,126.917874 +2545,(ɵ),5ȣ,37.557088,127.079577 +2727,(ɵ),7ȣ,37.556897,127.079338 +1709,,1ȣ,37.35356,126.948462 +1324,,ἱ,37.832067,127.557695 +2760,õ,7ȣ,37.506997,126.73128 +2551,ٸ(ȸ),5ȣ,37.545477,127.142853 +3111,,õ1ȣ,37.566379,126.742654 +1316,ݰ,ἱ,37.637382,127.207853 +1279,ݸ,߾Ӽ,37.751322,126.765347 +1708,,1ȣ,37.372221,126.943429 +1458,,4ȣ,37.372209,126.943417 +1703,õû,1ȣ,37.455626,126.89398 +1280,,߾Ӽ,37.766217,126.774644 +324,ȣ,3ȣ,37.548034,127.015872 +1865,,д缱,37.275061,127.11591 +4501,,μ,37.275449,127.116665 +2550,浿,5ȣ,37.537801,127.140004 +417,,4ȣ,37.603407,127.025053 +4511,跮,μ,37.237247,127.198781 +1327,,ἱ,37.818466,127.71434 +2513,,5ȣ,37.562384,126.801292 +4102,,9ȣ,37.561916,126.802152 +4207,,ö1ȣ,37.561842,126.801904 +4929,,,37.56236,126.801868 +1980,,ؼ,37.5617,126.8041 +2519,ġ,5ȣ,37.531768,126.846683 +2753,ġ,7ȣ,37.506207,126.810939 +227,,2ȣ,37.47693,126.963693 +2747,,7ȣ,37.486056,126.887249 +3225,û,õ2ȣ,37.448161,126.736939 +1883,δũ,μ,37.407722,126.695216 +331,͹̳(),3ȣ,37.485013,127.016189 +2739,,7ȣ,37.484596,126.971251 +1002,,1ȣ,37.541021,126.9713 +2828,,8ȣ,37.4624,127.13977 +1328,õ,ἱ,37.864007,127.723792 +434,·,4ȣ,37.463873,126.989134 +2823,ѻ꼺Ա(.û),8ȣ,37.451535,127.159816 +2737,,7ȣ,37.487618,126.993513 +4118,,9ȣ,37.512887,126.953222 +4117,뷮,9ȣ,37.513534,126.941005 +1004,뷮,1ȣ,37.514149,126.94271 +411,,4ȣ,37.65627,127.063276 +2715,,7ȣ,37.654836,127.060462 +313,,3ȣ,37.600927,126.935756 +2630,(걸û),6ȣ,37.534675,126.986695 +1908,,1ȣ,37.75938,127.042292 +1021,õ,1ȣ,37.644799,127.051269 +2734,,7ȣ,37.511093,127.021415 +4305,,źд缱,37.511093,127.021415 +1271,ɰ,߾Ӽ,37.618808,126.820783 +2824,ܴŸ,8ȣ,37.44521,127.156866 +4811,޹,ؼ,37.348847,126.809409 +1878,޿,μ,37.379681,126.745177 +2543,ʸ,5ȣ,37.566747,127.052704 +409,,4ȣ,37.670272,127.079066 +4407,,Ÿ,37.4902998,126.9275133 +237,,2ȣ,37.534946,126.902767 +4113,,9ȣ,37.533406,126.902809 +1729,,1ȣ,37.344285,126.948345 +9002,, ö,37.63191,126.81113 +1953,,3ȣ,37.631626,126.811024 +1452,,4ȣ,37.435675,127.006523 +233,븲(αû),2ȣ,37.493243,126.894932 +2746,븲(αû),7ȣ,37.493013,126.897075 +1028,Ա,д缱,37.491373,127.07272 +1005,,1ȣ,37.513342,126.926382 +4402,,Ÿ,37.5133059,126.9257265 +1320,뼺,ἱ,37.684071,127.379319 +1752,߹,4ȣ,37.328467,126.917332 +337,û,3ȣ,37.493514,127.079532 +335,ġ,3ȣ,37.494612,127.063642 +1958,ȭ,3ȣ,37.676087,126.747569 +2626,(),6ȣ,37.547771,126.942069 +1910,,1ȣ,37.818486,127.056486 +1208,,߾Ӽ,37.586781,127.208832 +1911,,1ȣ,37.843188,127.061277 +334,,3ȣ,37.490922,127.055452 +1025,,д缱,37.491224,127.055186 +1206,,߾Ӽ,37.608806,127.161153 +247,õ,2ȣ,37.514287,126.882768 +1902,,1ȣ,37.679563,127.045595 +2712,,7ȣ,37.689241,127.046509 +1903,,1ȣ,37.689534,127.046049 +1209,,߾Ӽ,37.579622,127.222672 +1817,,1ȣ,37.468446,126.642706 +1823,ȭ,1ȣ,37.46607,126.668672 +316,,3ȣ,37.574571,126.957748 +2614,,6ȣ,37.618456,126.933031 +1714,,1ȣ,37.466613,126.889249 +3206,,õ2ȣ,37.585212,126.675844 +2644,,6ȣ,37.610537,127.056431 +155,빮,1ȣ,37.571687,127.01093 +421,빮,4ȣ,37.57093,127.009287 +205,빮繮ȭ,2ȣ,37.565613,127.009054 +422,빮繮ȭ,4ȣ,37.565133,127.007885 +2537,빮繮ȭ,5ȣ,37.564665,127.005353 +322,Ա,3ȣ,37.559052,127.005602 +1915,õ,1ȣ,37.927878,127.05479 +1913,õ߾,1ȣ,37.901885,127.056482 +3132,,õ1ȣ,37.397878,126.674005 +159,,1ȣ,37.573197,127.01648 +2637,,6ȣ,37.572279,127.015653 +4505,,μ,37.269043,127.152716 +3121,,õ1ȣ,37.485312,126.718247 +1808,,1ȣ,37.471408,126.702896 +4608,,μ,37.745271,127.056947 +1811,õ,1ȣ,37.475276,126.632802 +431,(),4ȣ,37.502852,126.980347 +4120,(),9ȣ,37.502878,126.978153 +4314,õ,źд缱,37.337928,127.102976 +3131,,õ1ȣ,37.404737,126.681015 +9010,ź, ö,37.20034,127.09569 +1727,,1ȣ,36.833705,127.14896 +4515,,μ,37.267051,127.21364 +2555,̵,5ȣ,37.527788,127.136248 +4137,̿,9ȣ,37.519683,127.137989 +4109,,9ȣ,37.550632,126.865689 +2619,й̵Ƽ,6ȣ,37.576108,126.901391 +1294,й̵Ƽ,߾Ӽ,37.577475,126.900453 +4204,й̵Ƽ,ö1ȣ,37.576958,126.898609 +210,Ҽ,2ȣ,37.547184,127.047367 +2730,Ҽ,7ȣ,37.53154,127.066704 +2515,,5ȣ,37.560183,126.825448 +4105,,9ȣ,37.566778,126.82731 +4206,,ö1ȣ,37.565543,126.827378 +1955,,3ȣ,37.652206,126.77762 +2714,,7ȣ,37.66494,127.057675 +4922,,,37.640732,126.644344 +1319,,ἱ,37.652782,127.311767 +2542,,5ȣ,37.5661,127.042973 +3204,,õ2ȣ,37.597566,126.666998 +2561,õ,5ȣ,37.49499,127.152781 +2529,,5ȣ,37.539574,126.945932 +2621,û,6ȣ,37.563515,126.903343 +3224,,õ2ȣ,37.454911,126.732094 +1203,,߾Ӽ,37.59955,127.091909 +2622,,6ȣ,37.556094,126.910052 +1904,,1ȣ,37.709914,127.047455 +1869,,д缱,37.245795,127.057353 +1872,ű,д缱,37.265481,127.015678 +333,ź,3ȣ,37.486947,127.046769 +1870,źǼ,д缱,37.252759,127.040566 +2720,԰,7ȣ,37.610637,127.077725 +2723,,7ȣ,37.588579,127.087503 +424,,4ȣ,37.560989,126.986325 +2552,,5ȣ,37.55137,127.143999 +4510,,μ,37.237964,127.190294 +1707,,1ȣ,37.384653,126.935433 +2827,,8ȣ,37.433824,127.129837 +1853,,д缱,37.432052,127.129104 +3223,𷡳,õ2ȣ,37.45583,126.719298 +2521,,5ȣ,37.526065,126.864931 +2814,伺(ȭǹ),8ȣ,37.517409,127.112359 +315,,3ȣ,37.582299,126.950291 +235,,2ȣ,37.517933,126.89476 +1284,,߾Ӽ,37.854619,126.788047 +2819,,8ȣ,37.485855,127.1225 +3127,а,õ1ȣ,37.434935,126.698579 +1858,̱,д缱,37.350077,127.10891 +4313,̱,źд缱,37.349982,127.108918 +9996,̻,5ȣ,37.560927,127.193877 +415,̾(̹),4ȣ,37.62667,127.025983 +416,̾ƻŸ,4ȣ,37.613292,127.030053 +3112,,õ1ȣ,37.553703,126.745077 +1753,ݿ,4ȣ,37.312212,126.903524 +2735,,7ȣ,37.508178,127.011727 +4601,߰,μ,37.727048,127.052803 +2516,߻,5ȣ,37.558598,126.837668 +225,,2ȣ,37.481426,126.997596 +2557,,5ȣ,37.508857,127.126133 +1901,,1ȣ,37.667503,127.044273 +2511,ȭ,5ȣ,37.577446,126.812741 +1405,,1ȣ,36.777629,127.052991 +1273,鸶,߾Ӽ,37.658239,126.794461 +1954,鼮,3ȣ,37.643114,126.78787 +1325,縮,ἱ,37.830779,127.58933 +1807,,1ȣ,37.483664,126.707704 +2633,Ƽ,6ȣ,37.548013,127.007055 +1457,,4ȣ,37.389793,126.950806 +4603,,μ,37.728755,127.04353 +1313,,ἱ,37.64202,127.12684 +408,,4ȣ,37.66778,127.11581 +1716,,1ȣ,37.207503,127.032731 +2744,,7ȣ,37.499872,126.920428 +4404,,Ÿ,37.5002739,126.9204355 +4405,Ű,Ÿ,37.4955691,126.9180827 +4406,ź,Ÿ,37.4929598,126.9234964 +2639,,6ȣ,37.585274,127.019351 +4712,,̽ż,37.585286,127.019381 +1914,,1ȣ,37.913702,127.057277 +1861,,д缱,37.312752,127.108196 +4514,,μ,37.258965,127.218457 +2821,,8ȣ,37.471052,127.126732 +1031,,д缱,37.470345,127.126658 +1401,,1ȣ,36.801215,127.135763 +4129,,9ȣ,37.514219,127.060245 +229,õ,2ȣ,37.482362,126.941892 +2648,ȭ(Ƿ),6ȣ,37.617283,127.091401 +1815,ΰ,1ȣ,37.488418,126.74109 +1509,ι,氭,37.260192,127.490277 +1804,õ,1ȣ,37.48405,126.782686 +2757,õû,7ȣ,37.504631,126.763538 +2754,õտ,7ȣ,37.50538,126.797337 +1982,õտ,ؼ,37.505457,126.797289 +1806,,1ȣ,37.489445,126.724506 +3120,,õ1ȣ,37.490535,126.723453 +2761,û,7ȣ,37.507394,126.721599 +3118,û,õ1ȣ,37.508407,126.720555 +3122,Ÿ,õ1ȣ,37.477679,126.710208 +3119,,õ1ȣ,37.498383,126.722244 +4709,ѻ꺸,̽ż,37.612072,127.008251 +4701,ѻ,̽ż,37.662909,127.012706 +312,ұ,3ȣ,37.610553,126.92982 +2613,ұ,6ȣ,37.610873,126.92939 +2724,簡,7ȣ,37.580894,127.088478 +226,,2ȣ,37.476538,126.981544 +433,,4ȣ,37.476955,126.981651 +1315,縪,ἱ,37.65108,127.176933 +1877,縮,μ,37.28998,126.85685 +4926,(û),,37.620249,126.719731 +4124,,9ȣ,37.504206,127.015259 +3762,,7ȣ,37.5086,126.7035277 +1751,꺻,4ȣ,37.358101,126.933274 +2822,꼺,8ȣ,37.457122,127.149908 +4508,ﰡ,μ,37.242115,127.168075 +428,ﰢ,4ȣ,37.534075,126.9726 +2629,ﰢ,6ȣ,37.535534,126.974032 +1503,ﵿ,氭,37.409522,127.20336 +2759,ü,7ȣ,37.506411,126.742153 +3759,ü,7ȣ,37.50724,126.74179 +9006,Z, ö,37.50887,127.06324 +219,Z(),2ȣ,37.508844,127.06316 +4128,Z߾,9ȣ,37.513011,127.053282 +1950,,3ȣ,37.653083,126.895558 +4706,,̽ż,37.626914,127.018106 +4707,Ÿ,̽ż,37.621337,127.020473 +4131,,9ȣ,37.504738,127.088025 +1866,,д缱,37.26181,127.108847 +410,,4ȣ,37.660878,127.073572 +2741,,7ȣ,37.502834,126.94791 +3758,,7ȣ,37.505814,126.753163 +1754,ϼ,4ȣ,37.302795,126.866489 +2722,(ÿܹ͹̳),7ȣ,37.595577,127.085716 +1202,(ÿܹ͹̳),߾Ӽ,37.596678,127.08504 +2624,,6ȣ,37.547716,126.922852 +207,սʸ,2ȣ,37.564354,127.029354 +2643,(ѱб),6ȣ,37.606377,127.048491 +2554,ϵ,5ȣ,37.556712,127.166417 +1322,õ,ἱ,37.770246,127.454821 +4317,,źд缱,37.297664,127.069342 +4609,,μ,37.748885,127.06362 +2617,(Ż),6ȣ,37.591148,126.913629 +4116,,9ȣ,37.517274,126.928422 +4401,,Ÿ,37.5170969,126.929399 +1263,,߾Ӽ,37.551881,126.935711 +3210,û,õ2ȣ,37.543742,126.676787 +2533,빮,5ȣ,37.565773,126.966641 +1749,ź,1ȣ,37.195504,127.051672 +3214,οȸ,õ2ȣ,37.500168,126.675795 +1009,,߾Ӽ,37.519594,126.988537 +9005,, ö,37.55569,126.97296 +4410,뺥óŸ,Ÿ,37.4720019,126.9339351 +228,Ա(DZû),2ȣ,37.481247,126.952739 +1847,」,д缱,37.543617,127.044707 +426,↑,4ȣ,37.55281,126.972556 +1001,↑,1ȣ,37.554337,126.971134 +1291,↑,߾Ӽ,37.557231,126.97103 +4201,↑,ö1ȣ,37.553247,126.969769 +4403,溴û,Ÿ,37.5060464,126.9227083 +4409,,Ÿ,37.4782341,126.9330365 +1722,,1ȣ,37.056496,127.052819 +224,,2ȣ,37.491897,127.007917 +1855,,д缱,37.385126,127.123592 +2645,,6ȣ,37.614872,127.065595 +1018,,1ȣ,37.614532,127.065934 +3763,(źϽ),7ȣ,37.5062285,126.6762813 +3213,(źϽ),õ2ȣ,37.506193,126.676203 +3220,,õ2ȣ,37.457611,126.692575 +1704,,1ȣ,37.435047,126.902295 +3222,õŸ,õ2ȣ,37.456805,126.709986 +2816,,8ȣ,37.505557,127.106832 +4133,,9ȣ,37.505208,127.10704 +4132,̰,9ȣ,37.502558,127.097033 +220,,2ȣ,37.504286,127.048203 +1023,,д缱,37.504856,127.048807 +1450,,4ȣ,37.451673,127.002303 +4812,,ؼ,37.334353,126.809904 +4112,,9ȣ,37.53802,126.893525 +4127,,9ȣ,37.510297,127.043999 +1850,,д缱,37.510735,127.043677 +3128,,õ1ȣ,37.426684,126.698863 +1711,հ,1ȣ,37.300349,126.97075 +1512,,氭,37.39468,127.11945 +9008,, ö,37.39467,127.12058 +4316,,źд缱,37.313335,127.0801 +211,,2ȣ,37.544581,127.055961 +418,ſԱ(),4ȣ,37.592612,127.016441 +4711,ſԱ(),̽ż,37.592467,127.016516 +1725,ȯ,1ȣ,36.916076,127.126964 +1715,,1ȣ,37.245025,127.013222 +1717,,1ȣ,37.187533,127.04318 +1510,ո,氭,37.295309,127.570938 +3137,Ʈũ,õ1ȣ,37.393054,126.634729 +1880,ҷ,μ,37.40095,126.733522 +1814,һ,1ȣ,37.482753,126.79544 +4804,һ,ؼ,37.483279,126.795023 +4805,һ,ؼ,37.468467,126.797252 +1916,ҿ,1ȣ,37.9481,127.061034 +4702,ֹ,̽ż,37.65603,127.013273 +4708,ֻ,̽ż,37.620238,127.013626 +1805,۳,1ȣ,37.4876,126.753664 +1886,۵,μ,37.428514,126.657772 +3139,۵޺,õ1ȣ,37.407143,126.62597 +4614,ۻ,μ,37.737279,127.087159 +2514,,5ȣ,37.561184,126.811973 +1721,ź,1ȣ,37.075696,127.054301 +2817,,8ȣ,37.499703,127.112183 +4134,ij,9ȣ,37.510372,127.112216 +1856,,д缱,37.378455,127.114322 +2713,,7ȣ,37.67785,127.055315 +1763,,4ȣ,37.349801,126.925365 +1267,,߾Ӽ,37.580842,126.895611 +339,,3ȣ,37.487378,127.101907 +1030,,д缱,37.487472,127.101422 +9007,, ö,37.48637,127.10161 +1713,,1ȣ,37.266348,126.999561 +1846,,д缱,37.265917,126.999422 +1871,û,д缱,37.261911,127.030736 +414,(ϱû),4ȣ,37.638052,127.025732 +4315,û,źд缱,37.322702,127.095026 +2826,,8ȣ,37.437428,127.140722 +427,Ա(),4ȣ,37.54456,126.972106 +2740,ǴԱ(),7ȣ,37.496029,126.953822 +1889,,μ,37.460789,126.638297 +3219,ùΰ(ȭâ),õ2ȣ,37.458335,126.681192 +151,û,1ȣ,37.565715,126.977088 +201,û,2ȣ,37.563588,126.975411 +4509,û.δ,μ,37.239151,127.178406 +4810,ɰ,ؼ,37.369864,126.808573 +4806,,ؼ,37.450145,126.793041 +4809,û,ؼ,37.382223,126.805625 +1864,Ű,д缱,37.286102,127.111313 +2539,űȣ,5ȣ,37.554548,127.020331 +2526,ű,5ȣ,37.517623,126.914839 +1032,ű,1ȣ,37.516862,126.917865 +1760,űõ,4ȣ,37.338212,126.765844 +2649,ų,6ȣ,37.613174,127.102231 +1311,ų,ἱ,37.612887,127.103218 +4125,ų,9ȣ,37.504598,127.02506 +4306,ų,źд缱,37.504598,127.02506 +245,Ŵ,2ȣ,37.57004,127.046481 +206,Ŵ,2ȣ,37.56564,127.019614 +2636,Ŵ,6ȣ,37.566154,127.016146 +231,Ŵ,2ȣ,37.487462,126.913149 +2743,ŴŸ,7ȣ,37.499701,126.928276 +234,ŵ,2ȣ,37.508961,126.891084 +1007,ŵ,1ȣ,37.508787,126.891144 +1507,ŵе,氭,37.317185,127.40476 +230,Ÿ,2ȣ,37.484201,126.929715 +4408,Ÿ,Ÿ,37.4849266,126.9296159 +4111,Ÿ,9ȣ,37.544277,126.88308 +4122,Ź,9ȣ,37.503415,126.995925 +4104,Źȭ,9ȣ,37.567532,126.816601 +327,Ż,3ȣ,37.516334,127.020114 +4304,Ż,źд缱,37.516334,127.020114 +156,ż,1ȣ,37.576048,127.024634 +246,ż,2ȣ,37.574747,127.024932 +4713,ż,̽ż,37.576095,127.023242 +3129,ſ,õ1ȣ,37.41804,126.693863 +429,ſ,4ȣ,37.52917,126.967894 +1213,ſ,߾Ӽ,37.525545,127.372921 +1017,̹,1ȣ,37.601854,127.067325 +2520,(),5ȣ,37.524997,126.856191 +249,װŸ,2ȣ,37.520074,126.852912 +3756,ߵ,7ȣ,37.50282,126.77566 +1408,â(õ),1ȣ,36.769502,126.951108 +4807,õ,ؼ,37.439066,126.786788 +240,,2ȣ,37.555131,126.936926 +1252,,߾Ӽ,37.559733,126.942597 +1890,,μ,37.46874,126.623853 +2745,dz,7ȣ,37.50008,126.90993 +4808,,ؼ,37.409008,126.788017 +2825,,8ȣ,37.440918,127.147564 +413,ֹ,4ȣ,37.648627,127.034709 +1402,ֿ(緿),1ȣ,36.793759,127.1214 +1403,ƻ,1ȣ,36.792053,127.104361 +3209,ƽþƵ(̻Ÿ),õ2ȣ,37.5517,126.677122 +1215,ƽ,߾Ӽ,37.51382,127.443173 +2546,(̴Ĺ),5ȣ,37.551691,127.089761 +242,,2ȣ,37.557345,126.956141 +318,ȱ,3ȣ,37.576477,126.985443 +1759,Ȼ,4ȣ,37.327082,126.788532 +2640,Ⱦ(뺴),6ȣ,37.586272,127.029005 +1706,Ⱦ,1ȣ,37.401592,126.922874 +2811,ϻ,8ȣ,37.55021,127.127562 +326,б,3ȣ,37.527072,127.028461 +1848,бε,д缱,37.527381,127.040534 +2531,ֿ,5ȣ,37.553736,126.95682 +1277,ߴ,߾Ӽ,37.712327,126.761356 +1876,߸,μ,37.264179,126.879483 +1854,ž,д缱,37.411185,127.128715 +323,,3ȣ,37.554867,127.010541 +2634,,6ȣ,37.554263,127.010358 +1212,,߾Ӽ,37.545981,127.329098 +1204,,߾Ӽ,37.606596,127.107906 +332,(ʱû),3ȣ,37.484477,127.033902 +4308,(ʱû),źд缱,37.483809,127.034653 +4309,ùǽ(),źд缱,37.470023,127.03842 +1207,,߾Ӽ,37.60533,127.19364 +1909,,1ȣ,37.774381,127.044708 +248,õû,2ȣ,37.512398,126.865819 +4106,õⱳ,9ȣ,37.568381,126.841333 +4920,,,37.642379,126.614309 +2523,,5ȣ,37.525569,126.886129 +1217,,߾Ӽ,37.492773,127.491837 +4613,,μ,37.742802,127.085035 +2728,̴(),7ȣ,37.548014,127.074658 +4504,,μ,37.274917,127.143714 +1875,õ,μ,37.250102,126.90879 +4126,,9ȣ,37.507287,127.033868 +2528,dz,5ȣ,37.527098,126.932901 +2527,ǵ,5ȣ,37.521747,126.924357 +4115,ǵ,9ȣ,37.52176,126.92403 +1511,,氭,37.282308,127.628816 +1803,,1ȣ,37.485178,126.811502 +221,,2ȣ,37.500622,127.036456 +2612,,6ȣ,37.606021,126.922744 +1885,,μ,37.417804,126.67894 +311,ų,3ȣ,37.619229,126.921038 +2615,ų,6ȣ,37.618636,126.920625 +9004,ų, ö,37.61878,126.9213 +1919,õ,1ȣ,38.10073,127.07372 +4110,â,9ȣ,37.546936,126.874916 +1006,,1ȣ,37.515504,126.907628 +236,û,2ȣ,37.525706,126.89661 +2524,û,5ȣ,37.5242,126.89503 +2525,,5ȣ,37.522669,126.905139 +4217,,ö1ȣ,37.51202,126.524254 +1868,,д缱,37.251568,127.071394 +3125,ȸ,õ1ȣ,37.449396,126.701012 +342,,3ȣ,37.502129,127.128319 +2558,,5ȣ,37.502057,127.127938 +406,,4ȣ,37.705,127.19281 +1802,,1ȣ,37.494526,126.845365 +1859,,д缱,37.339824,127.108942 +2522,(񵿿),5ȣ,37.524496,126.875181 +1874,õ,μ,37.24304,126.963676 +1216,,߾Ӽ,37.506062,127.473868 +1719,,1ȣ,37.145885,127.06672 +1718,,1ȣ,37.168953,127.063197 +1762,̵,4ȣ,37.362357,126.738714 +325,,3ȣ,37.541684,127.017269 +1011,,߾Ӽ,37.540446,127.018672 +2752,¼(ȸԱ),7ȣ,37.492092,126.823023 +1821,¼(ȸԱ),1ȣ,37.492433,126.824086 +1407,¾õ,1ȣ,36.780483,127.003249 +2556,øȰ(ѱü),5ȣ,37.516201,127.130923 +4136,øȰ(ѱü),9ȣ,37.516269,127.130288 +3205,,õ2ȣ,37.592928,126.673203 +3202,ձ,õ2ȣ,37.59518,126.642696 +208,սʸ(û),2ȣ,37.561238,127.036954 +2541,սʸ(û),5ȣ,37.56184,127.037059 +1013,սʸ(û),߾Ӽ,37.561827,127.038352 +1016,ܴ,1ȣ,37.596073,127.063549 +244,,2ȣ,37.561904,127.050899 +250,(빮û),2ȣ,37.574028,127.038091 +2725,븶,7ȣ,37.573647,127.086727 +1219,빮,߾Ӽ,37.48223,127.594647 +1003,,1ȣ,37.529849,126.964561 +2517,,5ȣ,37.548768,126.836318 +1211,,߾Ӽ,37.554669,127.310115 +4512,.۴,μ,37.237845,127.209198 +4211,,ö1ȣ,37.492904,126.49379 +4924,,,37.653867,126.68393 +3227,(â),õ2ȣ,37.440127,126.75997 +1278,,߾Ӽ,37.725826,126.767257 +9000,, ö,37.71614,126.72841 +1286,õ,߾Ӽ,37.879942,126.769999 +4814,,ؼ,37.31321,126.796261 +1951,,3ȣ,37.653324,126.843041 +1218,,߾Ӽ,37.468672,127.547076 +4815,,ؼ,37.302371,126.786691 +1884,,μ,37.413049,126.686648 +3130,,õ1ȣ,37.412333,126.687869 +1981,,ؼ,37.5239,126.8049 +1948,,3ȣ,37.650658,126.872642 +1020,,1ȣ,37.633212,127.058831 +2642,(),6ȣ,37.601948,127.041518 +1879,,μ,37.391769,126.742699 +2620,Ű(),6ȣ,37.569532,126.899298 +1282,,߾Ӽ,37.796188,126.792587 +203,3,2ȣ,37.566306,126.991696 +320,3,3ȣ,37.566672,126.992548 +204,4,2ȣ,37.566595,126.997817 +2536,4,5ȣ,37.567352,126.998032 +202,Ա,2ȣ,37.566014,126.982618 +1012,,߾Ӽ,37.549946,127.034538 +2611,,6ȣ,37.598605,126.915577 +1710,ǿ,1ȣ,37.320852,126.948217 +1906,,1ȣ,37.738415,127.045958 +4605,νû,μ,37.739256,127.034781 +4607,߾,μ,37.743676,127.049565 +241,̴,2ȣ,37.556733,126.946013 +1502,̸,氭,37.394655,127.127819 +1860,̸,д缱,37.395371,127.128248 +2738,̼,7ȣ,37.485196,126.981605 +1508,õ,氭,37.265579,127.44226 +430,(߾ӹڹ),4ȣ,37.522295,126.974733 +1008,(߾ӹڹ),߾Ӽ,37.522427,126.973406 +2631,¿,6ȣ,37.534488,126.994302 +1455,δ,4ȣ,37.401553,126.976715 +1812,õ,1ȣ,37.476079,126.616801 +1891,õ,μ,37.476403,126.617326 +3215,õ,õ2ȣ,37.4897,126.675208 +4213,õ1͹̳,ö1ȣ,37.447464,126.452508 +4215,õ2͹̳,ö1ȣ,37.460699,126.441442 +1881,õ,μ,37.400614,126.722478 +3226,õ,õ2ȣ,37.448769,126.752618 +3136,õԱ,õ1ȣ,37.386007,126.639484 +3124,õû,õ1ȣ,37.457263,126.702143 +3221,õû,õ2ȣ,37.456833,126.701306 +3126,õ͹̳,õ1ȣ,37.442383,126.699706 +1888,ϴ,μ,37.448493,126.649619 +1275,ϻ,߾Ӽ,37.682077,126.769846 +338,Ͽ,3ȣ,37.483681,127.08439 +1285,,߾Ӽ,37.888421,126.746765 +3113,,õ1ȣ,37.545059,126.738665 +3116,,õ1ȣ,37.530415,126.722527 +216,(ıû),2ȣ,37.513262,127.100159 +2815,(ıû),8ȣ,37.514692,127.104338 +215,dz,2ȣ,37.520733,127.10379 +217,ǻ,2ȣ,37.511687,127.086162 +328,,3ȣ,37.512759,127.01122 +4923,,,37.643986,126.669017 +2742,¹,7ȣ,37.504898,126.93915 +2711,,7ȣ,37.700109,127.053196 +2820,,8ȣ,37.478703,127.126191 +2544,,5ȣ,37.56144,127.064623 +1918,,1ȣ,38.02458,127.0718 +4517,.,μ,37.285342,127.219561 +4710,,̽ż,37.603133,127.013396 +1956,߻,3ȣ,37.659477,126.773359 +1454,ΰõû,4ȣ,37.426513,126.98978 +1761,,4ȣ,37.351735,126.742989 +1857,,д缱,37.365994,127.10807 +4312,,źд缱,37.367098,127.108403 +157,⵿,1ȣ,37.578103,127.034893 +1810,,1ȣ,37.466769,126.656666 +152,,1ȣ,37.570161,126.982923 +153,3,1ȣ,37.570406,126.991847 +319,3,3ȣ,37.571605,126.991791 +2535,3,5ȣ,37.57254,126.990305 +154,5,1ȣ,37.570926,127.001849 +218,տ,2ȣ,37.511022,127.073704 +4130,տ,9ȣ,37.511426,127.076275 +1809,־,1ȣ,37.465047,126.679742 +3218,־,õ2ȣ,37.464992,126.679098 +3217,־ȱ,õ2ȣ,37.473703,126.68113 +1957,ֿ,3ȣ,37.670072,126.761334 +1862,,д缱,37.324753,127.107395 +2716,߰,7ȣ,37.644583,127.064303 +2726,߰,7ȣ,37.565923,127.08432 +1822,ߵ,1ȣ,37.486562,126.764843 +1201,߶,߾Ӽ,37.594917,127.076116 +1756,߾,4ȣ,37.315941,126.838573 +4138,߾Ӻƺ,9ȣ,37.529191,127.148739 +2721,ȭ,7ȣ,37.602545,127.079264 +4108,,9ȣ,37.557402,126.861939 +2618,(),6ȣ,37.583876,126.909645 +4503,,μ,37.269606,127.136515 +3135,,õ1ȣ,37.378384,126.645168 +1723,,1ȣ,37.0188,127.070444 +309,,3ȣ,37.648033,126.913917 +1220,,߾Ӽ,37.476393,127.629874 +1912,,1ȣ,37.892334,127.055716 +1726,,1ȣ,36.870593,127.143904 +1720,,1ȣ,37.109447,127.062278 +405,,4ȣ,37.7205,127.2034 +412,â,4ȣ,37.653088,127.047274 +1022,â,߾Ӽ,37.653007,127.047806 +2638,â,6ȣ,37.579661,127.015241 +1318,õ,ἱ,37.658978,127.285379 +1728,õ,1ȣ,36.810005,127.146826 +2751,õ,7ȣ,37.486637,126.838713 +2548,õȣ(dz伺),5ȣ,37.53864,127.123308 +2812,õȣ(dz伺),8ȣ,37.538113,127.123254 +2749,ö,7ȣ,37.47605,126.867911 +4310,ûԱ,źд缱,37.447211,127.055664 +2538,û,5ȣ,37.560276,127.013639 +2635,û,6ȣ,37.560608,127.013986 +2731,û,7ȣ,37.519365,127.05335 +4210,û,ö1ȣ,37.556409,126.624648 +158,û(øԱ),1ȣ,37.579956,127.044585 +1014,û(øԱ),߾Ӽ,37.580759,127.0483 +1867,û,д缱,37.259489,127.078934 +1321,û,ἱ,37.735488,127.42661 +4506,ʴ,μ,37.260752,127.159443 +1917,ʼ,1ȣ,37.98172,127.06912 +1505,ʿ,氭,37.374419,127.299 +4813,,ؼ,37.319619,126.808147 +1758,,4ȣ,37.320646,126.805913 +432,ѽŴԱ(̼),4ȣ,37.486263,126.981989 +3755,,7ȣ,37.50365,126.78828 +1329,õ,ἱ,37.885054,127.717023 +321,湫,3ȣ,37.56143,126.994072 +423,湫,4ȣ,37.561207,126.99408 +243,(Ա),2ȣ,37.559704,126.964378 +2532,(Ա),5ȣ,37.560236,126.9629 +3133,ķ۽Ÿ,õ1ȣ,37.387855,126.661673 +9001,Ųؽ, ö,37.66532,126.74843 +1276,ź,߾Ӽ,37.694023,126.761086 +4615,ž,μ,37.733579,127.088704 +1404,,1ȣ,36.78866,127.08485 +2646,¸Ա,6ȣ,37.617338,127.074735 +2719,¸Ա,7ȣ,37.618294,127.075397 +1852,,д缱,37.440019,127.127709 +3134,ũũ,õ1ȣ,37.382268,126.656365 +1314,,ἱ,37.648311,127.143952 +1283,,߾Ӽ,37.815298,126.792783 +1501,DZ,氭,37.394761,127.111217 +4311,DZ,źд缱,37.394761,127.112217 +1210,ȴ,߾Ӽ,37.547371,127.243939 +1317,ȣ,ἱ,37.653225,127.244493 +1456,,4ȣ,37.394287,126.963883 +1724,,1ȣ,36.990726,127.085159 +4927,dz,,37.612488,126.732387 +1274,dz,߾Ӽ,37.672346,126.786243 +2717,ϰ,7ȣ,37.636352,127.06799 +2565,ϳ˴ܻ,5ȣ,37.53972,127.22345 +2566,ϳû(dz-),5ȣ,37.54205,127.20612 +2564,ϳdz,5ȣ,37.552034,127.203864 +2733,е,7ȣ,37.514229,127.031656 +336,п,3ȣ,37.496663,127.070594 +2632,Ѱ,6ȣ,37.539631,127.001725 +1010,ѳ,߾Ӽ,37.52943,127.009169 +1755,Ѵ,4ȣ,37.309689,126.85344 +419,ѼԱ(Q),4ȣ,37.588458,127.006221 +4135,Ѽ,9ȣ,37.516404,127.116503 +209,Ѿ,2ȣ,37.555273,127.043655 +1024,Ƽ,д缱,37.496237,127.052873 +238,,2ȣ,37.549457,126.913808 +2623,,6ȣ,37.549209,126.913366 +2540,,5ȣ,37.557322,127.029476 +1270,,߾Ӽ,37.612102,126.834146 +420,ȭ,4ȣ,37.582336,127.001844 +1882,ȣ,μ,37.401637,126.708627 +239,ȫԱ,2ȣ,37.55679,126.923708 +1293,ȫԱ,߾Ӽ,37.557641,126.926683 +1264,ȫԱ,߾Ӽ,37.557641,126.926683 +4203,ȫԱ,ö1ȣ,37.557438,126.926715 +314,ȫ,3ȣ,37.589066,126.943736 +4705,ȭ,̽ż,37.634133,127.017511 +2518,ȭ,5ȣ,37.541513,126.840461 +2647,ȭ(←Ա),6ȣ,37.620064,127.084689 +1712,ȭ,1ȣ,37.283862,126.989627 +1268,ȭ,߾Ӽ,37.602888,126.868387 +1952,ȭ,3ȣ,37.634592,126.83265 +1015,ȸ,߾Ӽ,37.58946,127.057583 +1905,ȸ,1ȣ,37.724416,127.04736 +4602,ȸ,μ,37.725006,127.047073 +425,ȸ(빮),4ȣ,37.558514,126.978246 +4611,ȿ,μ,37.754025,127.076902 +2628,ȿâ,6ȣ,37.539233,126.961384 +1261,ȿâ,߾Ӽ,37.538579,126.96221 +4119,漮(߾ӴԱ),9ȣ,37.50877,126.963708 +4606,Q,μ,37.743302,127.037023 \ No newline at end of file diff --git a/backend/bang-ggood/src/main/resources/seoul_stations_240925.csv b/backend/bang-ggood/src/main/resources/seoul_stations_240925.csv new file mode 100644 index 000000000..da20d4abc --- /dev/null +++ b/backend/bang-ggood/src/main/resources/seoul_stations_240925.csv @@ -0,0 +1,768 @@ +_ID,,ȣ,,浵 +4703,4.19ֹ,̽ż,37.649502,127.013684 +1907,,1ȣ,37.748577,127.044213 +340,,3ȣ,37.492245,127.117757 +2818,,8ȣ,37.492888,127.118398 +2748,д,7ȣ,37.480338,126.882656 +1702,д,1ȣ,37.481581,126.882581 +4107,,9ȣ,37.561391,126.854456 +4704,,̽ż,37.641537,127.016789 +3216,,õ2ȣ,37.484192,126.683673 +3211,(Ƽ),õ2ȣ,37.524649,126.675539 +3212,߾ӽ,õ2ȣ,37.517054,126.676672 +1265,,߾Ӽ,37.568491,126.915487 +1851,õ,κд缱,37.448605,127.126697 +1323,,ἱ,37.814536,127.510739 +1816,,1ȣ,37.464737,126.694181 +3123,Ÿ,õ1ȣ,37.467048,126.707938 +1312,,ἱ,37.634118,127.114757 +3117,,õ1ȣ,37.517268,126.721514 +222,,2ȣ,37.49799,127.027912 +4307,,źд缱,37.496837,127.028104 +2732,û,7ȣ,37.517179,127.041255 +1849,û,κд缱,37.517469,127.041151 +4502,,ο,37.270161,127.126033 +2549,,5ȣ,37.535804,127.132481 +2813,û,8ȣ,37.530341,127.120508 +1269,,߾Ӽ,37.612314,126.843223 +214,(͹̳),2ȣ,37.535095,127.094681 +9995,,5ȣ,37.55749,127.17593 +1326,,ἱ,37.805723,127.634146 +2559,,5ȣ,37.498079,127.13482 +1801,,1ȣ,37.494594,126.85968 +1027,,κд缱,37.489116,127.06614 +4101,ȭ,9ȣ,37.578608,126.798153 +2512,ȭ,5ȣ,37.572399,126.806171 +2560,ſ,5ȣ,37.493105,127.14415 +212,ǴԱ,2ȣ,37.540373,127.069191 +2729,ǴԱ,7ȣ,37.540786,127.071011 +4925,Ϻ,,37.63165,126.705975 +3203,˴ܻŸ,õ2ȣ,37.60185,126.657108 +3201,˴ܿ(˴ܻ),õ2ȣ,37.594877,126.627178 +3208,˹,õ2ȣ,37.561405,126.677566 +4209,˾,ö,37.569098,126.674007 +3207,˾,õ2ȣ,37.56866,126.675687 +1504,Ɽ,氭,37.399907,127.25263 +4610,⵵ûϺû,ΰö,37.75059,127.071495 +1451,渶,4ȣ,37.443885,127.007888 +317,溹(μû),3ȣ,37.575762,126.97353 +3115,αԱ,õ1ȣ,37.538157,126.722597 +4604,ö,ΰö,37.737202,127.043257 +341,,3ȣ,37.495918,127.12454 +3114,,õ1ȣ,37.543238,126.728128 +4208,,ö,37.571662,126.7363 +3110,,õ1ȣ,37.571449,126.73578 +2553,,5ȣ,37.555004,127.154151 +2641,(),6ȣ,37.590508,127.036296 +1873,,κд缱,37.24963,126.980248 +329,͹̳,3ȣ,37.504891,127.004916 +2736,͹̳,7ȣ,37.503367,127.005068 +4123,͹̳,9ȣ,37.50598,127.004403 +1757,,4ȣ,37.316784,126.823144 +4513,,ο,37.24484,127.214251 +4928,,,37.601243,126.770345 +1272,,߾Ӽ,37.645676,126.801762 +4612,,ΰö,37.750471,127.083715 +1506,,氭,37.351315,127.34674 +2530,,5ȣ,37.544431,126.951372 +2627,,6ȣ,37.543555,126.951678 +1292,,߾Ӽ,37.542596,126.952099 +4202,,ö,37.54253,126.952024 +2718,(б),7ȣ,37.625742,127.072896 +4103,׽,9ȣ,37.563726,126.810678 +4212,ȭû,ö,37.459041,126.477516 +1453,õ,4ȣ,37.433021,126.996568 +1705,,1ȣ,37.419232,126.908706 +4411,ǻ(),Ÿ,37.4691018,126.9450639 +4319,(),źд缱,37.30211,127.044483 +4318,߾(ִ),źд缱,37.288617,127.051478 +2547,(Ŵ),5ȣ,37.545303,127.10357 +1750,,1ȣ,37.416182,126.884466 +2750,Ÿ,7ȣ,37.479252,126.854876 +1019,,1ȣ,37.623632,127.061835 +2534,ȭ(ȭȸ),5ȣ,37.571525,126.97717 +2625,â(),6ȣ,37.547456,126.931993 +223,(.û),2ȣ,37.493961,127.014667 +330,(.û),3ȣ,37.493025,127.013786 +4921,,,37.645384,126.628633 +1701,,1ȣ,37.503039,126.881966 +232,εд,2ȣ,37.485266,126.901401 +1026,,κд缱,37.486839,127.058856 +1205,,߾Ӽ,37.603392,127.143869 +4121,,9ȣ,37.501364,126.987332 +2616,,6ȣ,37.611377,126.91727 +1863,,κд缱,37.298969,127.105664 +9009,,GTX-A,37.29913,127.10389 +213,(û),2ȣ,37.537077,127.085916 +1813,,1ȣ,37.496756,126.870793 +310,Ĺ,3ȣ,37.636763,126.918821 +1214,,߾Ӽ,37.516169,127.399367 +3138,,õ1ȣ,37.399907,126.630347 +4114,ȸǻ,9ȣ,37.528105,126.917874 +2545,(ɵ),5ȣ,37.557088,127.079577 +2727,(ɵ),7ȣ,37.556897,127.079338 +1709,,1ȣ,37.35356,126.948462 +1324,,ἱ,37.832067,127.557695 +2760,õ,7ȣ,37.506997,126.73128 +2551,ٸ(ȸ),5ȣ,37.545477,127.142853 +3111,,õ1ȣ,37.566379,126.742654 +1316,ݰ,ἱ,37.637382,127.207853 +1279,ݸ,߾Ӽ,37.751322,126.765347 +1708,,1ȣ,37.372221,126.943429 +1458,,4ȣ,37.372209,126.943417 +1703,õû,1ȣ,37.455626,126.89398 +1280,,߾Ӽ,37.766217,126.774644 +324,ȣ,3ȣ,37.548034,127.015872 +1865,,κд缱,37.275061,127.11591 +4501,,ο,37.275449,127.116665 +2550,浿,5ȣ,37.537801,127.140004 +417,,4ȣ,37.603407,127.025053 +4511,跮,ο,37.237247,127.198781 +1327,,ἱ,37.818466,127.71434 +2513,,5ȣ,37.562384,126.801292 +4102,,9ȣ,37.561916,126.802152 +4207,,ö,37.561842,126.801904 +4929,,,37.56236,126.801868 +1980,,ؼ,37.5617,126.8041 +2519,ġ,5ȣ,37.531768,126.846683 +2753,ġ,7ȣ,37.506207,126.810939 +227,,2ȣ,37.47693,126.963693 +2747,,7ȣ,37.486056,126.887249 +3225,û,õ2ȣ,37.448161,126.736939 +1883,δũ,κд缱,37.407722,126.695216 +331,͹̳(),3ȣ,37.485013,127.016189 +2739,,7ȣ,37.484596,126.971251 +1002,,1ȣ,37.541021,126.9713 +2828,,8ȣ,37.4624,127.13977 +1328,õ,ἱ,37.864007,127.723792 +434,·,4ȣ,37.463873,126.989134 +2823,ѻ꼺Ա(.û),8ȣ,37.451535,127.159816 +2737,,7ȣ,37.487618,126.993513 +4118,,9ȣ,37.512887,126.953222 +4117,뷮,9ȣ,37.513534,126.941005 +1004,뷮,1ȣ,37.514149,126.94271 +411,,4ȣ,37.65627,127.063276 +2715,,7ȣ,37.654836,127.060462 +313,,3ȣ,37.600927,126.935756 +2630,(걸û),6ȣ,37.534675,126.986695 +1908,,1ȣ,37.75938,127.042292 +1021,õ,1ȣ,37.644799,127.051269 +2734,,7ȣ,37.511093,127.021415 +4305,,źд缱,37.511093,127.021415 +1271,ɰ,߾Ӽ,37.618808,126.820783 +2824,ܴŸ,8ȣ,37.44521,127.156866 +4811,޹,ؼ,37.348847,126.809409 +1878,޿,κд缱,37.379681,126.745177 +2543,ʸ,5ȣ,37.566747,127.052704 +409,,4ȣ,37.670272,127.079066 +4407,,Ÿ,37.4902998,126.9275133 +237,,2ȣ,37.534946,126.902767 +4113,,9ȣ,37.533406,126.902809 +1729,,1ȣ,37.344285,126.948345 +9002,,GTX-A,37.63191,126.81113 +1953,,3ȣ,37.631626,126.811024 +1452,,4ȣ,37.435675,127.006523 +233,븲(αû),2ȣ,37.493243,126.894932 +2746,븲(αû),7ȣ,37.493013,126.897075 +1028,Ա,κд缱,37.491373,127.07272 +1005,,1ȣ,37.513342,126.926382 +4402,,Ÿ,37.5133059,126.9257265 +1320,뼺,ἱ,37.684071,127.379319 +1752,߹,4ȣ,37.328467,126.917332 +337,û,3ȣ,37.493514,127.079532 +335,ġ,3ȣ,37.494612,127.063642 +1958,ȭ,3ȣ,37.676087,126.747569 +2626,(),6ȣ,37.547771,126.942069 +1910,,1ȣ,37.818486,127.056486 +1208,,߾Ӽ,37.586781,127.208832 +1911,,1ȣ,37.843188,127.061277 +334,,3ȣ,37.490922,127.055452 +1025,,κд缱,37.491224,127.055186 +1206,,߾Ӽ,37.608806,127.161153 +247,õ,2ȣ,37.514287,126.882768 +1902,,1ȣ,37.679563,127.045595 +2712,,7ȣ,37.689241,127.046509 +1903,,1ȣ,37.689534,127.046049 +1209,,߾Ӽ,37.579622,127.222672 +1817,,1ȣ,37.468446,126.642706 +1823,ȭ,1ȣ,37.46607,126.668672 +316,,3ȣ,37.574571,126.957748 +2614,,6ȣ,37.618456,126.933031 +1714,,1ȣ,37.466613,126.889249 +3206,,õ2ȣ,37.585212,126.675844 +2644,,6ȣ,37.610537,127.056431 +155,빮,1ȣ,37.571687,127.01093 +421,빮,4ȣ,37.57093,127.009287 +205,빮繮ȭ,2ȣ,37.565613,127.009054 +422,빮繮ȭ,4ȣ,37.565133,127.007885 +2537,빮繮ȭ,5ȣ,37.564665,127.005353 +322,Ա,3ȣ,37.559052,127.005602 +1915,õ,1ȣ,37.927878,127.05479 +1913,õ߾,1ȣ,37.901885,127.056482 +3132,,õ1ȣ,37.397878,126.674005 +159,,1ȣ,37.573197,127.01648 +2637,,6ȣ,37.572279,127.015653 +4505,,ο,37.269043,127.152716 +3121,,õ1ȣ,37.485312,126.718247 +1808,,1ȣ,37.471408,126.702896 +4608,,ΰö,37.745271,127.056947 +1811,õ,1ȣ,37.475276,126.632802 +431,(),4ȣ,37.502852,126.980347 +4120,(),9ȣ,37.502878,126.978153 +4314,õ,źд缱,37.337928,127.102976 +3131,,õ1ȣ,37.404737,126.681015 +9010,ź,GTX-A,37.20034,127.09569 +1727,,1ȣ,36.833705,127.14896 +4515,,ο,37.267051,127.21364 +2555,̵,5ȣ,37.527788,127.136248 +4137,̿,9ȣ,37.519683,127.137989 +4109,,9ȣ,37.550632,126.865689 +2619,й̵Ƽ,6ȣ,37.576108,126.901391 +1294,й̵Ƽ,߾Ӽ,37.577475,126.900453 +4204,й̵Ƽ,ö,37.576958,126.898609 +210,Ҽ,2ȣ,37.547184,127.047367 +2730,Ҽ,7ȣ,37.53154,127.066704 +2515,,5ȣ,37.560183,126.825448 +4105,,9ȣ,37.566778,126.82731 +4206,,ö,37.565543,126.827378 +1955,,3ȣ,37.652206,126.77762 +2714,,7ȣ,37.66494,127.057675 +4922,,,37.640732,126.644344 +1319,,ἱ,37.652782,127.311767 +2542,,5ȣ,37.5661,127.042973 +3204,,õ2ȣ,37.597566,126.666998 +2561,õ,5ȣ,37.49499,127.152781 +2529,,5ȣ,37.539574,126.945932 +2621,û,6ȣ,37.563515,126.903343 +3224,,õ2ȣ,37.454911,126.732094 +1203,,߾Ӽ,37.59955,127.091909 +2622,,6ȣ,37.556094,126.910052 +1904,,1ȣ,37.709914,127.047455 +1869,,κд缱,37.245795,127.057353 +1872,ű,κд缱,37.265481,127.015678 +333,ź,3ȣ,37.486947,127.046769 +1870,źǼ,κд缱,37.252759,127.040566 +2720,԰,7ȣ,37.610637,127.077725 +2723,,7ȣ,37.588579,127.087503 +424,,4ȣ,37.560989,126.986325 +2552,,5ȣ,37.55137,127.143999 +4510,,ο,37.237964,127.190294 +1707,,1ȣ,37.384653,126.935433 +2827,,8ȣ,37.433824,127.129837 +1853,,κд缱,37.432052,127.129104 +3223,𷡳,õ2ȣ,37.45583,126.719298 +2521,,5ȣ,37.526065,126.864931 +2814,伺(ȭǹ),8ȣ,37.517409,127.112359 +315,,3ȣ,37.582299,126.950291 +235,,2ȣ,37.517933,126.89476 +1284,,߾Ӽ,37.854619,126.788047 +2819,,8ȣ,37.485855,127.1225 +3127,а,õ1ȣ,37.434935,126.698579 +1858,̱,κд缱,37.350077,127.10891 +4313,̱,źд缱,37.349982,127.108918 +9996,̻,5ȣ,37.560927,127.193877 +415,̾(̹),4ȣ,37.62667,127.025983 +416,̾ƻŸ,4ȣ,37.613292,127.030053 +3112,,õ1ȣ,37.553703,126.745077 +1753,ݿ,4ȣ,37.312212,126.903524 +2735,,7ȣ,37.508178,127.011727 +4601,߰,ΰö,37.727048,127.052803 +2516,߻,5ȣ,37.558598,126.837668 +225,,2ȣ,37.481426,126.997596 +2557,,5ȣ,37.508857,127.126133 +1901,,1ȣ,37.667503,127.044273 +2511,ȭ,5ȣ,37.577446,126.812741 +1405,,1ȣ,36.777629,127.052991 +1273,鸶,߾Ӽ,37.658239,126.794461 +1954,鼮,3ȣ,37.643114,126.78787 +1325,縮,ἱ,37.830779,127.58933 +1807,,1ȣ,37.483664,126.707704 +2633,Ƽ,6ȣ,37.548013,127.007055 +1457,,4ȣ,37.389793,126.950806 +4603,,ΰö,37.728755,127.04353 +1313,,ἱ,37.64202,127.12684 +408,,4ȣ,37.66778,127.11581 +1716,,1ȣ,37.207503,127.032731 +2744,,7ȣ,37.499872,126.920428 +4404,,Ÿ,37.5002739,126.9204355 +4405,Ű,Ÿ,37.4955691,126.9180827 +4406,ź,Ÿ,37.4929598,126.9234964 +2639,,6ȣ,37.585274,127.019351 +4712,,̽ż,37.585286,127.019381 +1914,,1ȣ,37.913702,127.057277 +1861,,κд缱,37.312752,127.108196 +4514,,ο,37.258965,127.218457 +2821,,8ȣ,37.471052,127.126732 +1031,,κд缱,37.470345,127.126658 +1401,,1ȣ,36.801215,127.135763 +4129,,9ȣ,37.514219,127.060245 +229,õ,2ȣ,37.482362,126.941892 +2648,ȭ(Ƿ),6ȣ,37.617283,127.091401 +1815,ΰ,1ȣ,37.488418,126.74109 +1509,ι,氭,37.260192,127.490277 +1804,õ,1ȣ,37.48405,126.782686 +2757,õû,7ȣ,37.504631,126.763538 +2754,õտ,7ȣ,37.50538,126.797337 +1982,õտ,ؼ,37.505457,126.797289 +1806,,1ȣ,37.489445,126.724506 +3120,,õ1ȣ,37.490535,126.723453 +2761,û,7ȣ,37.507394,126.721599 +3118,û,õ1ȣ,37.508407,126.720555 +3122,Ÿ,õ1ȣ,37.477679,126.710208 +3119,,õ1ȣ,37.498383,126.722244 +4709,ѻ꺸,̽ż,37.612072,127.008251 +4701,ѻ,̽ż,37.662909,127.012706 +312,ұ,3ȣ,37.610553,126.92982 +2613,ұ,6ȣ,37.610873,126.92939 +2724,簡,7ȣ,37.580894,127.088478 +226,,2ȣ,37.476538,126.981544 +433,,4ȣ,37.476955,126.981651 +1315,縪,ἱ,37.65108,127.176933 +1877,縮,κд缱,37.28998,126.85685 +4926,(û),,37.620249,126.719731 +4124,,9ȣ,37.504206,127.015259 +3762,,7ȣ,37.5086,126.7035277 +1751,꺻,4ȣ,37.358101,126.933274 +2822,꼺,8ȣ,37.457122,127.149908 +4508,ﰡ,ο,37.242115,127.168075 +428,ﰢ,4ȣ,37.534075,126.9726 +2629,ﰢ,6ȣ,37.535534,126.974032 +1503,ﵿ,氭,37.409522,127.20336 +2759,ü,7ȣ,37.506411,126.742153 +3759,ü,7ȣ,37.50724,126.74179 +9006,Z,GTX-A,37.50887,127.06324 +219,Z(),2ȣ,37.508844,127.06316 +4128,Z߾,9ȣ,37.513011,127.053282 +1950,,3ȣ,37.653083,126.895558 +4706,,̽ż,37.626914,127.018106 +4707,Ÿ,̽ż,37.621337,127.020473 +4131,,9ȣ,37.504738,127.088025 +1866,,κд缱,37.26181,127.108847 +410,,4ȣ,37.660878,127.073572 +2741,,7ȣ,37.502834,126.94791 +3758,,7ȣ,37.505814,126.753163 +1754,ϼ,4ȣ,37.302795,126.866489 +2722,(ÿܹ͹̳),7ȣ,37.595577,127.085716 +1202,(ÿܹ͹̳),߾Ӽ,37.596678,127.08504 +2624,,6ȣ,37.547716,126.922852 +207,սʸ,2ȣ,37.564354,127.029354 +2643,(ѱб),6ȣ,37.606377,127.048491 +2554,ϵ,5ȣ,37.556712,127.166417 +1322,õ,ἱ,37.770246,127.454821 +4317,,źд缱,37.297664,127.069342 +4609,,ΰö,37.748885,127.06362 +2617,(Ż),6ȣ,37.591148,126.913629 +4116,,9ȣ,37.517274,126.928422 +4401,,Ÿ,37.5170969,126.929399 +1263,,߾Ӽ,37.551881,126.935711 +3210,û,õ2ȣ,37.543742,126.676787 +2533,빮,5ȣ,37.565773,126.966641 +1749,ź,1ȣ,37.195504,127.051672 +3214,οȸ,õ2ȣ,37.500168,126.675795 +1009,,߾Ӽ,37.519594,126.988537 +9005,,GTX-A,37.55569,126.97296 +4410,뺥óŸ,Ÿ,37.4720019,126.9339351 +228,Ա(DZû),2ȣ,37.481247,126.952739 +1847,」,κд缱,37.543617,127.044707 +426,↑,4ȣ,37.55281,126.972556 +1001,↑,1ȣ,37.554337,126.971134 +1291,↑,߾Ӽ,37.557231,126.97103 +4201,↑,ö,37.553247,126.969769 +4403,溴û,Ÿ,37.5060464,126.9227083 +4409,,Ÿ,37.4782341,126.9330365 +1722,,1ȣ,37.056496,127.052819 +224,,2ȣ,37.491897,127.007917 +1855,,κд缱,37.385126,127.123592 +2645,,6ȣ,37.614872,127.065595 +1018,,1ȣ,37.614532,127.065934 +3763,(źϽ),7ȣ,37.5062285,126.6762813 +3213,(źϽ),õ2ȣ,37.506193,126.676203 +3220,,õ2ȣ,37.457611,126.692575 +1704,,1ȣ,37.435047,126.902295 +3222,õŸ,õ2ȣ,37.456805,126.709986 +2816,,8ȣ,37.505557,127.106832 +4133,,9ȣ,37.505208,127.10704 +4132,̰,9ȣ,37.502558,127.097033 +220,,2ȣ,37.504286,127.048203 +1023,,κд缱,37.504856,127.048807 +1450,,4ȣ,37.451673,127.002303 +4812,,ؼ,37.334353,126.809904 +4112,,9ȣ,37.53802,126.893525 +4127,,9ȣ,37.510297,127.043999 +1850,,κд缱,37.510735,127.043677 +3128,,õ1ȣ,37.426684,126.698863 +1711,հ,1ȣ,37.300349,126.97075 +1512,,氭,37.39468,127.11945 +9008,,GTX-A,37.39467,127.12058 +4316,,źд缱,37.313335,127.0801 +211,,2ȣ,37.544581,127.055961 +418,ſԱ(),4ȣ,37.592612,127.016441 +4711,ſԱ(),̽ż,37.592467,127.016516 +1725,ȯ,1ȣ,36.916076,127.126964 +1715,,1ȣ,37.245025,127.013222 +1717,,1ȣ,37.187533,127.04318 +1510,ո,氭,37.295309,127.570938 +3137,Ʈũ,õ1ȣ,37.393054,126.634729 +1880,ҷ,κд缱,37.40095,126.733522 +1814,һ,1ȣ,37.482753,126.79544 +4804,һ,ؼ,37.483279,126.795023 +4805,һ,ؼ,37.468467,126.797252 +1916,ҿ,1ȣ,37.9481,127.061034 +4702,ֹ,̽ż,37.65603,127.013273 +4708,ֻ,̽ż,37.620238,127.013626 +1805,۳,1ȣ,37.4876,126.753664 +1886,۵,κд缱,37.428514,126.657772 +3139,۵޺,õ1ȣ,37.407143,126.62597 +4614,ۻ,ΰö,37.737279,127.087159 +2514,,5ȣ,37.561184,126.811973 +1721,ź,1ȣ,37.075696,127.054301 +2817,,8ȣ,37.499703,127.112183 +4134,ij,9ȣ,37.510372,127.112216 +1856,,κд缱,37.378455,127.114322 +2713,,7ȣ,37.67785,127.055315 +1763,,4ȣ,37.349801,126.925365 +1267,,߾Ӽ,37.580842,126.895611 +339,,3ȣ,37.487378,127.101907 +1030,,κд缱,37.487472,127.101422 +9007,,GTX-A,37.48637,127.10161 +1713,,1ȣ,37.266348,126.999561 +1846,,κд缱,37.265917,126.999422 +1871,û,κд缱,37.261911,127.030736 +414,(ϱû),4ȣ,37.638052,127.025732 +4315,û,źд缱,37.322702,127.095026 +2826,,8ȣ,37.437428,127.140722 +427,Ա(),4ȣ,37.54456,126.972106 +2740,ǴԱ(),7ȣ,37.496029,126.953822 +1889,,κд缱,37.460789,126.638297 +3219,ùΰ(ȭâ),õ2ȣ,37.458335,126.681192 +151,û,1ȣ,37.565715,126.977088 +201,û,2ȣ,37.563588,126.975411 +4509,û.δ,ο,37.239151,127.178406 +4810,ɰ,ؼ,37.369864,126.808573 +4806,,ؼ,37.450145,126.793041 +4809,û,ؼ,37.382223,126.805625 +1864,Ű,κд缱,37.286102,127.111313 +2539,űȣ,5ȣ,37.554548,127.020331 +2526,ű,5ȣ,37.517623,126.914839 +1032,ű,1ȣ,37.516862,126.917865 +1760,űõ,4ȣ,37.338212,126.765844 +2649,ų,6ȣ,37.613174,127.102231 +1311,ų,ἱ,37.612887,127.103218 +4125,ų,9ȣ,37.504598,127.02506 +4306,ų,źд缱,37.504598,127.02506 +245,Ŵ,2ȣ,37.57004,127.046481 +206,Ŵ,2ȣ,37.56564,127.019614 +2636,Ŵ,6ȣ,37.566154,127.016146 +231,Ŵ,2ȣ,37.487462,126.913149 +2743,ŴŸ,7ȣ,37.499701,126.928276 +234,ŵ,2ȣ,37.508961,126.891084 +1007,ŵ,1ȣ,37.508787,126.891144 +1507,ŵе,氭,37.317185,127.40476 +230,Ÿ,2ȣ,37.484201,126.929715 +4408,Ÿ,Ÿ,37.4849266,126.9296159 +4111,Ÿ,9ȣ,37.544277,126.88308 +4122,Ź,9ȣ,37.503415,126.995925 +4104,Źȭ,9ȣ,37.567532,126.816601 +327,Ż,3ȣ,37.516334,127.020114 +4304,Ż,źд缱,37.516334,127.020114 +156,ż,1ȣ,37.576048,127.024634 +246,ż,2ȣ,37.574747,127.024932 +4713,ż,̽ż,37.576095,127.023242 +3129,ſ,õ1ȣ,37.41804,126.693863 +429,ſ,4ȣ,37.52917,126.967894 +1213,ſ,߾Ӽ,37.525545,127.372921 +1017,̹,1ȣ,37.601854,127.067325 +2520,(),5ȣ,37.524997,126.856191 +249,װŸ,2ȣ,37.520074,126.852912 +3756,ߵ,7ȣ,37.50282,126.77566 +1408,â(õ),1ȣ,36.769502,126.951108 +4807,õ,ؼ,37.439066,126.786788 +240,,2ȣ,37.555131,126.936926 +1252,,߾Ӽ,37.559733,126.942597 +1890,,κд缱,37.46874,126.623853 +2745,dz,7ȣ,37.50008,126.90993 +4808,,ؼ,37.409008,126.788017 +2825,,8ȣ,37.440918,127.147564 +413,ֹ,4ȣ,37.648627,127.034709 +1402,ֿ(緿),1ȣ,36.793759,127.1214 +1403,ƻ,1ȣ,36.792053,127.104361 +3209,ƽþƵ(̻Ÿ),õ2ȣ,37.5517,126.677122 +1215,ƽ,߾Ӽ,37.51382,127.443173 +2546,(̴Ĺ),5ȣ,37.551691,127.089761 +242,,2ȣ,37.557345,126.956141 +318,ȱ,3ȣ,37.576477,126.985443 +1759,Ȼ,4ȣ,37.327082,126.788532 +2640,Ⱦ(뺴),6ȣ,37.586272,127.029005 +1706,Ⱦ,1ȣ,37.401592,126.922874 +2811,ϻ,8ȣ,37.55021,127.127562 +326,б,3ȣ,37.527072,127.028461 +1848,бε,κд缱,37.527381,127.040534 +2531,ֿ,5ȣ,37.553736,126.95682 +1277,ߴ,߾Ӽ,37.712327,126.761356 +1876,߸,κд缱,37.264179,126.879483 +1854,ž,κд缱,37.411185,127.128715 +323,,3ȣ,37.554867,127.010541 +2634,,6ȣ,37.554263,127.010358 +1212,,߾Ӽ,37.545981,127.329098 +1204,,߾Ӽ,37.606596,127.107906 +332,(ʱû),3ȣ,37.484477,127.033902 +4308,(ʱû),źд缱,37.483809,127.034653 +4309,ùǽ(),źд缱,37.470023,127.03842 +1207,,߾Ӽ,37.60533,127.19364 +1909,,1ȣ,37.774381,127.044708 +248,õû,2ȣ,37.512398,126.865819 +4106,õⱳ,9ȣ,37.568381,126.841333 +4920,,,37.642379,126.614309 +2523,,5ȣ,37.525569,126.886129 +1217,,߾Ӽ,37.492773,127.491837 +4613,,ΰö,37.742802,127.085035 +2728,̴(),7ȣ,37.548014,127.074658 +4504,,ο,37.274917,127.143714 +1875,õ,κд缱,37.250102,126.90879 +4126,,9ȣ,37.507287,127.033868 +2528,dz,5ȣ,37.527098,126.932901 +2527,ǵ,5ȣ,37.521747,126.924357 +4115,ǵ,9ȣ,37.52176,126.92403 +1511,,氭,37.282308,127.628816 +1803,,1ȣ,37.485178,126.811502 +221,,2ȣ,37.500622,127.036456 +2612,,6ȣ,37.606021,126.922744 +1885,,κд缱,37.417804,126.67894 +311,ų,3ȣ,37.619229,126.921038 +2615,ų,6ȣ,37.618636,126.920625 +9004,ų,GTX-A,37.61878,126.9213 +1919,õ,1ȣ,38.10073,127.07372 +4110,â,9ȣ,37.546936,126.874916 +1006,,1ȣ,37.515504,126.907628 +236,û,2ȣ,37.525706,126.89661 +2524,û,5ȣ,37.5242,126.89503 +2525,,5ȣ,37.522669,126.905139 +4217,,ö,37.51202,126.524254 +1868,,κд缱,37.251568,127.071394 +3125,ȸ,õ1ȣ,37.449396,126.701012 +342,,3ȣ,37.502129,127.128319 +2558,,5ȣ,37.502057,127.127938 +406,,4ȣ,37.705,127.19281 +1802,,1ȣ,37.494526,126.845365 +1859,,κд缱,37.339824,127.108942 +2522,(񵿿),5ȣ,37.524496,126.875181 +1874,õ,κд缱,37.24304,126.963676 +1216,,߾Ӽ,37.506062,127.473868 +1719,,1ȣ,37.145885,127.06672 +1718,,1ȣ,37.168953,127.063197 +1762,̵,4ȣ,37.362357,126.738714 +325,,3ȣ,37.541684,127.017269 +1011,,߾Ӽ,37.540446,127.018672 +2752,¼(ȸԱ),7ȣ,37.492092,126.823023 +1821,¼(ȸԱ),1ȣ,37.492433,126.824086 +1407,¾õ,1ȣ,36.780483,127.003249 +2556,øȰ(ѱü),5ȣ,37.516201,127.130923 +4136,øȰ(ѱü),9ȣ,37.516269,127.130288 +3205,,õ2ȣ,37.592928,126.673203 +3202,ձ,õ2ȣ,37.59518,126.642696 +208,սʸ(û),2ȣ,37.561238,127.036954 +2541,սʸ(û),5ȣ,37.56184,127.037059 +1013,սʸ(û),߾Ӽ,37.561827,127.038352 +1016,ܴ,1ȣ,37.596073,127.063549 +244,,2ȣ,37.561904,127.050899 +250,(빮û),2ȣ,37.574028,127.038091 +2725,븶,7ȣ,37.573647,127.086727 +1219,빮,߾Ӽ,37.48223,127.594647 +1003,,1ȣ,37.529849,126.964561 +2517,,5ȣ,37.548768,126.836318 +1211,,߾Ӽ,37.554669,127.310115 +4512,.۴,ο,37.237845,127.209198 +4211,,ö,37.492904,126.49379 +4924,,,37.653867,126.68393 +3227,(â),õ2ȣ,37.440127,126.75997 +1278,,߾Ӽ,37.725826,126.767257 +9000,,GTX-A,37.71614,126.72841 +1286,õ,߾Ӽ,37.879942,126.769999 +4814,,ؼ,37.31321,126.796261 +1951,,3ȣ,37.653324,126.843041 +1218,,߾Ӽ,37.468672,127.547076 +4815,,ؼ,37.302371,126.786691 +1884,,κд缱,37.413049,126.686648 +3130,,õ1ȣ,37.412333,126.687869 +1981,,ؼ,37.5239,126.8049 +1948,,3ȣ,37.650658,126.872642 +1020,,1ȣ,37.633212,127.058831 +2642,(),6ȣ,37.601948,127.041518 +1879,,κд缱,37.391769,126.742699 +2620,Ű(),6ȣ,37.569532,126.899298 +1282,,߾Ӽ,37.796188,126.792587 +203,3,2ȣ,37.566306,126.991696 +320,3,3ȣ,37.566672,126.992548 +204,4,2ȣ,37.566595,126.997817 +2536,4,5ȣ,37.567352,126.998032 +202,Ա,2ȣ,37.566014,126.982618 +1012,,߾Ӽ,37.549946,127.034538 +2611,,6ȣ,37.598605,126.915577 +1710,ǿ,1ȣ,37.320852,126.948217 +1906,,1ȣ,37.738415,127.045958 +4605,νû,ΰö,37.739256,127.034781 +4607,߾,ΰö,37.743676,127.049565 +241,̴,2ȣ,37.556733,126.946013 +1502,̸,氭,37.394655,127.127819 +1860,̸,κд缱,37.395371,127.128248 +2738,̼,7ȣ,37.485196,126.981605 +1508,õ,氭,37.265579,127.44226 +430,(߾ӹڹ),4ȣ,37.522295,126.974733 +1008,(߾ӹڹ),߾Ӽ,37.522427,126.973406 +2631,¿,6ȣ,37.534488,126.994302 +1455,δ,4ȣ,37.401553,126.976715 +1812,õ,1ȣ,37.476079,126.616801 +1891,õ,κд缱,37.476403,126.617326 +3215,õ,õ2ȣ,37.4897,126.675208 +4213,õ1͹̳,ö,37.447464,126.452508 +4215,õ2͹̳,ö,37.460699,126.441442 +1881,õ,κд缱,37.400614,126.722478 +3226,õ,õ2ȣ,37.448769,126.752618 +3136,õԱ,õ1ȣ,37.386007,126.639484 +3124,õû,õ1ȣ,37.457263,126.702143 +3221,õû,õ2ȣ,37.456833,126.701306 +3126,õ͹̳,õ1ȣ,37.442383,126.699706 +1888,ϴ,κд缱,37.448493,126.649619 +1275,ϻ,߾Ӽ,37.682077,126.769846 +338,Ͽ,3ȣ,37.483681,127.08439 +1285,,߾Ӽ,37.888421,126.746765 +3113,,õ1ȣ,37.545059,126.738665 +3116,,õ1ȣ,37.530415,126.722527 +216,(ıû),2ȣ,37.513262,127.100159 +2815,(ıû),8ȣ,37.514692,127.104338 +215,dz,2ȣ,37.520733,127.10379 +217,ǻ,2ȣ,37.511687,127.086162 +328,,3ȣ,37.512759,127.01122 +4923,,,37.643986,126.669017 +2742,¹,7ȣ,37.504898,126.93915 +2711,,7ȣ,37.700109,127.053196 +2820,,8ȣ,37.478703,127.126191 +2544,,5ȣ,37.56144,127.064623 +1918,,1ȣ,38.02458,127.0718 +4517,.,ο,37.285342,127.219561 +4710,,̽ż,37.603133,127.013396 +1956,߻,3ȣ,37.659477,126.773359 +1454,ΰõû,4ȣ,37.426513,126.98978 +1761,,4ȣ,37.351735,126.742989 +1857,,κд缱,37.365994,127.10807 +4312,,źд缱,37.367098,127.108403 +157,⵿,1ȣ,37.578103,127.034893 +1810,,1ȣ,37.466769,126.656666 +152,,1ȣ,37.570161,126.982923 +153,3,1ȣ,37.570406,126.991847 +319,3,3ȣ,37.571605,126.991791 +2535,3,5ȣ,37.57254,126.990305 +154,5,1ȣ,37.570926,127.001849 +218,տ,2ȣ,37.511022,127.073704 +4130,տ,9ȣ,37.511426,127.076275 +1809,־,1ȣ,37.465047,126.679742 +3218,־,õ2ȣ,37.464992,126.679098 +3217,־ȱ,õ2ȣ,37.473703,126.68113 +1957,ֿ,3ȣ,37.670072,126.761334 +1862,,κд缱,37.324753,127.107395 +2716,߰,7ȣ,37.644583,127.064303 +2726,߰,7ȣ,37.565923,127.08432 +1822,ߵ,1ȣ,37.486562,126.764843 +1201,߶,߾Ӽ,37.594917,127.076116 +1756,߾,4ȣ,37.315941,126.838573 +4138,߾Ӻƺ,9ȣ,37.529191,127.148739 +2721,ȭ,7ȣ,37.602545,127.079264 +4108,,9ȣ,37.557402,126.861939 +2618,(),6ȣ,37.583876,126.909645 +4503,,ο,37.269606,127.136515 +3135,,õ1ȣ,37.378384,126.645168 +1723,,1ȣ,37.0188,127.070444 +309,,3ȣ,37.648033,126.913917 +1220,,߾Ӽ,37.476393,127.629874 +1912,,1ȣ,37.892334,127.055716 +1726,,1ȣ,36.870593,127.143904 +1720,,1ȣ,37.109447,127.062278 +405,,4ȣ,37.7205,127.2034 +412,â,4ȣ,37.653088,127.047274 +1022,â,߾Ӽ,37.653007,127.047806 +2638,â,6ȣ,37.579661,127.015241 +1318,õ,ἱ,37.658978,127.285379 +1728,õ,1ȣ,36.810005,127.146826 +2751,õ,7ȣ,37.486637,126.838713 +2548,õȣ(dz伺),5ȣ,37.53864,127.123308 +2812,õȣ(dz伺),8ȣ,37.538113,127.123254 +2749,ö,7ȣ,37.47605,126.867911 +4310,ûԱ,źд缱,37.447211,127.055664 +2538,û,5ȣ,37.560276,127.013639 +2635,û,6ȣ,37.560608,127.013986 +2731,û,7ȣ,37.519365,127.05335 +4210,û,ö,37.556409,126.624648 +158,û(øԱ),1ȣ,37.579956,127.044585 +1014,û(øԱ),߾Ӽ,37.580759,127.0483 +1867,û,κд缱,37.259489,127.078934 +1321,û,ἱ,37.735488,127.42661 +4506,ʴ,ο,37.260752,127.159443 +1917,ʼ,1ȣ,37.98172,127.06912 +1505,ʿ,氭,37.374419,127.299 +4813,,ؼ,37.319619,126.808147 +1758,,4ȣ,37.320646,126.805913 +432,ѽŴԱ(̼),4ȣ,37.486263,126.981989 +3755,,7ȣ,37.50365,126.78828 +1329,õ,ἱ,37.885054,127.717023 +321,湫,3ȣ,37.56143,126.994072 +423,湫,4ȣ,37.561207,126.99408 +243,(Ա),2ȣ,37.559704,126.964378 +2532,(Ա),5ȣ,37.560236,126.9629 +3133,ķ۽Ÿ,õ1ȣ,37.387855,126.661673 +9001,Ųؽ,GTX-A,37.66532,126.74843 +1276,ź,߾Ӽ,37.694023,126.761086 +4615,ž,ΰö,37.733579,127.088704 +1404,,1ȣ,36.78866,127.08485 +2646,¸Ա,6ȣ,37.617338,127.074735 +2719,¸Ա,7ȣ,37.618294,127.075397 +1852,,κд缱,37.440019,127.127709 +3134,ũũ,õ1ȣ,37.382268,126.656365 +1314,,ἱ,37.648311,127.143952 +1283,,߾Ӽ,37.815298,126.792783 +1501,DZ,氭,37.394761,127.111217 +4311,DZ,źд缱,37.394761,127.112217 +1210,ȴ,߾Ӽ,37.547371,127.243939 +1317,ȣ,ἱ,37.653225,127.244493 +1456,,4ȣ,37.394287,126.963883 +1724,,1ȣ,36.990726,127.085159 +4927,dz,,37.612488,126.732387 +1274,dz,߾Ӽ,37.672346,126.786243 +2717,ϰ,7ȣ,37.636352,127.06799 +2565,ϳ˴ܻ,5ȣ,37.53972,127.22345 +2566,ϳû(dz-),5ȣ,37.54205,127.20612 +2564,ϳdz,5ȣ,37.552034,127.203864 +2733,е,7ȣ,37.514229,127.031656 +336,п,3ȣ,37.496663,127.070594 +2632,Ѱ,6ȣ,37.539631,127.001725 +1010,ѳ,߾Ӽ,37.52943,127.009169 +1755,Ѵ,4ȣ,37.309689,126.85344 +419,ѼԱ(Q),4ȣ,37.588458,127.006221 +4135,Ѽ,9ȣ,37.516404,127.116503 +209,Ѿ,2ȣ,37.555273,127.043655 +1024,Ƽ,κд缱,37.496237,127.052873 +238,,2ȣ,37.549457,126.913808 +2623,,6ȣ,37.549209,126.913366 +2540,,5ȣ,37.557322,127.029476 +1270,,߾Ӽ,37.612102,126.834146 +420,ȭ,4ȣ,37.582336,127.001844 +1882,ȣ,κд缱,37.401637,126.708627 +239,ȫԱ,2ȣ,37.55679,126.923708 +1293,ȫԱ,߾Ӽ,37.557641,126.926683 +1264,ȫԱ,߾Ӽ,37.557641,126.926683 +4203,ȫԱ,ö,37.557438,126.926715 +314,ȫ,3ȣ,37.589066,126.943736 +4705,ȭ,̽ż,37.634133,127.017511 +2518,ȭ,5ȣ,37.541513,126.840461 +2647,ȭ(←Ա),6ȣ,37.620064,127.084689 +1712,ȭ,1ȣ,37.283862,126.989627 +1268,ȭ,߾Ӽ,37.602888,126.868387 +1952,ȭ,3ȣ,37.634592,126.83265 +1015,ȸ,߾Ӽ,37.58946,127.057583 +1905,ȸ,1ȣ,37.724416,127.04736 +4602,ȸ,ΰö,37.725006,127.047073 +425,ȸ(빮),4ȣ,37.558514,126.978246 +4611,ȿ,ΰö,37.754025,127.076902 +2628,ȿâ,6ȣ,37.539233,126.961384 +1261,ȿâ,߾Ӽ,37.538579,126.96221 +4119,漮(߾ӴԱ),9ȣ,37.50877,126.963708 +4606,Q,ΰö,37.743302,127.037023 \ No newline at end of file diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/AcceptanceMockTestSupport.java b/backend/bang-ggood/src/test/java/com/bang_ggood/AcceptanceMockTestSupport.java new file mode 100644 index 000000000..bb94f4c78 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/AcceptanceMockTestSupport.java @@ -0,0 +1,28 @@ +package com.bang_ggood; + +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +@ActiveProfiles("test") +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +public abstract class AcceptanceMockTestSupport { + + @Autowired + protected MockMvc mockMvc; + + @LocalServerPort + protected int port; + + @BeforeEach + void setPort() { + RestAssured.port = port; + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/AcceptanceTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/AcceptanceTest.java new file mode 100644 index 000000000..1059973e7 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/AcceptanceTest.java @@ -0,0 +1,61 @@ +package com.bang_ggood; + +import com.bang_ggood.auth.controller.cookie.CookieProvider; +import com.bang_ggood.auth.service.jwt.JwtTokenProvider; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import io.restassured.RestAssured; +import io.restassured.http.Header; +import io.restassured.http.Headers; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + + +@ActiveProfiles("test") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Sql(scripts = {"/schema-test.sql", "/data-test.sql"}) +public abstract class AcceptanceTest extends IntegrationTestSupport { + + protected Headers headers; + @Autowired + private JwtTokenProvider jwtTokenProvider; + @Autowired + private CookieProvider cookieProvider; + @Autowired + private UserRepository userRepository; + private User authenticatedUser; + @LocalServerPort + private int port; + + @BeforeEach + void setUp() { + setPort(); + setResponseCookie(); + } + + private void setPort() { + RestAssured.port = port; + } + + private void setResponseCookie() { + authenticatedUser = userRepository.save(UserFixture.USER1); + String accessToken = jwtTokenProvider.createAccessToken(authenticatedUser); + String refreshToken = jwtTokenProvider.createRefreshToken(authenticatedUser); + ResponseCookie accessTokenResponseCookie = cookieProvider.createAccessTokenCookie(accessToken); + ResponseCookie refreshTokenCookie = cookieProvider.createRefreshTokenCookie(refreshToken); + + headers = new Headers(new Header(HttpHeaders.COOKIE, accessTokenResponseCookie.toString()), + new Header(HttpHeaders.COOKIE, refreshTokenCookie.toString())); + } + + public User getAuthenticatedUser() { + return authenticatedUser; + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/IntegrationTestSupport.java b/backend/bang-ggood/src/test/java/com/bang_ggood/IntegrationTestSupport.java new file mode 100644 index 000000000..446b4a654 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/IntegrationTestSupport.java @@ -0,0 +1,36 @@ +package com.bang_ggood; + +import com.bang_ggood.question.CustomChecklistFixture; +import com.bang_ggood.question.QuestionFixture; +import com.bang_ggood.question.repository.CategoryRepository; +import com.bang_ggood.question.repository.QuestionRepository; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + +@ActiveProfiles("test") +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@Sql(scripts = {"/schema-test.sql", "/data-test.sql"}) +public abstract class IntegrationTestSupport { + + @Autowired + CategoryRepository categoryRepository; + + @Autowired + QuestionRepository questionRepository; + + @Autowired + UserRepository userRepository; + + @BeforeEach + void init() { + UserFixture.init(userRepository); + QuestionFixture.init(categoryRepository, questionRepository); + CustomChecklistFixture.init(); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/JpaAuditingTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/JpaAuditingTest.java new file mode 100644 index 000000000..0fa721a46 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/JpaAuditingTest.java @@ -0,0 +1,76 @@ +package com.bang_ggood; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class JpaAuditingTest extends IntegrationTestSupport { + + @Autowired + private TestRepository testRepository; + + @DisplayName("JPA Auditing 성공") + @Test + void jpaAuditing() { + // given + TestEntity testEntity = new TestEntity("방끗"); + + // when + TestEntity savedEntity = testRepository.save(testEntity); + + // then + assertAll( + () -> assertThat(savedEntity.getCreatedAt()).isBeforeOrEqualTo(LocalDateTime.now()), + () -> assertThat(savedEntity.getModifiedAt()).isBeforeOrEqualTo(LocalDateTime.now()) + ); + } + + @DisplayName("JPA Auditing 성공 : Entity 값 변경 시 수정 시간이 modifiedAt에 반영된다.") + @Test + void jpaAuditing_modifyEntity() { + // given + TestEntity testEntity = new TestEntity("방끗"); + TestEntity savedEntity = testRepository.save(testEntity); + + // when + savedEntity.setName("방방이"); + TestEntity updatedEntity = testRepository.save(savedEntity); + + // then + assertAll( + () -> assertThat(savedEntity.getCreatedAt()).isEqualTo(updatedEntity.getCreatedAt()), + () -> assertThat(savedEntity.getModifiedAt()).isBefore(updatedEntity.getModifiedAt()) + ); + } + + @Table(name = "test_entity") + @Entity + static class TestEntity extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + protected TestEntity() { + } + + public TestEntity(String name) { + this.name = name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/TestRepository.java b/backend/bang-ggood/src/test/java/com/bang_ggood/TestRepository.java new file mode 100644 index 000000000..17f3ababf --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/TestRepository.java @@ -0,0 +1,9 @@ +package com.bang_ggood; + +import com.bang_ggood.JpaAuditingTest.TestEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TestRepository extends JpaRepository { +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/article/ArticleFixture.java b/backend/bang-ggood/src/test/java/com/bang_ggood/article/ArticleFixture.java new file mode 100644 index 000000000..762048299 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/article/ArticleFixture.java @@ -0,0 +1,31 @@ +package com.bang_ggood.article; + +import com.bang_ggood.article.domain.Article; +import com.bang_ggood.article.dto.request.ArticleCreateRequest; + +public class ArticleFixture { + + public static Article ARTICLE() { + return new Article("제목", "내용", "키워드", "요약", "썸네일"); + } + + public static Article ARTICLE_1() { + return new Article("제목1", "내용1", "키워드1", "요약1", "썸네일1"); + } + + public static Article ARTICLE_2() { + return new Article("제목2", "내용2", "키워드2", "요약2", "썸네일2"); + } + + public static Article ARTICLE_3() { + return new Article("제목3", "내용3", "키워드3", "요약3", "썸네일3"); + } + + public static Article ARTICLE_4() { + return new Article("제목4", "내용4", "키워드4", "요약4", "썸네일4"); + } + + public static ArticleCreateRequest ARTICLE_CREATE_REQUEST() { + return new ArticleCreateRequest("제목", "내용", "키워드", "요약", "썸네일"); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/article/controller/ArticleE2ETest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/article/controller/ArticleE2ETest.java new file mode 100644 index 000000000..28c9b92dc --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/article/controller/ArticleE2ETest.java @@ -0,0 +1,122 @@ +package com.bang_ggood.article.controller; + +import com.bang_ggood.AcceptanceTest; +import com.bang_ggood.article.ArticleFixture; +import com.bang_ggood.article.domain.Article; +import com.bang_ggood.article.dto.request.ArticleCreateRequest; +import com.bang_ggood.article.repository.ArticleRepository; +import com.bang_ggood.global.exception.ExceptionCode; +import com.bang_ggood.global.exception.dto.ExceptionResponse; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ArticleE2ETest extends AcceptanceTest { + + @Autowired + ArticleRepository articleRepository; + + @DisplayName("아티클 생성 성공") + @Test + void createArticle() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .body(ArticleFixture.ARTICLE_CREATE_REQUEST()) + .when().post("/articles") + .then().log().all() + .statusCode(201); + } + + @DisplayName("아티클 생성 실패: 유저가 아닌 경우") + @Test + void createArticle_notUser_exception() { + ExceptionResponse response = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(ArticleFixture.ARTICLE_CREATE_REQUEST()) + .when().post("/articles") + .then().log().all() + .statusCode(401) + .extract() + .as(ExceptionResponse.class); + + assertThat(response.message()).isEqualTo(ExceptionCode.AUTHENTICATION_TOKEN_EMPTY.getMessage()); + } + + @DisplayName("아티클 생성 실패: 제목이 비어있는 경우") + @Test + void createArticle_titleBlank_exception() { + ArticleCreateRequest request = new ArticleCreateRequest("", "내용", "키워드", "요약", "썸네일"); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .body(request) + .when().post("/articles") + .then().log().all() + .statusCode(400); + } + + @DisplayName("아티클 조회 성공") + @Test + void readArticle() { + Article article = articleRepository.save(ArticleFixture.ARTICLE()); + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().get("/articles/" + article.getId()) + .then().log().all() + .statusCode(200); + } + + @DisplayName("아티클 조회 실패 : 유효하지 않은 아이디인 경우") + @Test + void readArticle_invalidId_exception() { + long articleId = Long.MAX_VALUE; + + ExceptionResponse response = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().get("/articles/" + articleId) + .then().log().all() + .statusCode(400) + .extract() + .as(ExceptionResponse.class); + + assertThat(response.message()).isEqualTo(ExceptionCode.ARTICLE_NOT_FOUND.getMessage()); + } + + @DisplayName("아티클 목록 조회 성공") + @Test + void readArticlesListView() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().get("/articles") + .then().log().all() + .statusCode(200); + } + + @DisplayName("아티클 삭제 성공") + @Test + void deleteArticle() { + Article article = articleRepository.save(ArticleFixture.ARTICLE()); + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .when().delete("/articles/" + article.getId()) + .then().log().all() + .statusCode(204); + } + + @DisplayName("아티클 삭제 실패: 유저가 아닌 경우") + @Test + void deleteArticle_notUser_exception() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().delete("/articles/" + ArticleFixture.ARTICLE().getId()) + .then().log().all() + .statusCode(401); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/article/repository/ArticleRepositoryTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/article/repository/ArticleRepositoryTest.java new file mode 100644 index 000000000..7328d511f --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/article/repository/ArticleRepositoryTest.java @@ -0,0 +1,77 @@ +package com.bang_ggood.article.repository; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.article.ArticleFixture; +import com.bang_ggood.article.domain.Article; +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ArticleRepositoryTest extends IntegrationTestSupport { + + @Autowired + ArticleRepository articleRepository; + + @DisplayName("아티클 조회 성공") + @Test + void getById() { + // given + Article article = articleRepository.save(ArticleFixture.ARTICLE()); + + // when & then + assertThatCode(() -> articleRepository.getById(article.getId())) + .doesNotThrowAnyException(); + } + + @DisplayName("아티클 조회 실패: 삭제된 경우") + @Test + void getById_deleted_exception() { + // given + Article article = articleRepository.save(ArticleFixture.ARTICLE()); + articleRepository.deleteById(article.getId()); + + // when & then + assertThatThrownBy(() -> articleRepository.getById(article.getId())) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.ARTICLE_NOT_FOUND.getMessage()); + } + + @DisplayName("아티클 조회 실패: 해당 id 아티클이 없는 경우") + @Test + void getById_notFound_exception() { + // given & when & then + assertThatThrownBy(() -> articleRepository.getById(Long.MAX_VALUE)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.ARTICLE_NOT_FOUND.getMessage()); + } + + @DisplayName("아티클 목록 최신순 조회 성공") + @Test + void findLatestArticles() { + // given + Article article1 = articleRepository.save(ArticleFixture.ARTICLE_1()); + Article article2 = articleRepository.save(ArticleFixture.ARTICLE_2()); + + // when & then + assertThat(articleRepository.findLatestArticles()).containsExactly(article2, article1); + } + + @DisplayName("아티클 목록 최신순 조회 성공: 삭제된 아티클 제외") + @Test + void findLatestArticles_exceptDeletedArticle() { + // given + Article article1 = articleRepository.save(ArticleFixture.ARTICLE_1()); + Article article2 = articleRepository.save(ArticleFixture.ARTICLE_2()); + Article article3 = articleRepository.save(ArticleFixture.ARTICLE_3()); + articleRepository.deleteById(ArticleFixture.ARTICLE_1().getId()); + + // when & then + assertThat(articleRepository.findLatestArticles()).containsExactly(article3, article2, article1); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/article/service/ArticleServiceTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/article/service/ArticleServiceTest.java new file mode 100644 index 000000000..dd8122362 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/article/service/ArticleServiceTest.java @@ -0,0 +1,97 @@ +package com.bang_ggood.article.service; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.article.ArticleFixture; +import com.bang_ggood.article.domain.Article; +import com.bang_ggood.article.dto.request.ArticleCreateRequest; +import com.bang_ggood.article.dto.response.ArticlesResponse; +import com.bang_ggood.article.repository.ArticleRepository; +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ArticleServiceTest extends IntegrationTestSupport { + + @Autowired + ArticleService articleService; + @Autowired + ArticleRepository articleRepository; + + @DisplayName("아티클 생성 성공") + @Test + void createArticle() { + // given + ArticleCreateRequest request = ArticleFixture.ARTICLE_CREATE_REQUEST(); + + // when + Long articleId = articleService.createArticle(request); + + // then + assertThat(articleRepository.getById(articleId).getTitle()) + .isEqualTo(ArticleFixture.ARTICLE_CREATE_REQUEST().title()); + } + + @DisplayName("아티클 조회 성공") + @Test + void readArticle() { + // given + Article article = articleRepository.save(ArticleFixture.ARTICLE()); + + // when & then + assertThatCode(() -> articleService.readArticle(article.getId())) + .doesNotThrowAnyException(); + } + + @DisplayName("아티클 조회 실패 : 유효하지 않은 아이디인 경우") + @Test + void readArticle_invalidId_exception() { + // given + long articleId = Long.MAX_VALUE; + + // when & then + assertThatThrownBy(() -> articleService.readArticle(articleId)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.ARTICLE_NOT_FOUND.getMessage()); + } + + @DisplayName("아티클 목록 최신순 조회 성공") + @Test + void readArticles() { + // given + Article article1 = articleRepository.save(ArticleFixture.ARTICLE_1()); + Article article2 = articleRepository.save(ArticleFixture.ARTICLE_2()); + Article article3 = articleRepository.save(ArticleFixture.ARTICLE_3()); + Article article4 = articleRepository.save(ArticleFixture.ARTICLE_4()); + + // when + List articleTitles = articleService.readArticles().articles().stream() + .map(ArticlesResponse::title) + .toList(); + + // then + assertThat(articleTitles).containsExactly(article4.getTitle(), article3.getTitle(), article2.getTitle(), + article1.getTitle()); + } + + @DisplayName("아티클 삭제 성공") + @Test + void deleteArticle() { + // given + articleRepository.save(ArticleFixture.ARTICLE()); + + // when + articleService.deleteArticle(ArticleFixture.ARTICLE().getId()); + + //then + assertThatThrownBy(() -> articleService.readArticle(ArticleFixture.ARTICLE().getId())) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.ARTICLE_NOT_FOUND.getMessage()); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/auth/AuthFixture.java b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/AuthFixture.java new file mode 100644 index 000000000..f34733060 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/AuthFixture.java @@ -0,0 +1,50 @@ +package com.bang_ggood.auth; + +import com.bang_ggood.auth.dto.request.LocalLoginRequestV1; +import com.bang_ggood.auth.dto.request.OauthLoginRequest; +import com.bang_ggood.auth.service.oauth.OauthRequestProperties; + +public class AuthFixture { + + public static final LocalLoginRequestV1 LOCAL_LOGIN_REQUEST = new LocalLoginRequestV1( + "bang-ggood@gmail.com", + "password1234" + ); + + public static final LocalLoginRequestV1 LOCAL_LOGIN_REQUEST_NO_EMAIL = new LocalLoginRequestV1( + null, + "password1234" + ); + + public static final LocalLoginRequestV1 LOCAL_LOGIN_REQUEST_NO_PASSWORD = new LocalLoginRequestV1( + "bang-ggood@gmail.com", + null + ); + + public static final LocalLoginRequestV1 LOCAL_LOGIN_REQUEST_INVALID_EMAIL = new LocalLoginRequestV1( + "bang-bad@gmail.com", + "password1234" + ); + + public static final LocalLoginRequestV1 LOCAL_LOGIN_REQUEST_INVALID_PASSWORD = new LocalLoginRequestV1( + "bang-ggood@gmail.com", + "password12345" + ); + + public static final OauthLoginRequest OAUTH_LOGIN_REQUEST = new OauthLoginRequest("testCode", "localhost:3000"); + public static final String REGISTERED_REDIRECT_URIS = "localhost:3000, localhost:3001"; + public static final String INVALID_REGISTERED_REDIRECT_URI = "localhost:8081"; + + public static final OauthRequestProperties OAUTH_REQUEST_PROPERTIES() { + String tokenPostUri = "testTokenPostUri"; + String userInfoRequestUri = "testUserInfoRequestUri"; + String grantType = "testGrantType"; + String clientId = "testClientId"; + String clientSecret = "testClientSecret"; + + return new OauthRequestProperties( + tokenPostUri, userInfoRequestUri, + grantType, clientId, + REGISTERED_REDIRECT_URIS, clientSecret); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/auth/JwtTokenFixture.java b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/JwtTokenFixture.java new file mode 100644 index 000000000..2ed12dbc7 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/JwtTokenFixture.java @@ -0,0 +1,34 @@ +package com.bang_ggood.auth; + +import com.bang_ggood.auth.service.jwt.JwtTokenProperties; +import com.bang_ggood.auth.service.jwt.JwtTokenProvider; +import java.security.SecureRandom; +import java.util.Base64; + +public class JwtTokenFixture { + + private static final long THIRTY_MINUTE = 1800000L; + + public static JwtTokenProvider JWT_TOKEN_PROVIDER_WITH_INVALID_KEY() { + return new JwtTokenProvider(PROPERTIES_WITH_INVALID_SECRET_KEY()); + } + + public static JwtTokenProvider JWT_TOKEN_PROVIDER_WITH_INVALID_EXPIRED_TIME() { + return new JwtTokenProvider(PROPERTIES_WITH_SHORT_EXPIRED_MILLIS()); + } + + private static String createJwtSecretKey() { + SecureRandom secureRandom = new SecureRandom(); + byte[] key = new byte[32]; + secureRandom.nextBytes(key); + return Base64.getEncoder().encodeToString(key); + } + + private static JwtTokenProperties PROPERTIES_WITH_SHORT_EXPIRED_MILLIS() { + return new JwtTokenProperties(createJwtSecretKey(), 1L, 1L); + } + + private static JwtTokenProperties PROPERTIES_WITH_INVALID_SECRET_KEY() { + return new JwtTokenProperties(createJwtSecretKey(), THIRTY_MINUTE, THIRTY_MINUTE); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/auth/config/ArgumentResolverTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/config/ArgumentResolverTest.java new file mode 100644 index 000000000..7448f42ab --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/config/ArgumentResolverTest.java @@ -0,0 +1,118 @@ +package com.bang_ggood.auth.config; + +import com.bang_ggood.AcceptanceTest; +import com.bang_ggood.auth.controller.cookie.CookieProvider; +import com.bang_ggood.global.exception.ExceptionCode; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.domain.UserType; +import com.bang_ggood.user.repository.UserRepository; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import io.restassured.http.Header; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; + +import static org.hamcrest.Matchers.containsString; + +class ArgumentResolverTest extends AcceptanceTest { + + @Autowired + private CookieProvider cookieProvider; + @Autowired + private UserRepository userRepository; + + @DisplayName("@UserPrincipal 어노테이션 동작 성공 : 토큰값이 없으면 게스트 유저가 할당된다.") + @Test + void resolveUserPrincipalArgument_returnGuestUser() { + // given & when + userRepository.save(UserFixture.GUEST_USER1()); + + User user = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().get(TestController.USER_PRINCIPAL_URL) + .then().log().all() + .statusCode(200) + .extract().as(User.class); + + // then + Assertions.assertThat(user.getUserType()).isEqualTo(UserType.GUEST); + } + + @DisplayName("@UserPrincipal 어노테이션 동작 성공 : 토큰값이 있으면 인증된 유저를 할당한다.") + @Test + void resolveUserPrincipalArgument_returnUser() { + // given & when + User user = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .when().get(TestController.USER_PRINCIPAL_URL) + .then().log().all() + .statusCode(200) + .extract().as(User.class); + + // then + Assertions.assertThat(user.getUserType()).isEqualTo(UserType.USER); + } + + @DisplayName("@AuthPrincipal 어노테이션 동작 성공 : 쿠키값이 없으면 예외를 발생시킨다.") + @Test + void resolveAuthPrincipalArgument_throwException_whenCookieEmpty() { + // given & when & then + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().get(TestController.AUTH_PRINCIPAL_URL) + .then().log().all() + .statusCode(401) + .body("message", containsString(ExceptionCode.AUTHENTICATION_TOKEN_EMPTY.getMessage())); + } + + @DisplayName("@AuthPrincipal 어노테이션 동작 성공 : 토큰값이 없으면 예외를 발생시킨다.") + @Test + void resolveAuthPrincipalArgument_throwException_whenTokenEmpty() { + // given & when & then + ResponseCookie invalidResponseCookie = ResponseCookie.from("testKey", "testValue").build(); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(new Header(HttpHeaders.COOKIE, invalidResponseCookie.toString())) + .when().get(TestController.AUTH_PRINCIPAL_URL) + .then().log().all() + .statusCode(401) + .body("message", containsString(ExceptionCode.AUTHENTICATION_TOKEN_EMPTY.getMessage())); + } + + @DisplayName("@AuthPrinciapl 어노테이션 동작 성공 : 액세스 토큰 존재 X, 리프레시 토큰 존재 O 일때 예외를 발생시킨다.") + @Test + void resolveAuthPrincipalArgument_throwException_whenAccessTokenEmpty() { + // given & when & then + ResponseCookie refreshTokenCookie = cookieProvider.createRefreshTokenCookie("testToken"); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(new Header(HttpHeaders.COOKIE, refreshTokenCookie.toString())) + .when().get(TestController.AUTH_PRINCIPAL_URL) + .then().log().all() + .statusCode(401) + .body("message", containsString(ExceptionCode.AUTHENTICATION_ACCESS_TOKEN_EMPTY.getMessage())); + } + + @DisplayName("@AuthPrinciapl 어노테이션 동작 성공 : 액세스 토큰 존재 O, 리프레시 토큰 존재 X 일때 예외를 발생시킨다.") + @Test + void resolveAuthPrincipalArgument_throwException_whenRefreshTokenEmpty() { + // given & when & then + ResponseCookie accessTokenCookie = cookieProvider.createAccessTokenCookie("testToken"); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(new Header(HttpHeaders.COOKIE, accessTokenCookie.toString())) + .when().get(TestController.AUTH_PRINCIPAL_URL) + .then().log().all() + .statusCode(401) + .body("message", containsString(ExceptionCode.AUTHENTICATION_REFRESH_TOKEN_EMPTY.getMessage())); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/auth/config/TestController.java b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/config/TestController.java new file mode 100644 index 000000000..81bf4bf8b --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/config/TestController.java @@ -0,0 +1,22 @@ +package com.bang_ggood.auth.config; + +import com.bang_ggood.user.domain.User; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + + public static final String USER_PRINCIPAL_URL = "/test/user-principal"; + public static final String AUTH_PRINCIPAL_URL = "/test/auth-principal"; + + @GetMapping(USER_PRINCIPAL_URL) + public User testUserPrincipal(@UserPrincipal User user) { + return user; + } + + @GetMapping(AUTH_PRINCIPAL_URL) + public User testAuthPrincipal(@AuthRequiredPrincipal User user) { + return user; + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/auth/controller/AuthE2ETest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/controller/AuthE2ETest.java new file mode 100644 index 000000000..62f38fdbe --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/controller/AuthE2ETest.java @@ -0,0 +1,141 @@ +package com.bang_ggood.auth.controller; + +import com.bang_ggood.AcceptanceTest; +import com.bang_ggood.auth.dto.response.TokenExistResponse; +import com.bang_ggood.auth.service.AuthService; +import com.bang_ggood.checklist.ChecklistFixture; +import com.bang_ggood.global.exception.ExceptionCode; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import io.restassured.http.Header; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; + +import static com.bang_ggood.auth.AuthFixture.LOCAL_LOGIN_REQUEST; +import static com.bang_ggood.auth.AuthFixture.LOCAL_LOGIN_REQUEST_NO_EMAIL; +import static com.bang_ggood.auth.AuthFixture.LOCAL_LOGIN_REQUEST_NO_PASSWORD; +import static com.bang_ggood.auth.AuthFixture.OAUTH_LOGIN_REQUEST; +import static org.hamcrest.Matchers.containsString; + +class AuthE2ETest extends AcceptanceTest { + + @Autowired + private AuthService authService; + + @DisplayName("로컬 로그인 성공") + @Test + void localLogin() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(LOCAL_LOGIN_REQUEST) + .when().post("/v1/local-auth/login") + .then().log().all() + .statusCode(200); + } + + @DisplayName("로컬 로그인 실패: 이메일이 없는 경우") + @Test + void localLogin_noEmail_exception() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(LOCAL_LOGIN_REQUEST_NO_EMAIL) + .when().post("/v1/local-auth/login") + .then().log().all() + .statusCode(400) + .body("message", containsString("이메일이 존재하지 않습니다.")); + } + + @DisplayName("로컬 로그인 실패: 비밀번호가 없는 경우") + @Test + void localLogin_noPassword_exception() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(LOCAL_LOGIN_REQUEST_NO_PASSWORD) + .when().post("/v1/local-auth/login") + .then().log().all() + .statusCode(400) + .body("message", containsString("비밀번호가 존재하지 않습니다.")); + } + + @DisplayName("카카오 로그인 실패 : 인가코드가 없는 경우") + @Test + void login_code_notBlank_exception() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(OAUTH_LOGIN_REQUEST) + .when().post("/oauth/login") + .then().log().all() + .statusCode(400); + } + + @DisplayName("회원 탈퇴 성공") + @Test + void withdraw() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .when().delete("v1/withdraw") + .then().log().all() + .statusCode(204); + } + + @DisplayName("인증 실패 : 쿠키가 없는 경우") + @Test + void authentication_no_cookie_exception() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(new Header(HttpHeaders.COOKIE, null)) + .when().post("/checklists") + .then().log().all() + .statusCode(401) + .body("message", containsString(ExceptionCode.AUTHENTICATION_TOKEN_EMPTY.getMessage())); + } + + @DisplayName("인증 실패 : 쿠키가 잘못된 형태로 들어간 경우") + @Test + void authentication_invalid_cookie_exception() { + String testToken = "token"; + String expectedCookie = "invalidToken=" + testToken + "; Path=/; HttpOnly"; + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .header(new Header(HttpHeaders.COOKIE, expectedCookie)) + .when().post("/checklists") + .then().log().all() + .statusCode(401) + .body("message", containsString(ExceptionCode.AUTHENTICATION_TOKEN_EMPTY.getMessage())); + } + + @DisplayName("토큰 존재여부 반환 성공 : 쿠키가 존재하지 않는 경우") + @Test + void checkTokenExist_returnFalse() { + TokenExistResponse tokenExistResponse = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .when().get("/token-exist") + .then().log().all() + .statusCode(200) + .extract() + .as(TokenExistResponse.class); + + Assertions.assertThat(tokenExistResponse.isRefreshTokenExist()).isFalse(); + } + + @DisplayName("토큰 존재여부 반환 성공 : 액세스 토큰이 존재하고 리프레시 토큰이 존재하는 경우") + @Test + void checkTokenExist_AccessTokenExist_RefreshTokenExist() { + TokenExistResponse tokenExistResponse = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .when().get("/token-exist") + .then().log().all() + .statusCode(200) + .extract() + .as(TokenExistResponse.class); + + Assertions.assertThat(tokenExistResponse.isAccessTokenExist()).isTrue(); + Assertions.assertThat(tokenExistResponse.isRefreshTokenExist()).isTrue(); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/auth/controller/cookie/CookieResolverTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/controller/cookie/CookieResolverTest.java new file mode 100644 index 000000000..c22c57d24 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/controller/cookie/CookieResolverTest.java @@ -0,0 +1,116 @@ +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.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class CookieResolverTest { + + @DisplayName("쿠키에서 토큰 값 조회 성공 : 값이 액세스 토큰일 때") + @Test + void extractAccessToken() { + // given + HttpServletRequest request = mock(HttpServletRequest.class); + String expectedToken = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI2IiwiaWF0Ijox"; + CookieResolver cookieResolver = new CookieResolver(); + Cookie[] cookies = {new Cookie(CookieProvider.ACCESS_TOKEN_COOKIE_NAME, expectedToken)}; + + // when + when(request.getCookies()).thenReturn(cookies); + String token = cookieResolver.extractAccessToken(request); + + // then + Assertions.assertThat(token).isEqualTo(expectedToken); + } + + @DisplayName("쿠키에서 토큰 값 조회 성공 : 값이 리프레시 토큰일 때") + @Test + void extractRefreshToken() { + // given + HttpServletRequest request = mock(HttpServletRequest.class); + String expectedToken = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI2IiwiaWF0Ijox"; + CookieResolver cookieResolver = new CookieResolver(); + Cookie[] cookies = {new Cookie(CookieProvider.REFRESH_TOKEN_COOKIE_NAME, expectedToken)}; + + // when + when(request.getCookies()).thenReturn(cookies); + String token = cookieResolver.extractRefreshToken(request); + + // then + Assertions.assertThat(token).isEqualTo(expectedToken); + } + + @DisplayName("쿠키에서 토큰 값 조회 실패 : 액세스 토큰 값이 존재하지 않을 때") + @Test + void tokenValueNotExist() { + // given + HttpServletRequest request = mock(HttpServletRequest.class); + CookieResolver cookieResolver = new CookieResolver(); + Cookie[] cookies = new Cookie[1]; + cookies[0] = new Cookie("testName", "testValue"); + + // when & then + when(request.getCookies()).thenReturn(cookies); + Assertions.assertThatThrownBy(() -> cookieResolver.extractAccessToken(request)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.AUTHENTICATION_ACCESS_TOKEN_EMPTY.getMessage()); + + } + + @DisplayName("쿠키 존재 여부 반환 성공 : 토큰 정보가 존재하면 false를 반환한다.") + @Test + void isAllTokenNotExist_returnFalse() { + // given + CookieResolver cookieResolver = new CookieResolver(); + HttpServletRequest httpServletRequest = mock(HttpServletRequest.class); + Cookie[] cookies = {new Cookie(CookieProvider.ACCESS_TOKEN_COOKIE_NAME, "test"), + new Cookie(CookieProvider.REFRESH_TOKEN_COOKIE_NAME, "test")}; + + // when + when(httpServletRequest.getCookies()).thenReturn(cookies); + boolean result = cookieResolver.isTokenEmpty(httpServletRequest); + + // then + Assertions.assertThat(result).isFalse(); + } + + @DisplayName("쿠키 존재 여부 반환 성공 : 액세스 & 리프레시 토큰 정보가 존재하지 않으면 true를 반환한다.") + @Test + void isTokenEmpty_returnTrue() { + // given + CookieResolver cookieResolver = new CookieResolver(); + HttpServletRequest httpServletRequest = mock(HttpServletRequest.class); + Cookie[] cookies = {new Cookie("test", "test"), + new Cookie("test", "test")}; + + // when + when(httpServletRequest.getCookies()).thenReturn(cookies); + boolean result = cookieResolver.isTokenEmpty(httpServletRequest); + + // then + Assertions.assertThat(result).isTrue(); + } + + @DisplayName("쿠키 존재 여부 반환 성공 : 토큰 정보가 하나라도 존재하지 않으면 false를 반환한다.") + @Test + void isTokenNotEmpty_returnFalse() { + // given + CookieResolver cookieResolver = new CookieResolver(); + HttpServletRequest httpServletRequest = mock(HttpServletRequest.class); + Cookie[] cookies = {new Cookie(CookieProvider.ACCESS_TOKEN_COOKIE_NAME, "test")}; + + // when + when(httpServletRequest.getCookies()).thenReturn(cookies); + boolean result = cookieResolver.isTokenEmpty(httpServletRequest); + + // then + Assertions.assertThat(result).isFalse(); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/auth/controller/cookie/LoginMockE2ETest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/controller/cookie/LoginMockE2ETest.java new file mode 100644 index 000000000..175cf2463 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/controller/cookie/LoginMockE2ETest.java @@ -0,0 +1,55 @@ +package com.bang_ggood.auth.controller.cookie; + +import com.bang_ggood.AcceptanceMockTestSupport; +import com.bang_ggood.auth.dto.request.OauthLoginRequest; +import com.bang_ggood.auth.dto.response.AuthTokenResponse; +import com.bang_ggood.auth.service.AuthService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import static com.bang_ggood.auth.AuthFixture.OAUTH_LOGIN_REQUEST; +import static org.mockito.ArgumentMatchers.any; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class LoginMockE2ETest extends AcceptanceMockTestSupport { + + private static final String COOKIE_DELIMITER = "="; + @Autowired + ObjectMapper objectMapper; + @MockBean + AuthService authService; + + @DisplayName("로그인 성공 : 액세스 토큰과 리프레시 토큰을 쿠키로 반환한다.") + @Test + void login() throws Exception { + // given + AuthTokenResponse authTokenResponse = AuthTokenResponse.of("accessToken", "refreshToken"); + String accessTokenCookieHeader = + CookieProvider.ACCESS_TOKEN_COOKIE_NAME + COOKIE_DELIMITER + authTokenResponse.accessToken(); + String refreshTokenCookieHeader = + CookieProvider.REFRESH_TOKEN_COOKIE_NAME + COOKIE_DELIMITER + authTokenResponse.refreshToken(); + String oauthLoginRequestJson = objectMapper.writeValueAsString(OAUTH_LOGIN_REQUEST); + + // when & then + Mockito.when(authService.oauthLogin(any(OauthLoginRequest.class))).thenReturn(authTokenResponse); + + mockMvc.perform(post("/oauth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(oauthLoginRequestJson)) + .andExpect(status().isOk()) + .andExpect(result -> { + String[] cookies = result.getResponse().getHeaders(HttpHeaders.SET_COOKIE).toArray(new String[0]); + + Assertions.assertThat(cookies[0]).contains(accessTokenCookieHeader); + Assertions.assertThat(cookies[1]).contains(refreshTokenCookieHeader); + }); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/auth/service/AuthServiceTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/service/AuthServiceTest.java new file mode 100644 index 000000000..220483ec8 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/service/AuthServiceTest.java @@ -0,0 +1,380 @@ +package com.bang_ggood.auth.service; + +import com.bang_ggood.IntegrationTestSupport; +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.service.jwt.JwtTokenProvider; +import com.bang_ggood.auth.service.oauth.OauthClient; +import com.bang_ggood.checklist.dto.response.ChecklistsPreviewResponse; +import com.bang_ggood.checklist.service.ChecklistManageService; +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import com.bang_ggood.question.dto.response.CategoryQuestionsResponse; +import com.bang_ggood.question.dto.response.CustomChecklistQuestionsResponse; +import com.bang_ggood.question.service.QuestionManageService; +import com.bang_ggood.question.service.QuestionService; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.Optional; + +import static com.bang_ggood.auth.AuthFixture.LOCAL_LOGIN_REQUEST; +import static com.bang_ggood.auth.AuthFixture.LOCAL_LOGIN_REQUEST_INVALID_EMAIL; +import static com.bang_ggood.auth.AuthFixture.LOCAL_LOGIN_REQUEST_INVALID_PASSWORD; +import static com.bang_ggood.auth.AuthFixture.OAUTH_LOGIN_REQUEST; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; + +@ExtendWith(MockitoExtension.class) +class AuthServiceTest extends IntegrationTestSupport { + + @MockBean + private OauthClient oauthClient; + @Autowired + private AuthService authService; + @Autowired + private QuestionService questionService; + @Autowired + private ChecklistManageService checklistManageService; + @Autowired + private QuestionManageService questionManageService; + @Autowired + private UserRepository userRepository; + @Autowired + private JwtTokenProvider jwtTokenProvider; + + @DisplayName("로컬 로그인 성공") + @Test + void localLogin() { + // given & when + AuthTokenResponse response = authService.localLogin(LOCAL_LOGIN_REQUEST); + + // then + assertAll( + () -> assertThat(response.accessToken()).isNotBlank(), + () -> assertThat(response.refreshToken()).isNotBlank() + ); + } + + @DisplayName("로컬 로그인 실패: 일치하는 유저가 없는 경우") + @Test + void localLogin_userNotFound() { + // given & when & then + assertThatThrownBy(() -> authService.localLogin(LOCAL_LOGIN_REQUEST_INVALID_EMAIL)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.USER_NOT_FOUND.getMessage()); + } + + @DisplayName("로컬 로그인 실패: 비밀번호가 일치하지 않는 경우") + @Test + void localLogin_userInvalidPassword() { + // given & when & then + assertThatThrownBy(() -> authService.localLogin(LOCAL_LOGIN_REQUEST_INVALID_PASSWORD)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.USER_INVALID_PASSWORD.getMessage()); + } + + @DisplayName("회원가입 성공 : 회원가입 이력 없는 회원인 경우") + @Test + void register_newUser() { + //given + RegisterRequestV1 request = new RegisterRequestV1("방방이", "bang@gmail.com", "password1234"); + + //when + Long userId = authService.register(request); + + //then + User findUser = userRepository.findById(userId).orElseThrow(); + assertThat(findUser.getId()).isEqualTo(userId); + } + + @DisplayName("회원가입 성공 : 탈퇴한 회원인 경우") + @Test + void register_deletedUser() { + // given + RegisterRequestV1 request = new RegisterRequestV1("방방이", "bang@gmail.com", "password1234"); + User existingUser = userRepository.save(request.toUserEntity()); + userRepository.deleteById(existingUser.getId()); + + // when + Long userId = authService.register(request); + + // then + User findUser = userRepository.findById(userId).orElseThrow(); + assertThat(findUser.getId()).isEqualTo(userId); + } + + @DisplayName("회원 가입 성공 : 회원 가입 시 디폴트 체크리스트 질문을 추가") + @Test + void register_default_checklist_question() { + // given + RegisterRequestV1 request = new RegisterRequestV1("방방이", "bang@gmail.com", "password1234"); + authService.register(request); + + // when + AuthTokenResponse token = authService.localLogin(new LocalLoginRequestV1("bang@gmail.com", "password1234")); + + // then + User user = authService.getAuthUser(token.accessToken()); + CustomChecklistQuestionsResponse customChecklistQuestions = questionManageService.readCustomChecklistQuestions( + user); + + int sum = 0; + for (CategoryQuestionsResponse response : customChecklistQuestions.categories()) { + sum += response.questions().size(); + } + + assertThat(sum).isEqualTo(questionService.findDefaultQuestions().size()); + } + + @DisplayName("회원 가입 성공 : 회원 가입시 디폴트 체크리스트를 추가") + @Test + void register_default_checklist() { + // given + RegisterRequestV1 request = new RegisterRequestV1("방방이", "bang@gmail.com", "password1234"); + Long userId = authService.register(request); + + // when + AuthTokenResponse token = authService.localLogin(new LocalLoginRequestV1("bang@gmail.com", "password1234")); + + // then + User user = authService.getAuthUser(token.accessToken()); + ChecklistsPreviewResponse response = checklistManageService.readAllChecklistsPreview(user); + assertThat(response.checklists()).hasSize(1); + } + + + @DisplayName("회원가입 성공 : 비밀번호 암호화") + @Test + void register_encodePassword() { + // given + String password = "password1234"; + RegisterRequestV1 request = new RegisterRequestV1("방방이", "bang@gmail.com", password); + + // when + Long userId = authService.register(request); + User findUser = userRepository.findById(userId).orElseThrow(); + String findPassword = findUser.getPassword().getValue(); + + String[] passwordParts = findPassword.split(":"); + String salt = passwordParts[1]; + + String expectedPassword = PasswordEncoder.encodeWithSpecificSalt(password, findPassword); + + // then + assertThat(findPassword).isEqualTo(expectedPassword); + } + + @DisplayName("회원가입 실패 : 이미 사용되는 이메일인 경우") + @Test + void register_emailAlreadyUsed() { + //given + RegisterRequestV1 request = new RegisterRequestV1("방방이", "bang@gmail.com", "password1234"); + + //when + authService.register(request); + + //then + assertThatThrownBy(() -> authService.register(request)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.USER_EMAIL_ALREADY_USED.getMessage()); + } + + @DisplayName("회원 탈퇴 성공") + @Test + void withdraw() { + //given + userRepository.save(UserFixture.USER1()); + + //when + authService.withdraw(UserFixture.USER1_WITH_ID()); + + //then + Optional findUser = userRepository.findById(UserFixture.USER1_WITH_ID().getId()); + assertThat(findUser).isEmpty(); + } + + @DisplayName("카카오 로그인 성공 : 존재하지 않는 회원이면 회원 가입 후 로그인") + @Test + void oauthLogin_signup() { + // given + Mockito.when(oauthClient.requestOauthInfo(any(OauthLoginRequest.class))) + .thenReturn(UserFixture.OAUTH_INFO_RESPONSE_USER2()); + + // when + AuthTokenResponse token = authService.oauthLogin(OAUTH_LOGIN_REQUEST); + + // then + assertAll( + () -> assertThat(token.accessToken()).isNotBlank(), + () -> assertThat(token.refreshToken()).isNotBlank() + ); + } + + @DisplayName("카카오 로그인 성공 : 존재하는 회원이면 로그인") + @Test + void oauthLogin() { + // given + userRepository.save(UserFixture.USER1()); + Mockito.when(oauthClient.requestOauthInfo(any(OauthLoginRequest.class))) + .thenReturn(UserFixture.OAUTH_INFO_RESPONSE_USER1()); + + // when + AuthTokenResponse token = authService.oauthLogin(OAUTH_LOGIN_REQUEST); + + // then + assertAll( + () -> assertThat(token.accessToken()).isNotBlank(), + () -> assertThat(token.refreshToken()).isNotBlank() + ); + } + + @DisplayName("카카오 로그인 성공 : 탈퇴한 회원이면 재가입") + @Test + void oauthLogin_withdrawUser() { + // given + User user = userRepository.save(UserFixture.USER1()); + userRepository.deleteById(user.getId()); + Mockito.when(oauthClient.requestOauthInfo(any(OauthLoginRequest.class))) + .thenReturn(UserFixture.OAUTH_INFO_RESPONSE_USER1()); + + // when + AuthTokenResponse token = authService.oauthLogin(OAUTH_LOGIN_REQUEST); + + // then + assertAll( + () -> assertThat(token.accessToken()).isNotBlank(), + () -> assertThat(token.refreshToken()).isNotBlank() + ); + } + + @DisplayName("카카오 로그인 성공 : 회원 가입시 디폴트 체크리스트 질문을 추가") + @Test + void oauthLogin_default_checklist_question() { + // given + Mockito.when(oauthClient.requestOauthInfo(any(OauthLoginRequest.class))) + .thenReturn(UserFixture.OAUTH_INFO_RESPONSE_USER2()); + + // when + AuthTokenResponse token = authService.oauthLogin(OAUTH_LOGIN_REQUEST); + + // then + User user = authService.getAuthUser(token.accessToken()); + CustomChecklistQuestionsResponse customChecklistQuestions = questionManageService.readCustomChecklistQuestions( + user); + + int sum = 0; + for (CategoryQuestionsResponse response : customChecklistQuestions.categories()) { + sum += response.questions().size(); + } + + assertThat(sum).isEqualTo(questionService.findDefaultQuestions().size()); + } + + @DisplayName("카카오 로그인 성공 : 회원 가입시 디폴트 체크리스트를 추가") + @Test + void oauthLogin_default_checklist() { + // given + Mockito.when(oauthClient.requestOauthInfo(any(OauthLoginRequest.class))) + .thenReturn(UserFixture.OAUTH_INFO_RESPONSE_USER2()); + + // when + AuthTokenResponse token = authService.oauthLogin(OAUTH_LOGIN_REQUEST); + + // then + User user = authService.getAuthUser(token.accessToken()); + ChecklistsPreviewResponse response = checklistManageService.readAllChecklistsPreview(user); + assertThat(response.checklists()).hasSize(1); + } + + @DisplayName("로그아웃 성공") + @Test + void logout() { + // given + User user = userRepository.save(UserFixture.USER1()); + String accessToken = jwtTokenProvider.createAccessToken(user); + String refreshToken = jwtTokenProvider.createRefreshToken(user); + + // when & then + assertThatCode(() -> authService.logout(accessToken, refreshToken)) + .doesNotThrowAnyException(); + } + + @DisplayName("로그아웃 실패 : accessToken 유저와 refreshToken 유저가 다른 경우") + @Test + void logout_userMismatch_exception() { + // given + User user1 = userRepository.save(UserFixture.USER1()); + User user2 = userRepository.save(UserFixture.USER2()); + String accessToken = jwtTokenProvider.createAccessToken(user1); + String refreshToken = jwtTokenProvider.createRefreshToken(user2); + + // when & then + assertThatThrownBy(() -> authService.logout(accessToken, refreshToken)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.AUTHENTICATION_TOKEN_USER_MISMATCH.getMessage()); + } + + @DisplayName("게스트 유저 할당 실패 : 게스트 유저의 수가 2명이면 예외를 발생") + @Test + void assignGuestUser_UnexpectedGuestUserExist() { + // given + userRepository.save(UserFixture.GUEST_USER1()); + userRepository.save(UserFixture.GUEST_USER2()); + + // when & then + assertThatThrownBy(() -> authService.assignGuestUser()) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.GUEST_USER_UNEXPECTED_EXIST.getMessage()); + } + + @DisplayName("게스트 유저 할당 실패 : 게스트 유저가 존재하지 않으면 예외를 발생") + @Test + void assingGuestUser_GuestUserNotExist() { + // when & then + assertThatThrownBy(() -> authService.assignGuestUser()) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.GUEST_USER_NOT_FOUND.getMessage()); + } + + @DisplayName("게스트 유저 할당 성공") + @Test + void assignGuestUser() { + // given + User guestUser = userRepository.save(UserFixture.GUEST_USER1()); + + // when + User assignedGuestUser = authService.assignGuestUser(); + + // then + assertThat(assignedGuestUser).isEqualTo(guestUser); + } + + @DisplayName("액세스 토큰 재발행 성공") + @Test + void reissueAccessToken() { + // given + userRepository.save(UserFixture.USER1()); + Mockito.when(oauthClient.requestOauthInfo(any(OauthLoginRequest.class))) + .thenReturn(UserFixture.OAUTH_INFO_RESPONSE_USER1()); + AuthTokenResponse tokenResponse = authService.oauthLogin(OAUTH_LOGIN_REQUEST); + + // when & then + assertThatCode(() -> authService.reissueAccessToken(tokenResponse.refreshToken())) + .doesNotThrowAnyException(); + + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/auth/service/JwtTokenProviderTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/service/JwtTokenProviderTest.java new file mode 100644 index 000000000..86abb435e --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/service/JwtTokenProviderTest.java @@ -0,0 +1,66 @@ +package com.bang_ggood.auth.service; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.auth.JwtTokenFixture; +import com.bang_ggood.auth.service.jwt.JwtTokenProvider; +import com.bang_ggood.auth.service.jwt.JwtTokenResolver; +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class JwtTokenProviderTest extends IntegrationTestSupport { + + @Autowired + private JwtTokenResolver jwtTokenResolver; + @Autowired + private JwtTokenProvider jwtTokenProvider; + @Autowired + private UserRepository userRepository; + + @DisplayName("토큰 생성 성공") + @Test + void createToken() { + // given + User user = userRepository.getUserById(1L); + String token = jwtTokenProvider.createAccessToken(user); + + // when + AuthUser authUser = jwtTokenResolver.resolveAccessToken(token); + + // then + Assertions.assertThat(authUser.id()).isEqualTo(user.getId()); + } + + @DisplayName("토큰 확인 실패 : 시그니처가 잘못된 경우") + @Test + void resolveToken_invalidSignature_exception() { + // given + JwtTokenProvider invalidJwtTokenProvider = JwtTokenFixture.JWT_TOKEN_PROVIDER_WITH_INVALID_KEY(); + + User user = userRepository.save(UserFixture.USER1()); + String token = invalidJwtTokenProvider.createAccessToken(user); + + // when & then + Assertions.assertThatCode(() -> jwtTokenResolver.resolveAccessToken(token)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.AUTHENTICATION_TOKEN_INVALID.getMessage()); + } + + @DisplayName("토큰 확인 실패 : 토큰 형태가 잘못된 경우") + @Test + void resolveToken_invalidToken_exception() { + // given + String invalidToken = "malformed"; + + // when & then + Assertions.assertThatCode(() -> jwtTokenResolver.resolveAccessToken(invalidToken)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.AUTHENTICATION_TOKEN_INVALID.getMessage()); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/auth/service/OauthPropertiesTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/service/OauthPropertiesTest.java new file mode 100644 index 000000000..c2cf69b0b --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/service/OauthPropertiesTest.java @@ -0,0 +1,59 @@ +package com.bang_ggood.auth.service; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.auth.AuthFixture; +import com.bang_ggood.auth.dto.request.OauthLoginRequest; +import com.bang_ggood.auth.service.oauth.OauthRequestProperties; +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class OauthPropertiesTest extends IntegrationTestSupport { + + @DisplayName("토큰 요청 바디 생성 실패 : Redirect Uri이 일치하지 않을 때") + @Test + void createTokenRequestBodyFail_whenRedirectUriMisMatch() { + // given + OauthRequestProperties oauthRequestProperties = AuthFixture.OAUTH_REQUEST_PROPERTIES(); + + String invalidRedirectUri = AuthFixture.INVALID_REGISTERED_REDIRECT_URI; + OauthLoginRequest oauthLoginRequest = new OauthLoginRequest("testCode", invalidRedirectUri); + + // when & then + Assertions.assertThatThrownBy(() -> oauthRequestProperties.createTokenRequestBody(oauthLoginRequest)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.OAUTH_REDIRECT_URI_MISMATCH.getMessage()); + } + + @DisplayName("토큰 요청 바디 생성 성공") + @Test + void createTokenRequestBody() { + // given + OauthRequestProperties oauthRequestProperties = AuthFixture.OAUTH_REQUEST_PROPERTIES(); + String redirectUris = AuthFixture.REGISTERED_REDIRECT_URIS; + OauthLoginRequest oauthLoginRequest = new OauthLoginRequest("testCode", "localhost:3000"); + + // when & then + Assertions.assertThatCode(() -> oauthRequestProperties.createTokenRequestBody(oauthLoginRequest)) + .doesNotThrowAnyException(); + } + + @DisplayName("Redirect Uri 여러개를 받아 저장한다.") + @Test + void convertToList() { + // given + String firstRedirectUri = "localhost:3000"; + String secondRedirectUri = "localhost:3001"; + String testRegisteredUris = firstRedirectUri + ", " + secondRedirectUri; + + OauthRequestProperties oauthRequestProperties = new OauthRequestProperties( + "testPostUri", "testUserUri", "testGrantType", + "testClientId", testRegisteredUris, "testClientSecret"); + + // when & then + Assertions.assertThat(oauthRequestProperties.getRegisteredRedirectUris()) + .containsExactlyInAnyOrder(firstRedirectUri, secondRedirectUri); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/auth/service/PasswordEncoderTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/service/PasswordEncoderTest.java new file mode 100644 index 000000000..35cd297df --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/auth/service/PasswordEncoderTest.java @@ -0,0 +1,34 @@ +package com.bang_ggood.auth.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PasswordEncoderTest { + + @DisplayName("고유 salt로 비밀번호 암호화 성공") + @Test + void encodeWithGeneralSalt() { + // given & when + String password = "bang-ggood"; + String encodedPassword = PasswordEncoder.encodeWithGeneralSalt("bang-ggood"); + + // then + assertThat(password).isNotEqualTo(encodedPassword); + } + + @DisplayName("특정 salt로 비밀번호 암호화 성공") + @Test + void encodeWithSpecificSalt() { + // given + String password = "bang-ggood"; + String encodedPassword = PasswordEncoder.encodeWithGeneralSalt(password); + + // when + String targetPassword = PasswordEncoder.encodeWithSpecificSalt(password, encodedPassword); + + // then + assertThat(targetPassword).isEqualTo(encodedPassword); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/ChecklistFixture.java b/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/ChecklistFixture.java new file mode 100644 index 000000000..1d8c9a6f3 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/ChecklistFixture.java @@ -0,0 +1,219 @@ +package com.bang_ggood.checklist; + +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.contract.domain.OccupancyMonth; +import com.bang_ggood.contract.domain.OccupancyPeriod; +import com.bang_ggood.like.domain.ChecklistLike; +import com.bang_ggood.maintenance.domain.ChecklistMaintenance; +import com.bang_ggood.maintenance.domain.MaintenanceItem; +import com.bang_ggood.option.domain.Option; +import com.bang_ggood.question.QuestionFixture; +import com.bang_ggood.question.dto.request.QuestionRequest; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.station.dto.request.ChecklistStationRequest; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import java.util.List; + +public class ChecklistFixture { + + public static Checklist CHECKLIST1_USER1(Room room, User user) { + return new Checklist( + room, + user, + 1000, 50, 5, 12, + OccupancyMonth.OCTOBER, OccupancyPeriod.EARLY, + "방끗공인중개사", "메모", "한줄평" + ); + } + + public static Checklist CHECKLIST1_USER1_UPDATE(Room room, User user) { + return new Checklist( + room, + user, + 1000, 50, 5, 12, + OccupancyMonth.OCTOBER, OccupancyPeriod.EARLY, + "방끗공인중개", "메모 추가", "한줄평 수정" + ); + } + + public static Checklist CHECKLIST2_USER1(Room room, User user) { + return new Checklist( + room, + user, + 1000, 50, 5, 12, + OccupancyMonth.OCTOBER, OccupancyPeriod.EARLY, + "방끗공인중개사", "메모", "한줄평" + ); + } + + public static Checklist CHECKLIST3_USER1(Room room, User user) { + return new Checklist( + room, + user, + 1000, 50, 5, 12, + OccupancyMonth.OCTOBER, OccupancyPeriod.EARLY, + "방끗공인중개사", "메모", "한줄평" + ); + } + + public static Checklist CHECKLIST3_USER2(Room room, User user) { + return new Checklist( + room, + user, + 1000, 50, 5, 12, + OccupancyMonth.OCTOBER, OccupancyPeriod.EARLY, + "방끗공인중개사", "메모", "한줄평" + ); + } + + public static Checklist CHECKLIST1_WITH_USER1_ID(Room room) { + return new Checklist( + room, + UserFixture.USER1_WITH_ID(), + 1000, 50, 5, 12, + OccupancyMonth.OCTOBER, OccupancyPeriod.EARLY, + "방끗공인중개사", "메모", "한줄평" + ); + } + + public static QuestionRequest QUESTION_1_CREATE_REQUEST() { + return new QuestionRequest(QuestionFixture.QUESTION1_CATEGORY1.getId(), "GOOD"); + } + + public static QuestionRequest QUESTION_2_CREATE_REQUEST() { + return new QuestionRequest(QuestionFixture.QUESTION2_CATEGORY1.getId(), "GOOD"); + } + + public static QuestionRequest QUESTION_3_CREATE_REQUEST() { + return new QuestionRequest(QuestionFixture.QUESTION3_CATEGORY2.getId(), "BAD"); + } + + public static QuestionRequest QUESTION_4_CREATE_REQUEST() { + return new QuestionRequest(QuestionFixture.QUESTION4_CATEGORY2.getId(), "BAD"); + } + + public static QuestionRequest QUESTION_5_CREATE_REQUEST() { + return new QuestionRequest(QuestionFixture.QUESTION5_CATEGORY2.getId(), "GOOD"); + } + + public static QuestionRequest QUESTION_5_UPDATE_REQUEST() { + return new QuestionRequest(QuestionFixture.QUESTION5_CATEGORY2.getId(), "GOOD"); + } + + public static QuestionRequest QUESTION_CREATE_REQUEST_NO_ID() { + return new QuestionRequest(null, "NONE"); + } + + public static ChecklistRequest CHECKLIST_CREATE_REQUEST() { + return new ChecklistRequest( + RoomFixture.ROOM_CREATE_REQUEST(), + List.of(Option.REFRIGERATOR.getId(), Option.SINK.getId(), Option.INDUCTION.getId(), + Option.SHOE_RACK.getId()), + List.of(QUESTION_1_CREATE_REQUEST(), QUESTION_2_CREATE_REQUEST(), + QUESTION_3_CREATE_REQUEST(), QUESTION_5_CREATE_REQUEST()) + ); + } + + public static ChecklistRequestV1 CHECKLIST_CREATE_REQUEST_V1() { + return new ChecklistRequestV1( + RoomFixture.ROOM_CREATE_REQUEST(), + List.of(Option.REFRIGERATOR.getId(), Option.SINK.getId(), Option.INDUCTION.getId(), + Option.SHOE_RACK.getId()), + List.of(QUESTION_1_CREATE_REQUEST(), QUESTION_2_CREATE_REQUEST(), + QUESTION_3_CREATE_REQUEST(), QUESTION_5_CREATE_REQUEST()), + ChecklistStationRequest.of(38, 127) + ); + } + + public static ChecklistRequest CHECKLIST_CREATE_REQUEST2() { + return new ChecklistRequest( + RoomFixture.ROOM_CREATE_REQUEST(), + List.of(Option.REFRIGERATOR.getId(), Option.SINK.getId(), Option.INDUCTION.getId(), + Option.CLOSET.getId()), + List.of(QUESTION_1_CREATE_REQUEST(), QUESTION_2_CREATE_REQUEST(), + QUESTION_4_CREATE_REQUEST()) + ); + } + + public static ChecklistRequest CHECKLIST_CREATE_REQUEST_NO_ROOM_NAME() { + return new ChecklistRequest( + RoomFixture.ROOM_CREATE_REQUEST_NO_ROOM_NAME(), + List.of(Option.REFRIGERATOR.getId(), Option.SINK.getId(), Option.INDUCTION.getId(), + Option.SHOE_RACK.getId()), + List.of(QUESTION_1_CREATE_REQUEST(), QUESTION_2_CREATE_REQUEST(), + QUESTION_3_CREATE_REQUEST(), QUESTION_5_CREATE_REQUEST()) + ); + } + + public static ChecklistRequest CHECKLIST_UPDATE_REQUEST() { + return new ChecklistRequest( + RoomFixture.ROOM_UPDATE_REQUEST(), List.of(1, 2, 3, 4), + List.of(QUESTION_1_CREATE_REQUEST(), QUESTION_2_CREATE_REQUEST(), + QUESTION_3_CREATE_REQUEST(), QUESTION_5_UPDATE_REQUEST()) + ); + } + + public static ChecklistRequestV1 CHECKLIST_UPDATE_REQUEST_V1() { + return new ChecklistRequestV1( + RoomFixture.ROOM_UPDATE_REQUEST(), List.of(Option.REFRIGERATOR.getId(), Option.INDUCTION.getId(), + Option.BED.getId(), Option.WASHING_MACHINE.getId()), + List.of(QUESTION_1_CREATE_REQUEST(), QUESTION_2_CREATE_REQUEST(), + QUESTION_3_CREATE_REQUEST(), QUESTION_5_UPDATE_REQUEST()), + ChecklistStationRequest.of(37.5, 127.1) + ); + } + + public static ChecklistRequest CHECKLIST_UPDATE_REQUEST_NO_ROOM_NAME() { + return new ChecklistRequest( + RoomFixture.ROOM_CREATE_REQUEST_NO_ROOM_NAME(), + List.of(Option.REFRIGERATOR.getId(), Option.BED.getId(), Option.INDUCTION.getId(), + Option.SHOE_RACK.getId()), + List.of(QUESTION_1_CREATE_REQUEST(), QUESTION_2_CREATE_REQUEST(), + QUESTION_3_CREATE_REQUEST(), QUESTION_5_UPDATE_REQUEST()) + ); + } + + public static ChecklistRequest CHECKLIST_CREATE_REQUEST_NO_QUESTION_ID() { + return new ChecklistRequest( + RoomFixture.ROOM_CREATE_REQUEST(), + List.of(Option.REFRIGERATOR.getId(), Option.SINK.getId(), Option.INDUCTION.getId(), + Option.SHOE_RACK.getId()), + List.of(QUESTION_1_CREATE_REQUEST(), QUESTION_2_CREATE_REQUEST(), + QUESTION_3_CREATE_REQUEST(), QUESTION_CREATE_REQUEST_NO_ID()) + ); + } + + public static ChecklistRequest CHECKLIST_UPDATE_REQUEST_NO_QUESTION_ID() { + return new ChecklistRequest( + RoomFixture.ROOM_UPDATE_REQUEST(), + List.of(Option.REFRIGERATOR.getId(), Option.BED.getId(), Option.INDUCTION.getId(), + Option.SHOE_RACK.getId()), + List.of(QUESTION_1_CREATE_REQUEST(), QUESTION_2_CREATE_REQUEST(), + QUESTION_3_CREATE_REQUEST(), QUESTION_CREATE_REQUEST_NO_ID()) + ); + } + + public static ChecklistLike CHECKLIST1_LIKE(Checklist checklist) { + return new ChecklistLike(checklist); + } + + public static ChecklistLike CHECKLIST2_LIKE(Checklist checklist) { + return new ChecklistLike(checklist); + } + + public static ChecklistMaintenance CHECKLIST1_INCLUDED_MAINTENANCE_1(Checklist checklist) { + return new ChecklistMaintenance( + checklist, MaintenanceItem.ELECTRICITY + ); + } + + public static ChecklistMaintenance CHECKLIST1_INCLUDED_MAINTENANCE_2(Checklist checklist) { + return new ChecklistMaintenance( + checklist, MaintenanceItem.GAS + ); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/controller/ChecklistE2ETest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/controller/ChecklistE2ETest.java new file mode 100644 index 000000000..e95a95d8a --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/controller/ChecklistE2ETest.java @@ -0,0 +1,218 @@ +package com.bang_ggood.checklist.controller; + +import com.bang_ggood.AcceptanceTest; +import com.bang_ggood.checklist.ChecklistFixture; +import com.bang_ggood.checklist.domain.Checklist; +import com.bang_ggood.checklist.repository.ChecklistRepository; +import com.bang_ggood.checklist.service.ChecklistManageService; +import com.bang_ggood.checklist.service.ChecklistService; +import com.bang_ggood.like.repository.ChecklistLikeRepository; +import com.bang_ggood.question.CustomChecklistFixture; +import com.bang_ggood.question.repository.CustomChecklistQuestionRepository; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.repository.RoomRepository; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.hamcrest.Matchers.containsString; + +class ChecklistE2ETest extends AcceptanceTest { + + @Autowired + private ChecklistManageService checklistManageService; + @Autowired + private ChecklistRepository checklistRepository; + @Autowired + private RoomRepository roomRepository; + @Autowired + private CustomChecklistQuestionRepository customChecklistQuestionRepository; + + @DisplayName("체크리스트 작성 성공") + @Test + void createChecklist() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .body(ChecklistFixture.CHECKLIST_CREATE_REQUEST()) + .when().post("/checklists") + .then().log().all() + .statusCode(201); + } + + @DisplayName("체크리스트 작성 v1 성공") + @Test + void createChecklistV1() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .body(ChecklistFixture.CHECKLIST_CREATE_REQUEST_V1()) + .when().post("v1/checklists") + .then().log().all() + .statusCode(201); + } + + @DisplayName("체크리스트 작성 실패: 방 이름을 넣지 않은 경우") + @Test + void createChecklist_noRoomName_exception() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .body(ChecklistFixture.CHECKLIST_CREATE_REQUEST_NO_ROOM_NAME()) + .when().post("/checklists") + .then().log().all() + .statusCode(400) + .body("message", containsString("방 이름이 존재하지 않습니다.")); + } + + @DisplayName("체크리스트 작성 실패: 질문 ID를 넣지 않은 경우") + @Test + void createChecklist_noQuestionId_exception() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .body(ChecklistFixture.CHECKLIST_CREATE_REQUEST_NO_QUESTION_ID()) + .when().post("/checklists") + .then().log().all() + .statusCode(400) + .body("message", containsString("질문 아이디가 존재하지 않습니다.")); + } + + @DisplayName("체크리스트 질문 조회 성공") + @Test + void readChecklistQuestions() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .when().get("/checklists/questions") + .then().log().all() + .statusCode(200); + } + + @DisplayName("작성된 체크리스트 조회 성공") + @Test + void readChecklistById() { + long checklistId = checklistManageService.createChecklist(this.getAuthenticatedUser(), + ChecklistFixture.CHECKLIST_CREATE_REQUEST()); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .when().get("/checklists/" + checklistId) + .then().log().all() + .statusCode(200); + /*.extract() + .as(SelectedChecklistResponse.class); + + Assertions.assertAll( + () -> assertThat(selectedChecklistResponse.room().roomName()).isEqualTo(ChecklistFixture.CHECKLIST_CREATE_REQUEST().room().roomName()), + () -> assertThat(selectedChecklistResponse.room().address()).isEqualTo(ChecklistFixture.CHECKLIST_CREATE_REQUEST().room().address()) + );*/ + //TODO 수정 + } + + @DisplayName("작성된 체크리스트 조회 v1 성공") + @Test + void readChecklistV1() { + long checklistId = checklistManageService.createChecklist(this.getAuthenticatedUser(), + ChecklistFixture.CHECKLIST_CREATE_REQUEST()); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .when().get("v1/checklists/" + checklistId) + .then().log().all() + .statusCode(200); + } + + @DisplayName("좋아요된 체크리스트 리스트 조회 성공") + @Test + void readLikedUserChecklistsPreview() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .when().get("/checklists/like") + .then().log().all() + .statusCode(200); + } + + @DisplayName("체크리스트 수정 성공") + @Test + void updateChecklist() { + long checklistId = checklistManageService.createChecklist(this.getAuthenticatedUser(), + ChecklistFixture.CHECKLIST_CREATE_REQUEST()); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .body(ChecklistFixture.CHECKLIST_UPDATE_REQUEST()) + .when().put("/checklists/" + checklistId) + .then().log().all() + .statusCode(204); + } + + @DisplayName("체크리스트 수정 v1 성공") + @Test + void updateChecklistV1() { + long checklistId = checklistManageService.createChecklistV1(this.getAuthenticatedUser(), + ChecklistFixture.CHECKLIST_CREATE_REQUEST_V1()); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .body(ChecklistFixture.CHECKLIST_UPDATE_REQUEST_V1()) + .when().put("/v1/checklists/" + checklistId) + .then().log().all() + .statusCode(204); + } + + @DisplayName("체크리스트 수정 실패: 방 이름을 넣지 않은 경우") + @Test + void updateChecklist_noRoomName_exception() { + long checklistId = checklistManageService.createChecklist(this.getAuthenticatedUser(), + ChecklistFixture.CHECKLIST_CREATE_REQUEST()); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .body(ChecklistFixture.CHECKLIST_UPDATE_REQUEST_NO_ROOM_NAME()) + .when().put("/checklists/" + checklistId) + .then().log().all() + .statusCode(400) + .body("message", containsString("방 이름이 존재하지 않습니다.")); + } + + @DisplayName("체크리스트 수정 실패: 질문 ID를 넣지 않은 경우") + @Test + void updateChecklist_noQuestionId_exception() { + long checklistId = checklistManageService.createChecklist(this.getAuthenticatedUser(), + ChecklistFixture.CHECKLIST_CREATE_REQUEST()); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .body(ChecklistFixture.CHECKLIST_UPDATE_REQUEST_NO_QUESTION_ID()) + .when().put("/checklists/" + checklistId) + .then().log().all() + .statusCode(400) + .body("message", containsString("질문 아이디가 존재하지 않습니다.")); + } + + @DisplayName("체크리스트 삭제 성공") + @Test + void deleteChecklistById() { + Room room = roomRepository.save(RoomFixture.ROOM_1()); + Checklist saved = checklistRepository.save( + ChecklistFixture.CHECKLIST1_USER1(room, this.getAuthenticatedUser())); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .when().delete("/checklists/" + saved.getId()) + .then().log().all() + .statusCode(204); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/domain/ChecklistTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/domain/ChecklistTest.java new file mode 100644 index 000000000..afa0eef08 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/domain/ChecklistTest.java @@ -0,0 +1,55 @@ +package com.bang_ggood.checklist.domain; + +import com.bang_ggood.checklist.ChecklistFixture; +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.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.user.UserFixture; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ChecklistTest { + + @DisplayName("체크리스트 생성 실패 : 메모가 1000자 넘을 경우") + @Test + void constructor_ChecklistInvalidMemoLength_exception() { + // given + String memo = "a".repeat(1001); + + // when & then + assertThatThrownBy( + () -> new Checklist(RoomFixture.ROOM_1(), UserFixture.USER1(), 1000, 20, 10, 12, + OccupancyMonth.OCTOBER, OccupancyPeriod.EARLY, "공인중개사", memo, "요약") + ) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.CHECKLIST_MEMO_INVALID_LENGTH.getMessage()); + } + + @DisplayName("체크리스트가 유저가 작성한 것인지 판별 성공 : 유저가 작성한 경우") + @Test + void isOwnedBy_true() { + //given + Room room = RoomFixture.ROOM_1(); + Checklist checklist = ChecklistFixture.CHECKLIST1_WITH_USER1_ID(room); + + //when & then + Assertions.assertThat(checklist.isOwnedBy(UserFixture.USER1_WITH_ID())).isTrue(); + } + + @DisplayName("체크리스트가 유저가 작성한 것인지 판별 성공 : 유저가 작성하지 않은 경우") + @Test + void isOwnedBy_false() { + //given + Room room = RoomFixture.ROOM_1(); + Checklist checklist = ChecklistFixture.CHECKLIST1_WITH_USER1_ID(room); + + //when & then + Assertions.assertThat(checklist.isOwnedBy(UserFixture.USER2_WITH_ID())).isFalse(); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/repository/ChecklistRepositoryTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/repository/ChecklistRepositoryTest.java new file mode 100644 index 000000000..4caf5ce66 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/repository/ChecklistRepositoryTest.java @@ -0,0 +1,196 @@ +package com.bang_ggood.checklist.repository; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.checklist.ChecklistFixture; +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.like.repository.ChecklistLikeRepository; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.repository.RoomRepository; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + + +class ChecklistRepositoryTest extends IntegrationTestSupport { + + @Autowired + private ChecklistRepository checklistRepository; + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ChecklistLikeRepository checklistLikeRepository; + + @DisplayName("아이디를 통해 체크리스트 갖고 오기 성공") + @Test + void findById() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist savedChecklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + + //when + Checklist foundChecklist = checklistRepository.getById(savedChecklist.getId()); + + //then + assertThat(foundChecklist.getId()).isEqualTo(savedChecklist.getId()); + } + + @DisplayName("아이디를 통해 체크리스트 갖고 오기 실패 : 해당하는 체크리스트가 없을 경우") + @Test + void findById_notFound_exception() { + assertThatThrownBy(() -> checklistRepository.getById(1L)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.CHECKLIST_NOT_FOUND.getMessage()); + } + + @DisplayName("체크리스트 아이디 리스트 중 유저가 생성한 체크리스트 목록 갖고 오기 성공") + @Test + void findByUserAndIdIn() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist savedChecklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + + assertThat(checklistRepository.findByUserAndIdIn(user, List.of(savedChecklist.getId()))) + .isEqualTo(List.of(savedChecklist)); + } + + @Transactional + @DisplayName("체크리스트 리스트 조회 성공 : 논리적 삭제를 적용해서 체크리스트 리스트를 조회한다.") + @Test + void findAllByUser() { + // given + Room room1 = roomRepository.save(RoomFixture.ROOM_1()); + Room room2 = roomRepository.save(RoomFixture.ROOM_2()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist1 = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room1, user)); + Checklist checklist2 = checklistRepository.save(ChecklistFixture.CHECKLIST2_USER1(room2, user)); + checklistRepository.saveAll(List.of(checklist1, checklist2)); + + checklistRepository.deleteById(checklist1.getId()); + + // when + List checklists = checklistRepository.findAllByUserOrderByLatest(user); + + // then + Assertions.assertThat(checklists).containsOnly(checklist2); + } + + @DisplayName("체크리스트 리스트 조회 성공 : 체크리스트를 최신순으로 조회한다.") + @Test + void findAllByUser_OrderByLatest() { + // given + Room room1 = roomRepository.save(RoomFixture.ROOM_1()); + Room room2 = roomRepository.save(RoomFixture.ROOM_2()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist1 = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room1, user)); + checklistRepository.save(checklist1); + + Checklist checklist2 = checklistRepository.save(ChecklistFixture.CHECKLIST2_USER1(room2, user)); + checklistRepository.save(checklist2); + + // when + List checklists = checklistRepository.findAllByUserOrderByLatest(user); + + // then + Assertions.assertThat(checklists).containsExactly(checklist2, checklist1); + } + + @Transactional + @DisplayName("체크리스트 리스트 조회 성공 : 사용자의 체크리스트만 조회한다.") + @Test + void findAllByUser_OnlyUserChecklists() { + // given + Room room1 = roomRepository.save(RoomFixture.ROOM_1()); + Room room2 = roomRepository.save(RoomFixture.ROOM_2()); + User user1 = userRepository.save(UserFixture.USER1()); + Checklist checklist1 = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room1, user1)); + Checklist checklist2 = checklistRepository.save(ChecklistFixture.CHECKLIST2_USER1(room2, user1)); + + User user2 = userRepository.save(UserFixture.USER2()); + Room room3 = roomRepository.save(RoomFixture.ROOM_3()); + Checklist checklist3 = ChecklistFixture.CHECKLIST3_USER2(room3, user2); + + checklistRepository.saveAll(List.of(checklist1, checklist2, checklist3)); + + // when + List checklists = checklistRepository.findAllByUserOrderByLatest(user1); + + // then + Assertions.assertThat(checklists).containsOnly(checklist1, checklist2); + } + + @Transactional + @DisplayName("체크리스트 좋아요 필터링 조회 성공") + @Test + void findAllByUserAndIsLiked() { + // given + Room room1 = roomRepository.save(RoomFixture.ROOM_1()); + Room room2 = roomRepository.save(RoomFixture.ROOM_2()); + Room room3 = roomRepository.save(RoomFixture.ROOM_3()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist1 = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room1, user)); + Checklist checklist2 = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room2, user)); + Checklist checklist3 = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room3, user)); + checklistLikeRepository.save(ChecklistFixture.CHECKLIST1_LIKE(checklist1)); + checklistLikeRepository.save(ChecklistFixture.CHECKLIST2_LIKE(checklist2)); + + // when + List checklists = checklistRepository.findAllByUserAndIsLiked(user); + + // then + Assertions.assertThat(checklists) + .containsOnly(checklist1, checklist2); + } + + @DisplayName("아이디를 통해 체크리스트 존재 확인 성공 : 존재하는 경우") + @Test + void existsById_true() { + //given & when + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist savedChecklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + + //then + assertThat(checklistRepository.existsById(savedChecklist.getId().longValue())).isTrue(); + } + + @DisplayName("아이디를 통해 체크리스트 존재 확인 성공 : 존재하지 않는 경우") + @Test + void existsById_false() { + //given & when & then + assertThat(checklistRepository.existsById(1L)).isFalse(); + } + + @DisplayName("체크리스트 삭제 성공") + @Test + void deleteById() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist savedChecklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + + //when + checklistRepository.deleteById(savedChecklist.getId()); + + //then + assertThat(checklistRepository.existsById(savedChecklist.getId())).isFalse(); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/service/ChecklistManageServiceTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/service/ChecklistManageServiceTest.java new file mode 100644 index 000000000..cf0591933 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/service/ChecklistManageServiceTest.java @@ -0,0 +1,241 @@ +package com.bang_ggood.checklist.service; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.checklist.ChecklistFixture; +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.ChecklistsPreviewResponse; +import com.bang_ggood.checklist.dto.response.SelectedChecklistResponse; +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.like.repository.ChecklistLikeRepository; +import com.bang_ggood.like.service.ChecklistLikeManageService; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.domain.Structure; +import com.bang_ggood.room.repository.RoomRepository; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +class ChecklistManageServiceTest extends IntegrationTestSupport { + + @Autowired + private ChecklistManageService checklistManageService; + @Autowired + private ChecklistLikeManageService checklistLikeManageService; + @Autowired + private RoomRepository roomRepository; + @Autowired + private ChecklistRepository checklistRepository; + @Autowired + private UserRepository userRepository; + @Autowired + private ChecklistLikeRepository checklistLikeRepository; + + @DisplayName("체크리스트 작성 성공") + @Test + void createChecklist() { + //given + User user = userRepository.save(UserFixture.USER1()); + ChecklistRequest checklist = ChecklistFixture.CHECKLIST_CREATE_REQUEST(); + + // when + long checklistId = checklistManageService.createChecklist(user, checklist); + + //then + assertThat(checklistId).isGreaterThan(0); + } + + @DisplayName("체크리스트 작성 v1 성공") + @Test + void createChecklistV1() { + //given + User user = userRepository.save(UserFixture.USER1()); + ChecklistRequestV1 checklistRequestV1 = ChecklistFixture.CHECKLIST_CREATE_REQUEST_V1(); + + // when + long checklistId = checklistManageService.createChecklistV1(user, checklistRequestV1); + + //then + assertThat(checklistId).isGreaterThan(0); + } + + @DisplayName("작성된 체크리스트 조회 성공") + @Test + void readChecklist() { + // given & when + User user = userRepository.save(UserFixture.USER1()); + Room room = roomRepository.save(RoomFixture.ROOM_1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + SelectedChecklistResponse selectedChecklistResponse = checklistManageService + .readChecklist(user, checklist.getId()); + + // then + assertAll( + () -> assertThat(selectedChecklistResponse.room().roomName()).isEqualTo(room.getName()), + () -> assertThat(selectedChecklistResponse.room().address()).isEqualTo(room.getAddress()) + ); + } + + @DisplayName("작성된 체크리스트 조회 성공 : 좋아요 여부를 true로 반환한다.") + @Test + void readChecklist_returnIsLikedTrue() { + // given + User user = userRepository.save(UserFixture.USER1()); + Room room = roomRepository.save(RoomFixture.ROOM_1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + checklistLikeManageService.createLike(user, checklist.getId()); + + // when + SelectedChecklistResponse selectedChecklistResponse = checklistManageService + .readChecklist(user, checklist.getId()); + + // then + assertThat(selectedChecklistResponse.isLiked()).isTrue(); + } + + @DisplayName("작성된 체크리스트 조회 성공 : 좋아요 여부를 false로 반환한다.") + @Test + void readChecklist_returnIsLikedFalse() { + // given + User user = userRepository.save(UserFixture.USER1()); + Room room = roomRepository.save(RoomFixture.ROOM_1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + + // when + SelectedChecklistResponse selectedChecklistResponse = checklistManageService + .readChecklist(user, checklist.getId()); + + // then + assertThat(selectedChecklistResponse.isLiked()).isFalse(); + } + + + @DisplayName("작성된 체크리스트 조회 실패 : 체크리스트가 존재하지 않는 id인 경우") + @Test + void readChecklistById_invalidChecklistId_exception() { + // given + User user = userRepository.save(UserFixture.USER1()); + + //when & then + assertThatThrownBy(() -> checklistManageService.readChecklist(user, 0L)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.CHECKLIST_NOT_FOUND.getMessage()); + } + + @DisplayName("체크리스트 삭제 성공") + @Test + void deleteChecklistById() { + // given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + + // when + checklistManageService.deleteChecklistById(user, checklist.getId()); + + // then + assertThat(checklistRepository.existsById(checklist.getId().longValue())).isFalse(); + } + + @DisplayName("체크리스트 삭제 실패 : 체크리스트 작성 유저와 삭제하려는 유저가 다른 경우") + @Test + void deleteChecklistById_notOwnedByUser_exception() { + // given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user1 = userRepository.save(UserFixture.USER1()); + User user2 = userRepository.save(UserFixture.USER2()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user1)); + + // when & then + assertThatThrownBy( + () -> checklistManageService.deleteChecklistById(user2, checklist.getId()) + ) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.CHECKLIST_NOT_OWNED_BY_USER.getMessage()); + } + + @DisplayName("체크리스트 리스트 조회 성공") + @Test + void readUserChecklistsPreview() { + // given + User user = userRepository.save(UserFixture.USER1()); + ChecklistRequest checklistRequest1 = ChecklistFixture.CHECKLIST_CREATE_REQUEST(); + ChecklistRequest checklistRequest2 = ChecklistFixture.CHECKLIST_CREATE_REQUEST2(); + + Long checklistId1 = checklistManageService.createChecklist(user, checklistRequest1); + Long checklistId2 = checklistManageService.createChecklist(user, checklistRequest2); + + // when + ChecklistsPreviewResponse response = checklistManageService.readAllChecklistsPreview(user); + + // then + ChecklistPreviewResponse previewResponse1 = response.checklists().get(0); + ChecklistPreviewResponse previewResponse2 = response.checklists().get(1); + + assertThat(previewResponse1.checklistId()).isEqualTo(checklistId2); // 최신순으로 조회 + assertThat(previewResponse2.checklistId()).isEqualTo(checklistId1); + } + + @DisplayName("좋아요된 체크리스트 리스트 최신순으로 조회 성공") + @Test + void readLikedChecklistsPreview() { + //given + int EXPECTED_LIKE_COUNT = 2; + User user = userRepository.save(UserFixture.USER1()); + Room room1 = roomRepository.save(RoomFixture.ROOM_1()); + Room room2 = roomRepository.save(RoomFixture.ROOM_2()); + Room room3 = roomRepository.save(RoomFixture.ROOM_3()); + Checklist checklist1 = ChecklistFixture.CHECKLIST1_USER1(room1, user); + Checklist checklist2 = ChecklistFixture.CHECKLIST2_USER1(room2, user); + Checklist checklist3 = ChecklistFixture.CHECKLIST3_USER1(room3, user); + checklistRepository.saveAll( + List.of(checklist1, checklist2, checklist3) + ); + checklistLikeRepository.saveAll( + List.of(ChecklistFixture.CHECKLIST1_LIKE(checklist1), + ChecklistFixture.CHECKLIST2_LIKE(checklist2)) + ); + + //when + ChecklistsPreviewResponse response = checklistManageService.readLikedChecklistsPreview(user); + + //then + assertAll( + () -> assertThat(response.checklists()).hasSize(EXPECTED_LIKE_COUNT), + () -> assertThat(response.checklists().get(0).checklistId()).isEqualTo(checklist2.getId()), + () -> assertThat(response.checklists().get(1).checklistId()).isEqualTo(checklist1.getId()) + ); + } + + @DisplayName("체크리스트 수정 성공") + @Test + void updateChecklistById() { + //given + User user = userRepository.save(UserFixture.USER1()); + long checklistId = checklistManageService.createChecklist(user, ChecklistFixture.CHECKLIST_CREATE_REQUEST()); + ChecklistRequest updateChecklistRequest = ChecklistFixture.CHECKLIST_UPDATE_REQUEST(); + + //when + checklistManageService.updateChecklistById(user, checklistId, updateChecklistRequest); + + //then + Checklist checklist = checklistRepository.getById(checklistId); + assertAll( + () -> assertThat(checklist.getRoom().getStructure()).isEqualTo(Structure.OPEN_ONE_ROOM), + () -> assertThat(checklist.getMemo()).isEqualTo(updateChecklistRequest.room().memo()) + ); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/service/ChecklistServiceTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/service/ChecklistServiceTest.java new file mode 100644 index 000000000..edf3dc26e --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/checklist/service/ChecklistServiceTest.java @@ -0,0 +1,152 @@ +package com.bang_ggood.checklist.service; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.checklist.ChecklistFixture; +import com.bang_ggood.checklist.domain.Checklist; +import com.bang_ggood.checklist.repository.ChecklistRepository; +import com.bang_ggood.like.repository.ChecklistLikeRepository; +import com.bang_ggood.option.repository.ChecklistOptionRepository; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.repository.RoomRepository; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + + +class ChecklistServiceTest extends IntegrationTestSupport { + + @Autowired + private ChecklistManageService checklistManageService; + + @Autowired + private ChecklistService checklistService; + + @Autowired + private ChecklistRepository checklistRepository; + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private ChecklistOptionRepository checklistOptionRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ChecklistLikeRepository checklistLikeRepository; + + @DisplayName("체크리스트 작성 성공") + @Test + void createChecklist() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = ChecklistFixture.CHECKLIST1_USER1(room, user); + + //when + Checklist savedChecklist = checklistService.createChecklist(checklist); + + //then + assertAll( + () -> assertThat(savedChecklist.getRealEstate()).isEqualTo(checklist.getRealEstate()), + () -> assertThat(savedChecklist.getMemo()).isEqualTo(checklist.getMemo()), + () -> assertThat(savedChecklist.getSummary()).isEqualTo(checklist.getSummary()) + ); + } + + @DisplayName("체크리스트 수정 성공") + @Test + void updateChecklist() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist savedChecklist = checklistService.createChecklist(ChecklistFixture.CHECKLIST1_USER1(room, user)); + + //when + Checklist updateChecklist = ChecklistFixture.CHECKLIST1_USER1_UPDATE(room, user); + checklistService.updateChecklist(savedChecklist, updateChecklist); + + //then + assertAll( + () -> assertThat(savedChecklist.getRealEstate()).isEqualTo(updateChecklist.getRealEstate()), + () -> assertThat(savedChecklist.getMemo()).isEqualTo(updateChecklist.getMemo()), + () -> assertThat(savedChecklist.getSummary()).isEqualTo(updateChecklist.getSummary()) + ); + } + + @DisplayName("체크리스트 삭제 성공") + @Test + void deleteById() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = ChecklistFixture.CHECKLIST1_USER1(room, user); + Checklist savedChecklist = checklistService.createChecklist(checklist); + + //when + checklistService.deleteById(checklist.getId()); + + //then + assertThat(checklistRepository.findById(checklist.getId()).isEmpty()).isTrue(); + } + + @DisplayName("체크리스트 리스트 최신순으로 조회 성공") + @Test + void readAllChecklistsOrderByLatest() { + // given + User user = userRepository.save(UserFixture.USER1()); + Room room1 = roomRepository.save(RoomFixture.ROOM_1()); + Room room2 = roomRepository.save(RoomFixture.ROOM_2()); + + Checklist checklist1 = ChecklistFixture.CHECKLIST1_USER1(room1, user); + Checklist checklist2 = ChecklistFixture.CHECKLIST3_USER2(room2, user); + + checklistService.createChecklist(checklist1); + checklistService.createChecklist(checklist2); + + // when + List checklists = checklistService.readAllChecklistsOrderByLatest(user); + + // then + assertThat(checklists).containsExactly(checklist2, checklist1); + } + + @DisplayName("좋아요된 체크리스트 리스트 최신순 조회 성공") + @Test + void readLikedChecklistsPreview() { + //given + User user = userRepository.save(UserFixture.USER1()); + Room room1 = roomRepository.save(RoomFixture.ROOM_1()); + Room room2 = roomRepository.save(RoomFixture.ROOM_2()); + Room room3 = roomRepository.save(RoomFixture.ROOM_3()); + Checklist checklist1 = ChecklistFixture.CHECKLIST1_USER1(room1, user); + Checklist checklist2 = ChecklistFixture.CHECKLIST2_USER1(room2, user); + Checklist checklist3 = ChecklistFixture.CHECKLIST3_USER1(room3, user); + checklistRepository.saveAll( + List.of(checklist1, checklist2, checklist3) + ); + checklistLikeRepository.saveAll( + List.of(ChecklistFixture.CHECKLIST1_LIKE(checklist1), + ChecklistFixture.CHECKLIST2_LIKE(checklist2)) + ); + + //when + List checklists = checklistService.readLikedChecklistsPreview(user); + + //then + assertAll( + () -> assertThat(checklists.size()).isEqualTo(2), + () -> assertThat(checklists.get(0).getId()).isEqualTo(checklist2.getId()), + () -> assertThat(checklists.get(1).getId()).isEqualTo(checklist1.getId()) + ); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/contract/domain/OccupancyMonthTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/contract/domain/OccupancyMonthTest.java new file mode 100644 index 000000000..5dbac470b --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/contract/domain/OccupancyMonthTest.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 org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class OccupancyMonthTest { + + @DisplayName("생성 성공") + @Test + void constructor() { + assertThat(OccupancyMonth.from(1)).isEqualTo(OccupancyMonth.JANUARY); + } + + @DisplayName("생성 성공: null인 경우") + @Test + void constructor_null() { + assertThat(OccupancyMonth.from(null)).isEqualTo(OccupancyMonth.NONE); + } + + @DisplayName("생성 실패: 유효하지 않은 달인 경우") + @Test + void constructor_invalidMonth_exception() { + assertThatThrownBy( + () -> OccupancyMonth.from(13) + ).isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.OCCUPANCY_MONTH_INVALID.getMessage()); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/contract/domain/OccupancyPeriodTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/contract/domain/OccupancyPeriodTest.java new file mode 100644 index 000000000..07e709867 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/contract/domain/OccupancyPeriodTest.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 org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class OccupancyPeriodTest { + + @DisplayName("생성 성공") + @Test + void constructor() { + assertThat(OccupancyPeriod.from("초")).isEqualTo(OccupancyPeriod.EARLY); + } + + @DisplayName("생성 성공: null인 경우") + @Test + void constructor_null() { + assertThat(OccupancyPeriod.from(null)).isEqualTo(OccupancyPeriod.NONE); + } + + @DisplayName("생성 실패: 유효하지 않은 기간인 경우") + @Test + void constructor_invalidPeriod_exception() { + assertThatThrownBy( + () -> OccupancyPeriod.from("기간") + ).isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.OCCUPANCY_PERIOD_INVALID.getMessage()); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/global/datasource/DataSourceTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/global/datasource/DataSourceTest.java new file mode 100644 index 000000000..c0c1b69e3 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/global/datasource/DataSourceTest.java @@ -0,0 +1,55 @@ +package com.bang_ggood.global.datasource; + +import com.bang_ggood.global.DBInitializer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +import static org.assertj.core.api.Assertions.assertThat; + +@ActiveProfiles("read-write-test") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class DataSourceTest { + + @MockBean + DBInitializer dbInitializer; + + @Autowired + private DataSource dataSource; + + @BeforeEach + void setUp() { + Mockito.doNothing().when(dbInitializer).run(); + } + + @Transactional(readOnly = true) + @DisplayName("read DB와 연결 성공: read-only인 경우") + @Test + public void read() { + try (Connection connection = dataSource.getConnection()) { + assertThat(connection.getMetaData().getURL()).contains("read"); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Transactional + @DisplayName("write DB와 연결 성공: read-only가 아닌 경우") + @Test + public void write() { + try (Connection connection = dataSource.getConnection()) { + assertThat(connection.getMetaData().getURL()).contains("write"); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/like/controller/ChecklistLikeE2ETest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/like/controller/ChecklistLikeE2ETest.java new file mode 100644 index 000000000..db0cf0683 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/like/controller/ChecklistLikeE2ETest.java @@ -0,0 +1,76 @@ +package com.bang_ggood.like.controller; + +import com.bang_ggood.AcceptanceTest; +import com.bang_ggood.checklist.ChecklistFixture; +import com.bang_ggood.checklist.domain.Checklist; +import com.bang_ggood.checklist.repository.ChecklistRepository; +import com.bang_ggood.checklist.service.ChecklistManageService; +import com.bang_ggood.like.repository.ChecklistLikeRepository; +import com.bang_ggood.like.service.ChecklistLikeManageService; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.repository.RoomRepository; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class ChecklistLikeE2ETest extends AcceptanceTest { + + @Autowired + private ChecklistManageService checklistManageService; + @Autowired + private ChecklistLikeManageService checklistLikeManageService; + @Autowired + private ChecklistRepository checklistRepository; + @Autowired + private RoomRepository roomRepository; + @Autowired + private ChecklistLikeRepository checklistLikeRepository; + + @DisplayName("체크리스트 좋아요 추가 성공") + @Test + void createChecklistLike() { + long checklistId = checklistManageService.createChecklist(getAuthenticatedUser(), + ChecklistFixture.CHECKLIST_CREATE_REQUEST()); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .when().post("/checklists/" + checklistId + "/like") + .then().log().all() + .statusCode(204); + } + + @DisplayName("체크리스트 좋아요 추가 시도 : 이미 좋아요가 추가가 된 체크리스트인 경우") + @Test + void createChecklistLike_checklistAlreadyLiked() { + long checklistId = checklistManageService.createChecklist(getAuthenticatedUser(), + ChecklistFixture.CHECKLIST_CREATE_REQUEST()); + checklistLikeManageService.createLike(getAuthenticatedUser(), checklistId); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .when().post("/checklists/" + checklistId + "/like") + .then().log().all() + .statusCode(204); + } + + @DisplayName("체크리스트 좋아요 삭제 성공") + @Test + void deleteChecklistLikeByChecklistId() { + Room room = roomRepository.save(RoomFixture.ROOM_1()); + Checklist checklist = checklistRepository.save( + ChecklistFixture.CHECKLIST1_USER1(room, this.getAuthenticatedUser())); + checklistLikeRepository.save(ChecklistFixture.CHECKLIST1_LIKE(checklist)); + + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .when().delete("/checklists/" + checklist.getId() + "/like") + .then().log().all() + .statusCode(204); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/like/service/ChecklistLikeManageServiceTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/like/service/ChecklistLikeManageServiceTest.java new file mode 100644 index 000000000..87901eb2c --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/like/service/ChecklistLikeManageServiceTest.java @@ -0,0 +1,97 @@ +package com.bang_ggood.like.service; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.checklist.ChecklistFixture; +import com.bang_ggood.checklist.domain.Checklist; +import com.bang_ggood.checklist.repository.ChecklistRepository; +import com.bang_ggood.like.domain.ChecklistLike; +import com.bang_ggood.like.repository.ChecklistLikeRepository; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.repository.RoomRepository; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +class ChecklistLikeManageServiceTest extends IntegrationTestSupport { + + @Autowired + private ChecklistLikeManageService checklistLikeManageService; + + @Autowired + private ChecklistRepository checklistRepository; + + @Autowired + private ChecklistLikeRepository checklistLikeRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoomRepository roomRepository; + + @DisplayName("체크리스트 좋아요 추가 성공") + @Test + void createChecklistLike() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + + // when + checklistLikeManageService.createLike(user, checklist.getId()); + + //then + assertThat(checklistLikeRepository.existsByChecklist(checklist)).isTrue(); + } + + @DisplayName("체크리스트 좋아요 추가 시도 : 이미 좋아요가 추가가 된 체크리스트인 경우") + @Test + void createChecklistLike_checklistAlreadyLiked() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + + // when + checklistLikeManageService.createLike(user, checklist.getId()); + + //then + assertThat(checklistLikeRepository.count()).isEqualTo(1); + } + + @DisplayName("체크리스트 좋아요 삭제 성공") + @Test + void deleteChecklistLikeByChecklistId() { + // given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + ChecklistLike checklistLike = checklistLikeRepository.save(ChecklistFixture.CHECKLIST1_LIKE(checklist)); + + // when + checklistLikeManageService.deleteLike(user, checklist.getId()); + + // then + assertThat(checklistLikeRepository.existsById(checklistLike.getId())).isFalse(); + } + + @DisplayName("체크리스트 좋아요 삭제 시도 : 체크리스트 좋아요가 없는 경우") + @Test + void deleteChecklistLikeByChecklistId_notFound() { + // given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + + // when & then + assertThatCode(() -> checklistLikeManageService.deleteLike(user, checklist.getId())) + .doesNotThrowAnyException(); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/like/service/ChecklistLikeServiceTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/like/service/ChecklistLikeServiceTest.java new file mode 100644 index 000000000..bdeeef3de --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/like/service/ChecklistLikeServiceTest.java @@ -0,0 +1,97 @@ +package com.bang_ggood.like.service; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.checklist.ChecklistFixture; +import com.bang_ggood.checklist.domain.Checklist; +import com.bang_ggood.checklist.repository.ChecklistRepository; +import com.bang_ggood.like.domain.ChecklistLike; +import com.bang_ggood.like.repository.ChecklistLikeRepository; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.repository.RoomRepository; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +class ChecklistLikeServiceTest extends IntegrationTestSupport { + + @Autowired + private ChecklistLikeService checklistLikeService; + + @Autowired + private ChecklistRepository checklistRepository; + + @Autowired + private ChecklistLikeRepository checklistLikeRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoomRepository roomRepository; + + @DisplayName("체크리스트 좋아요 추가 성공") + @Test + void createChecklistLike() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + + // when + checklistLikeService.createLike(user, checklist); + + //then + assertThat(checklistLikeRepository.existsByChecklist(checklist)).isTrue(); + } + + @DisplayName("체크리스트 좋아요 추가 시도 : 이미 좋아요가 추가가 된 체크리스트인 경우") + @Test + void createChecklistLike_checklistAlreadyLiked() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + // when + checklistLikeService.createLike(user, checklist); + checklistLikeService.createLike(user, checklist); + + //then + assertThat(checklistLikeRepository.count()).isEqualTo(1); + } + + @DisplayName("체크리스트 좋아요 삭제 성공") + @Test + void deleteChecklistLikeByChecklistId() { + // given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + ChecklistLike checklistLike = checklistLikeRepository.save(ChecklistFixture.CHECKLIST1_LIKE(checklist)); + + // when + checklistLikeService.deleteLike(user, checklist); + + // then + assertThat(checklistLikeRepository.existsById(checklistLike.getId())).isFalse(); + } + + @DisplayName("체크리스트 좋아요 삭제 시도 : 체크리스트 좋아요가 없는 경우") + @Test + void deleteChecklistLikeByChecklistId_notFound() { + // given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + + // when & then + assertThatCode(() -> checklistLikeService.deleteLike(user, checklist)) + .doesNotThrowAnyException(); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/maintenance/ChecklistMaintenanceFixture.java b/backend/bang-ggood/src/test/java/com/bang_ggood/maintenance/ChecklistMaintenanceFixture.java new file mode 100644 index 000000000..565832f7b --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/maintenance/ChecklistMaintenanceFixture.java @@ -0,0 +1,42 @@ +package com.bang_ggood.maintenance; + +import com.bang_ggood.checklist.domain.Checklist; +import com.bang_ggood.maintenance.domain.ChecklistMaintenance; +import com.bang_ggood.maintenance.domain.MaintenanceItem; +import java.util.List; + +public class ChecklistMaintenanceFixture { + + public static ChecklistMaintenance CHECKLIST1_MAINTENANCE_WATERWORKS(Checklist checklist) { + return new ChecklistMaintenance( + checklist, + MaintenanceItem.WATERWORKS + ); + } + + public static ChecklistMaintenance CHECKLIST1_MAINTENANCE_INTERNET(Checklist checklist) { + return new ChecklistMaintenance( + checklist, + MaintenanceItem.INTERNET + ); + } + + public static ChecklistMaintenance CHECKLIST1_MAINTENANCE_GAS(Checklist checklist) { + return new ChecklistMaintenance( + checklist, + MaintenanceItem.GAS + ); + } + + public static List CHECKLIST1_MAINTENANCES(Checklist checklist) { + return List.of(CHECKLIST1_MAINTENANCE_WATERWORKS(checklist), CHECKLIST1_MAINTENANCE_INTERNET(checklist)); + } + + public static List CHECKLIST1_MAINTENANCES_UPDATE(Checklist checklist) { + return List.of(CHECKLIST1_MAINTENANCE_GAS(checklist)); + } + + public static List CHECKLIST1_MAINTENANCES_DUPLICATE(Checklist checklist) { + return List.of(CHECKLIST1_MAINTENANCE_WATERWORKS(checklist), CHECKLIST1_MAINTENANCE_WATERWORKS(checklist)); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/maintenance/domain/MaintenanceItemTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/maintenance/domain/MaintenanceItemTest.java new file mode 100644 index 000000000..3a7fe7523 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/maintenance/domain/MaintenanceItemTest.java @@ -0,0 +1,46 @@ +package com.bang_ggood.maintenance.domain; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import com.bang_ggood.option.domain.Option; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class MaintenanceItemTest { + + @DisplayName("관리 항목 포함 성공: 포함하는 경우") + @Test + void contains_true() { + assertThat(MaintenanceItem.contains(MaintenanceItem.GAS.getId())).isTrue(); + } + + @DisplayName("관리 항목 포함 성공 : 포함하지 않는 경우") + @Test + void contains_false() { + assertThat(Option.contains(Integer.MAX_VALUE)).isFalse(); + } + + @DisplayName("아이디를 통해 생성 성공") + @Test + void fromId() { + // given & when + MaintenanceItem maintenanceItem = MaintenanceItem.ELECTRICITY; + + //then + assertThat(MaintenanceItem.fromId(maintenanceItem.getId())).isEqualTo(maintenanceItem); + } + + @DisplayName("아이디를 통해 생성 실패 : 유효하지 않은 아이디일 경우") + @Test + void fromId_invalid_exception() { + // given & when & then + assertThatThrownBy( + () -> MaintenanceItem.fromId(Integer.MAX_VALUE) + ) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.MAINTENANCE_ITEM_INVALID.getMessage()); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/maintenance/repository/ChecklistMaintenanceRepositoryTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/maintenance/repository/ChecklistMaintenanceRepositoryTest.java new file mode 100644 index 000000000..5cd698e35 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/maintenance/repository/ChecklistMaintenanceRepositoryTest.java @@ -0,0 +1,102 @@ +package com.bang_ggood.maintenance.repository; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.checklist.ChecklistFixture; +import com.bang_ggood.checklist.domain.Checklist; +import com.bang_ggood.checklist.repository.ChecklistRepository; +import com.bang_ggood.maintenance.domain.ChecklistMaintenance; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.repository.RoomRepository; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class ChecklistMaintenanceRepositoryTest extends IntegrationTestSupport { + + @Autowired + private ChecklistRepository checklistRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private ChecklistMaintenanceRepository checklistMaintenanceRepository; + + @DisplayName("체크리스트 관리비 포함 항목 조회 성공") + @Test + void findAllByChecklist() { + // given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + ChecklistMaintenance saved1 = checklistMaintenanceRepository.save( + ChecklistFixture.CHECKLIST1_INCLUDED_MAINTENANCE_1(checklist)); + ChecklistMaintenance saved2 = checklistMaintenanceRepository.save( + ChecklistFixture.CHECKLIST1_INCLUDED_MAINTENANCE_2(checklist)); + + // when + List checklistMaintenances = checklistMaintenanceRepository + .findAllByChecklistId(checklist.getId()); + + // then + Assertions.assertThat(checklistMaintenances).containsExactly(saved1, saved2); + } + + @DisplayName("체크리스트 관리비 포함 항목 조회 성공 : 삭제된 항목은 조회하지 않는다.") + @Test + void findAllByChecklist_() { + // given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + checklistMaintenanceRepository.save(ChecklistFixture.CHECKLIST1_INCLUDED_MAINTENANCE_1(checklist)); + checklistMaintenanceRepository.save(ChecklistFixture.CHECKLIST1_INCLUDED_MAINTENANCE_2(checklist)); + checklistMaintenanceRepository.deleteAllByChecklistId(ChecklistFixture.CHECKLIST1_USER1(room, user).getId()); + + // when + List checklistMaintenances = checklistMaintenanceRepository + .findAllByChecklistId(ChecklistFixture.CHECKLIST1_USER1(room, user).getId()); + + // then + Assertions.assertThat(checklistMaintenances).isEmpty(); + } + + @DisplayName("관리 항목 체크리스트 ID로 논리적 삭제 성공") + @Test + void deleteById() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + ChecklistMaintenance saved1 = checklistMaintenanceRepository.save( + ChecklistFixture.CHECKLIST1_INCLUDED_MAINTENANCE_1(checklist)); + ChecklistMaintenance saved2 = checklistMaintenanceRepository.save( + ChecklistFixture.CHECKLIST1_INCLUDED_MAINTENANCE_2(checklist)); + + //when + checklistMaintenanceRepository.deleteAllByChecklistId( + ChecklistFixture.CHECKLIST1_INCLUDED_MAINTENANCE_1(checklist).getChecklist().getId()); + + //then + assertAll( + () -> assertThat(checklistMaintenanceRepository.existsById(saved1.getId())).isTrue(), + () -> assertThat(checklistMaintenanceRepository.existsById(saved2.getId())).isTrue(), + () -> assertThat( + checklistMaintenanceRepository.findById(saved1.getId()).get().isDeleted()).isTrue(), + () -> assertThat( + checklistMaintenanceRepository.findById(saved2.getId()).get().isDeleted()).isTrue() + ); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/maintenance/service/ChecklistMaintenanceServiceTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/maintenance/service/ChecklistMaintenanceServiceTest.java new file mode 100644 index 000000000..2d2409d8a --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/maintenance/service/ChecklistMaintenanceServiceTest.java @@ -0,0 +1,138 @@ +package com.bang_ggood.maintenance.service; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.checklist.ChecklistFixture; +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.maintenance.ChecklistMaintenanceFixture; +import com.bang_ggood.maintenance.domain.ChecklistMaintenance; +import com.bang_ggood.maintenance.repository.ChecklistMaintenanceRepository; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.repository.RoomRepository; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ChecklistMaintenanceServiceTest extends IntegrationTestSupport { + + @Autowired + private ChecklistMaintenanceService checklistMaintenanceService; + + @Autowired + private ChecklistMaintenanceRepository checklistMaintenanceRepository; + + @Autowired + private ChecklistRepository checklistRepository; + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private UserRepository userRepository; + + @DisplayName("관리비 포함 항목 작성 성공") + @Test + void createChecklistMaintenance() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistMaintenances = ChecklistMaintenanceFixture.CHECKLIST1_MAINTENANCES( + checklist); + + //when + checklistMaintenanceService.createMaintenances(checklistMaintenances); + + //then + assertThat(checklistMaintenanceRepository.findAllByChecklistId(checklist.getId())).hasSize( + checklistMaintenances.size()); + } + + @DisplayName("관리비 포함 항목 작성 실패: 관리 항목 id가 중복일 경우") + @Test + void createChecklistMaintenance_duplicatedId_exception() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistMaintenances = ChecklistMaintenanceFixture.CHECKLIST1_MAINTENANCES_DUPLICATE( + checklist); + + // when & then + assertThatThrownBy( + () -> checklistMaintenanceService.createMaintenances(checklistMaintenances)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.MAINTENANCE_ITEM_DUPLICATE.getMessage()); + } + + @DisplayName("관리비 포함 항목 삭제 성공") + @Test + void deleteAllByChecklistId() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistMaintenances = List.of( + ChecklistMaintenanceFixture.CHECKLIST1_MAINTENANCE_WATERWORKS(checklist), + ChecklistMaintenanceFixture.CHECKLIST1_MAINTENANCE_INTERNET(checklist) + ); + checklistMaintenanceService.createMaintenances(checklistMaintenances); + + //when + checklistMaintenanceService.deleteAllByChecklistId(checklist.getId()); + + //then + assertThat(checklistMaintenanceRepository.findAllByChecklistId(checklist.getId())).hasSize(0); + } + + @DisplayName("관리비 포함 항목 수정 성공") + @Test + void updateChecklistMaintenance() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistMaintenances = ChecklistMaintenanceFixture.CHECKLIST1_MAINTENANCES( + checklist); + checklistMaintenanceService.createMaintenances(checklistMaintenances); + + //when + List updateMaintenances = ChecklistMaintenanceFixture.CHECKLIST1_MAINTENANCES_UPDATE( + checklist); + checklistMaintenanceService.updateMaintenances(checklist.getId(), updateMaintenances); + + //then + assertThat(checklistMaintenanceRepository.findAllByChecklistId(checklist.getId())).hasSize( + updateMaintenances.size()); + } + + @DisplayName("관리비 포함 항목 수정 실패: : 관리 항목 id가 중복일 경우") + @Test + void updateChecklistMaintenance_duplicatedId_exception() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistMaintenances = ChecklistMaintenanceFixture.CHECKLIST1_MAINTENANCES( + checklist); + checklistMaintenanceService.createMaintenances(checklistMaintenances); + + //when & then + List updateMaintenances = ChecklistMaintenanceFixture.CHECKLIST1_MAINTENANCES_DUPLICATE( + checklist); + assertThatThrownBy( + () -> checklistMaintenanceService.updateMaintenances(checklist.getId(), updateMaintenances)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.MAINTENANCE_ITEM_DUPLICATE.getMessage()); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/option/ChecklistOptionFixture.java b/backend/bang-ggood/src/test/java/com/bang_ggood/option/ChecklistOptionFixture.java new file mode 100644 index 000000000..93169d542 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/option/ChecklistOptionFixture.java @@ -0,0 +1,42 @@ +package com.bang_ggood.option; + +import com.bang_ggood.checklist.domain.Checklist; +import com.bang_ggood.option.domain.ChecklistOption; +import com.bang_ggood.option.domain.Option; +import java.util.List; + +public class ChecklistOptionFixture { + + public static ChecklistOption CHECKLIST1_OPTION_CLOSET(Checklist checklist) { + return new ChecklistOption( + checklist, + Option.CLOSET.getId() + ); + } + + public static ChecklistOption CHECKLIST1_OPTION_BED(Checklist checklist) { + return new ChecklistOption( + checklist, + Option.BED.getId() + ); + } + + public static ChecklistOption CHECKLIST1_OPTION_ELEVATOR(Checklist checklist) { + return new ChecklistOption( + checklist, + Option.ELEVATOR.getId() + ); + } + + public static List CHECkLIST1_OPTIONS(Checklist checklist) { + return List.of(CHECKLIST1_OPTION_CLOSET(checklist), CHECKLIST1_OPTION_BED(checklist)); + } + + public static List CHECkLIST1_OPTIONS_UPDATE(Checklist checklist) { + return List.of(CHECKLIST1_OPTION_ELEVATOR(checklist)); + } + + public static List CHECkLIST1_OPTIONS_DUPLICATE(Checklist checklist) { + return List.of(CHECKLIST1_OPTION_CLOSET(checklist), CHECKLIST1_OPTION_CLOSET(checklist)); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/option/domain/OptionTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/option/domain/OptionTest.java new file mode 100644 index 000000000..015504bf7 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/option/domain/OptionTest.java @@ -0,0 +1,21 @@ +package com.bang_ggood.option.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class OptionTest { + + @DisplayName("옵션 포함 성공: 포함하는 경우") + @Test + void contains_true() { + assertThat(Option.contains(1)).isTrue(); + } + + @DisplayName("옵션 포함 성공 : 포함하지 않는 경우") + @Test + void contains_false() { + assertThat(Option.contains(9999)).isFalse(); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/option/repository/ChecklistOptionRepositoryTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/option/repository/ChecklistOptionRepositoryTest.java new file mode 100644 index 000000000..64c2e6dc5 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/option/repository/ChecklistOptionRepositoryTest.java @@ -0,0 +1,79 @@ +package com.bang_ggood.option.repository; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.checklist.ChecklistFixture; +import com.bang_ggood.checklist.repository.ChecklistRepository; +import com.bang_ggood.checklist.service.ChecklistManageService; +import com.bang_ggood.option.domain.ChecklistOption; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class ChecklistOptionRepositoryTest extends IntegrationTestSupport { + + @Autowired + private ChecklistManageService checklistManageService; + + @Autowired + private ChecklistRepository checklistRepository; + + @Autowired + private ChecklistOptionRepository checklistOptionRepository; + @Autowired + private UserRepository userRepository; + + private long checklistId; + + @BeforeEach + void setUp() { + User user = userRepository.save(UserFixture.USER1()); + checklistId = checklistManageService.createChecklist(user, ChecklistFixture.CHECKLIST_CREATE_REQUEST()); + } + + @DisplayName("체크리스트 ID로 옵션 찾기 성공") + @Test + void findAllByChecklistId() { + // given & when + List checklistOptions = checklistOptionRepository.findAllByChecklistId(checklistId); + + // then + assertThat(checklistOptions) + .allSatisfy(option -> + assertThat(option.isDeleted()).isFalse() + ); + } + + @DisplayName("체크리스트 옵션 수 세기 성공") + @Test + void countByChecklist() { + // given & when + int optionCount = checklistOptionRepository.countByChecklist(checklistRepository.getById(checklistId)); + + // then + assertThat(optionCount).isEqualTo(ChecklistFixture.CHECKLIST_CREATE_REQUEST().options().size()); + + } + + @DisplayName("체크리스트 ID로 옵션 삭제 성공") + @Test + void deleteAllByChecklistId() { + // given + List checklistOptions = checklistOptionRepository.findAllByChecklistId(checklistId); + + // when + checklistOptionRepository.deleteAllByChecklistId(checklistId); + + // then + assertThat(checklistOptions) + .allSatisfy(option -> + assertThat(checklistOptionRepository.findById(option.getId()).get().isDeleted()).isTrue() + ); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/option/service/ChecklistOptionServiceTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/option/service/ChecklistOptionServiceTest.java new file mode 100644 index 000000000..3efaa0518 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/option/service/ChecklistOptionServiceTest.java @@ -0,0 +1,134 @@ +package com.bang_ggood.option.service; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.checklist.ChecklistFixture; +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.option.ChecklistOptionFixture; +import com.bang_ggood.option.domain.ChecklistOption; +import com.bang_ggood.option.repository.ChecklistOptionRepository; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.repository.RoomRepository; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +class ChecklistOptionServiceTest extends IntegrationTestSupport { + + @Autowired + private ChecklistOptionService checklistOptionService; + + @Autowired + private ChecklistOptionRepository checklistOptionRepository; + + @Autowired + private ChecklistRepository checklistRepository; + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private UserRepository userRepository; + + @DisplayName("옵션 작성 성공") + @Test + void createOptions() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistOptions = ChecklistOptionFixture.CHECkLIST1_OPTIONS(checklist); + + //when + checklistOptionService.createOptions(checklistOptions); + + //then + assertThat(checklistOptionRepository.findAllByChecklistId(checklist.getId())).hasSize(checklistOptions.size()); + } + + @DisplayName("옵션 작성 실패: 옵션 id가 중복일 경우") + @Test + void createOption_duplicateId_exception() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistOptions = ChecklistOptionFixture.CHECkLIST1_OPTIONS_DUPLICATE(checklist); + + // when & then + assertThatThrownBy( + () -> checklistOptionService.createOptions(checklistOptions)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.OPTION_DUPLICATED.getMessage()); + } + + @DisplayName("옵션 삭제 성공") + @Test + void deleteAllByChecklistId() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistOptions = List.of( + ChecklistOptionFixture.CHECKLIST1_OPTION_CLOSET(checklist), + ChecklistOptionFixture.CHECKLIST1_OPTION_BED(checklist) + ); + checklistOptionService.createOptions(checklistOptions); + + //when + checklistOptionService.deleteAllByChecklistId(checklist.getId()); + + //then + assertThat(checklistOptionRepository.findAllByChecklistId(checklist.getId())).hasSize(0); + } + + @DisplayName("옵션 수정 성공") + @Test + void updateOptions() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistOptions = ChecklistOptionFixture.CHECkLIST1_OPTIONS(checklist); + checklistOptionService.createOptions(checklistOptions); + + //when + List updateOptions = ChecklistOptionFixture.CHECkLIST1_OPTIONS_UPDATE(checklist); + checklistOptionService.updateOptions(checklist.getId(), updateOptions); + + //then + assertAll( + () -> assertThat(checklistOptionRepository.findAllByChecklistId(checklist.getId())).hasSize( + updateOptions.size()) + ); + } + + @DisplayName("옵션 수정 실패: 옵션 id가 중복일 경우") + @Test + void updateOptions_duplicateId_exception() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistOptions = ChecklistOptionFixture.CHECkLIST1_OPTIONS(checklist); + checklistOptionService.createOptions(checklistOptions); + + // when & then + List updateOptions = ChecklistOptionFixture.CHECkLIST1_OPTIONS_DUPLICATE(checklist); + assertThatThrownBy( + () -> checklistOptionService.updateOptions(checklist.getId(), updateOptions)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.OPTION_DUPLICATED.getMessage()); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/question/ChecklistQuestionFixture.java b/backend/bang-ggood/src/test/java/com/bang_ggood/question/ChecklistQuestionFixture.java new file mode 100644 index 000000000..48618562b --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/question/ChecklistQuestionFixture.java @@ -0,0 +1,62 @@ +package com.bang_ggood.question; + +import com.bang_ggood.checklist.domain.Checklist; +import com.bang_ggood.question.domain.Answer; +import com.bang_ggood.question.domain.ChecklistQuestion; +import com.bang_ggood.question.domain.Question; +import java.util.List; + +public class ChecklistQuestionFixture { + + public static ChecklistQuestion CHECKLIST1_QUESTION1(Checklist checklist, Question question) { + return new ChecklistQuestion( + checklist, + question, + Answer.BAD + ); + } + + public static ChecklistQuestion CHECKLIST1_QUESTION2(Checklist checklist, Question question) { + return new ChecklistQuestion( + checklist, + question, + Answer.GOOD + ); + } + + public static ChecklistQuestion CHECKLIST1_QUESTION2_UPDATE_ANSWER(Checklist checklist, Question question) { + return new ChecklistQuestion( + checklist, + question, + Answer.BAD + ); + } + + public static ChecklistQuestion CHECKLIST1_QUESTION11(Checklist checklist, Question question) { + return new ChecklistQuestion( + checklist, + question, + null + ); + } + + public static List CHECKLIST1_QUESTIONS(Checklist checklist, Question question1, Question question2) { + return List.of(CHECKLIST1_QUESTION1(checklist, question1), CHECKLIST1_QUESTION2(checklist, question2)); + } + + public static List CHECKLIST1_DUPLICATE(Checklist checklist, Question question) { + return List.of(CHECKLIST1_QUESTION1(checklist, question), CHECKLIST1_QUESTION1(checklist, question)); + } + + public static List CHECKLIST1_QUESTIONS_UPDATE(Checklist checklist, Question question1, Question question2) { + return List.of(CHECKLIST1_QUESTION1(checklist, question1), CHECKLIST1_QUESTION2_UPDATE_ANSWER(checklist, question2)); + } + + public static List CHECKLIST1_QUESTIONS_DIFFERENT_LENGTH(Checklist checklist, Question question) { + return List.of(CHECKLIST1_QUESTION2_UPDATE_ANSWER(checklist, question)); + } + + public static List CHECKLIST1_QUESTIONS_DIFFERENT_QUESTION(Checklist checklist, Question question2, Question question1) { + return List.of(CHECKLIST1_QUESTION2_UPDATE_ANSWER(checklist, question2), CHECKLIST1_QUESTION11(checklist, question1)); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/question/CustomChecklistFixture.java b/backend/bang-ggood/src/test/java/com/bang_ggood/question/CustomChecklistFixture.java new file mode 100644 index 000000000..c6e5db8af --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/question/CustomChecklistFixture.java @@ -0,0 +1,44 @@ +package com.bang_ggood.question; + +import com.bang_ggood.question.domain.CustomChecklistQuestion; +import com.bang_ggood.question.dto.request.CustomChecklistUpdateRequest; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import java.util.ArrayList; +import java.util.List; + + +public class CustomChecklistFixture { + + public static List CUSTOM_CHECKLIST_QUESTION_DEFAULT; + public static CustomChecklistUpdateRequest CUSTOM_CHECKLIST_UPDATE_REQUEST; + + + public static List CUSTOM_CHECKLIST_QUESTION_DEFAULT(User user) { + return List.of(new CustomChecklistQuestion(user, QuestionFixture.QUESTION1_CATEGORY1), + new CustomChecklistQuestion(user, QuestionFixture.QUESTION3_CATEGORY2)); + } + + public static CustomChecklistUpdateRequest CUSTOM_CHECKLIST_UPDATE_REQUEST() { + return new CustomChecklistUpdateRequest(List.of(QuestionFixture.QUESTION1_CATEGORY1.getId(), + QuestionFixture.QUESTION2_CATEGORY1.getId(), + QuestionFixture.QUESTION3_CATEGORY2.getId())); + } + + public static CustomChecklistUpdateRequest CUSTOM_CHECKLIST_UPDATE_REQUEST_EMPTY() { + return new CustomChecklistUpdateRequest(new ArrayList<>()); + } + + public static CustomChecklistUpdateRequest CUSTOM_CHECKLIST_UPDATE_REQUEST_DUPLICATED() { + return new CustomChecklistUpdateRequest(List.of(1, 1)); + } + + public static CustomChecklistUpdateRequest CUSTOM_CHECKLIST_UPDATE_REQUEST_INVALID() { + return new CustomChecklistUpdateRequest(List.of(99999)); + } + + public static void init() { + CUSTOM_CHECKLIST_QUESTION_DEFAULT = List.of(new CustomChecklistQuestion(UserFixture.USER1, QuestionFixture.QUESTION1_CATEGORY1), + new CustomChecklistQuestion(UserFixture.USER1, QuestionFixture.QUESTION2_CATEGORY1)); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/question/QuestionFixture.java b/backend/bang-ggood/src/test/java/com/bang_ggood/question/QuestionFixture.java new file mode 100644 index 000000000..2ff90cfc6 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/question/QuestionFixture.java @@ -0,0 +1,27 @@ +package com.bang_ggood.question; + +import com.bang_ggood.question.domain.Category; +import com.bang_ggood.question.domain.Question; +import com.bang_ggood.question.repository.CategoryRepository; +import com.bang_ggood.question.repository.QuestionRepository; + +public class QuestionFixture { + + public static Question QUESTION1_CATEGORY1; + public static Question QUESTION2_CATEGORY1; + public static Question QUESTION3_CATEGORY2; + public static Question QUESTION4_CATEGORY2; + public static Question QUESTION5_CATEGORY2; + public static Category CATEGORY1; + public static Category CATEGORY2; + + public static void init(CategoryRepository categoryRepository, QuestionRepository questionRepository) { + CATEGORY1 = categoryRepository.save(new Category("방 컨디션")); + CATEGORY2 = categoryRepository.save(new Category("창문")); + QUESTION1_CATEGORY1 = questionRepository.save(new Question(CATEGORY1, "testTitle1", "testSubTitle1", true)); + QUESTION2_CATEGORY1 = questionRepository.save(new Question(CATEGORY1, "testTitle2", "testSubTitle2", true)); + QUESTION3_CATEGORY2 = questionRepository.save(new Question(CATEGORY2, "testTitle3", "testSubTitle3", true)); + QUESTION4_CATEGORY2 = questionRepository.save(new Question(CATEGORY2, "testTitle4", "testSubTitle3", true)); + QUESTION5_CATEGORY2 = questionRepository.save(new Question(CATEGORY2, "testTitle5", "testSubTitle3", true)); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/question/controller/QuestionE2ETest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/question/controller/QuestionE2ETest.java new file mode 100644 index 000000000..47ea3b514 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/question/controller/QuestionE2ETest.java @@ -0,0 +1,46 @@ +package com.bang_ggood.question.controller; + +import com.bang_ggood.AcceptanceTest; +import com.bang_ggood.question.CustomChecklistFixture; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class QuestionE2ETest extends AcceptanceTest { + + @DisplayName("커스텀 체크리스트 질문 조회 성공") + @Test + void readCustomChecklistQuestions() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .when().get("/checklists/questions") + .then().log().all() + .statusCode(200); + } + + @DisplayName("커스텀 체크리스트 질문 전체 조회 성공") + @Test + void readAllCustomChecklistQuestion() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .when().get("/custom-checklist/all") + .then().log().all() + .statusCode(200); + } + + @DisplayName("커스텀 체크리스트 업데이트 성공") + @Test + void updateCustomChecklist() { + RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .body(CustomChecklistFixture.CUSTOM_CHECKLIST_UPDATE_REQUEST()) + .when().put("/custom-checklist") + .then().log().all() + .statusCode(204); + } + +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/question/domain/ChecklistQuestionTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/question/domain/ChecklistQuestionTest.java new file mode 100644 index 000000000..156a67355 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/question/domain/ChecklistQuestionTest.java @@ -0,0 +1,49 @@ +package com.bang_ggood.question.domain; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.checklist.ChecklistFixture; +import com.bang_ggood.question.ChecklistQuestionFixture; +import com.bang_ggood.question.QuestionFixture; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ChecklistQuestionTest extends IntegrationTestSupport { + + @DisplayName("체크리스트 내에서 질문끼리 다른 id를 갖고 있는지 확인 성공 : 다른 id일 경우") + @Test + void isDifferentQuestionId_true() { + //given + Room room = RoomFixture.ROOM_1(); + User user = UserFixture.USER1(); + + ChecklistQuestion checklistQuestion1 = ChecklistQuestionFixture.CHECKLIST1_QUESTION1( + ChecklistFixture.CHECKLIST1_USER1(room, user), QuestionFixture.QUESTION1_CATEGORY1); + ChecklistQuestion checklistQuestion2 = ChecklistQuestionFixture.CHECKLIST1_QUESTION2( + ChecklistFixture.CHECKLIST1_USER1(room, user), QuestionFixture.QUESTION2_CATEGORY1); + + //when & then + assertThat(checklistQuestion1.isDifferentQuestionId(checklistQuestion2)).isTrue(); + } + + @DisplayName("체크리스트 내에서 질문끼리 다른 id를 갖고 있는지 확인 성공 : 같은 id일 경우") + @Test + void isDifferentQuestionId_false() { + //given + Room room = RoomFixture.ROOM_1(); + User user = UserFixture.USER1(); + ChecklistQuestion checklistQuestion = ChecklistQuestionFixture.CHECKLIST1_QUESTION1( + ChecklistFixture.CHECKLIST1_USER1(room, user), QuestionFixture.QUESTION1_CATEGORY1); + ChecklistQuestion compareChecklistQuestion = ChecklistQuestionFixture.CHECKLIST1_QUESTION1( + ChecklistFixture.CHECKLIST1_USER1(room, user), QuestionFixture.QUESTION1_CATEGORY1); + + //when & then + assertThat(checklistQuestion.isDifferentQuestionId(compareChecklistQuestion)).isFalse(); + } + +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/question/repository/CategoryRepositoryTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/question/repository/CategoryRepositoryTest.java new file mode 100644 index 000000000..dbdcd167c --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/question/repository/CategoryRepositoryTest.java @@ -0,0 +1,45 @@ +package com.bang_ggood.question.repository; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.question.QuestionFixture; +import com.bang_ggood.question.domain.Category; +import com.bang_ggood.question.domain.CustomChecklistQuestion; +import com.bang_ggood.user.UserFixture; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + +class CategoryRepositoryTest extends IntegrationTestSupport { + + @Autowired + private CategoryRepository categoryRepository; + @Autowired + private CustomChecklistQuestionRepository customChecklistQuestionRepository; + + @DisplayName("카테고리 조회 성공 : 커스텀 체크리스트 카테고리 조회") + @Test + void findAllById() { + //given + int expectedCategory = 2; + CustomChecklistQuestion customChecklistQuestion1 = new CustomChecklistQuestion( + UserFixture.USER1, + QuestionFixture.QUESTION1_CATEGORY1); + CustomChecklistQuestion customChecklistQuestion2 = new CustomChecklistQuestion( + UserFixture.USER1, + QuestionFixture.QUESTION2_CATEGORY1); + CustomChecklistQuestion customChecklistQuestion3 = new CustomChecklistQuestion( + UserFixture.USER1, + QuestionFixture.QUESTION3_CATEGORY2); + customChecklistQuestionRepository.saveAll(List.of(customChecklistQuestion1, customChecklistQuestion2, customChecklistQuestion3)); + + // when + List categories = categoryRepository.findAllCustomQuestionCategoriesByUserId(UserFixture.USER1.getId()); + + // then + Assertions.assertThat(categories) + .hasSize(expectedCategory) + .containsOnly(QuestionFixture.CATEGORY1, QuestionFixture.CATEGORY2); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/question/repository/ChecklistQuestionRepositoryTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/question/repository/ChecklistQuestionRepositoryTest.java new file mode 100644 index 000000000..bb282da96 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/question/repository/ChecklistQuestionRepositoryTest.java @@ -0,0 +1,87 @@ +package com.bang_ggood.question.repository; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.checklist.ChecklistFixture; +import com.bang_ggood.checklist.domain.Checklist; +import com.bang_ggood.checklist.repository.ChecklistRepository; +import com.bang_ggood.question.ChecklistQuestionFixture; +import com.bang_ggood.question.QuestionFixture; +import com.bang_ggood.question.domain.ChecklistQuestion; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.repository.RoomRepository; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class ChecklistQuestionRepositoryTest extends IntegrationTestSupport { + + @Autowired + private ChecklistRepository checklistRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private ChecklistQuestionRepository checklistQuestionRepository; + + @DisplayName("질문 답변을 체크리스트 ID로 조회 성공") + @Test + void findAllByChecklistId() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + checklistQuestionRepository.save(ChecklistQuestionFixture.CHECKLIST1_QUESTION1(checklist, QuestionFixture.QUESTION1_CATEGORY1)); + checklistQuestionRepository.save(ChecklistQuestionFixture.CHECKLIST1_QUESTION2(checklist, QuestionFixture.QUESTION2_CATEGORY1)); + + // when + List checklistQuestions = checklistQuestionRepository.findAllByChecklistId( + checklist.getId()); + + //then + assertAll( + () -> assertThat(checklistQuestions.get(0).isDeleted()).isFalse() + ); + } + + @DisplayName("질문 답변을 체크리스트 ID로 논리적 삭제 성공") + @Test + void deleteAllByChecklistId() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + ChecklistQuestion checklistQuestion1 = checklistQuestionRepository.save( + ChecklistQuestionFixture.CHECKLIST1_QUESTION1(checklist, QuestionFixture.QUESTION1_CATEGORY1)); + ChecklistQuestion checklistQuestion2 = checklistQuestionRepository.save( + ChecklistQuestionFixture.CHECKLIST1_QUESTION2(checklist, QuestionFixture.QUESTION2_CATEGORY1)); + + //when + checklistQuestionRepository.deleteAllByChecklistId( + ChecklistQuestionFixture.CHECKLIST1_QUESTION1(checklist, QuestionFixture.QUESTION1_CATEGORY1).getChecklistId()); + + //then + assertAll( + () -> assertThat( + checklistQuestionRepository.existsById( + checklistQuestion1.getId())).isTrue(), + () -> assertThat( + checklistQuestionRepository.findById(checklistQuestion2.getId()) + .get() + .isDeleted()).isTrue() + ); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/question/repository/QuestionRepositoryTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/question/repository/QuestionRepositoryTest.java new file mode 100644 index 000000000..cf27f0735 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/question/repository/QuestionRepositoryTest.java @@ -0,0 +1,45 @@ +package com.bang_ggood.question.repository; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.question.QuestionFixture; +import com.bang_ggood.question.domain.Question; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + +class QuestionRepositoryTest extends IntegrationTestSupport { + + @Autowired + private QuestionRepository questionRepository; + + @DisplayName("디폴트 체크리스트 조회 성공") + @Test + void findByIsDefaultTrue() { + // given + Question question1 = questionRepository.save(new Question(QuestionFixture.CATEGORY1, "test", "test", true)); + Question question2 = questionRepository.save(new Question(QuestionFixture.CATEGORY2, "test", "test", false)); + + // when + List questions = questionRepository.findAllByIsDefaultTrue(); + + // then + Assertions.assertThat(questions).contains(question1); + Assertions.assertThat(questions).doesNotContain(question2); + } + + @DisplayName("체크리스트 id로 조회 성공") + @Test + void findAllByIdIn() { + // given + Question question1 = QuestionFixture.QUESTION1_CATEGORY1; + Question question2 = QuestionFixture.QUESTION2_CATEGORY1; + + // when + List questions = questionRepository.findAllByIdIn(List.of(question1.getId(), question2.getId())); + + // then + Assertions.assertThat(questions).containsExactly(question1, question2); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/question/service/ChecklistQuestionServiceTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/question/service/ChecklistQuestionServiceTest.java new file mode 100644 index 000000000..546f3fa2d --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/question/service/ChecklistQuestionServiceTest.java @@ -0,0 +1,245 @@ +package com.bang_ggood.question.service; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.checklist.ChecklistFixture; +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.question.ChecklistQuestionFixture; +import com.bang_ggood.question.QuestionFixture; +import com.bang_ggood.question.domain.Answer; +import com.bang_ggood.question.domain.ChecklistQuestion; +import com.bang_ggood.question.domain.CustomChecklistQuestion; +import com.bang_ggood.question.domain.Question; +import com.bang_ggood.question.repository.ChecklistQuestionRepository; +import com.bang_ggood.question.repository.CustomChecklistQuestionRepository; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.repository.RoomRepository; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ChecklistQuestionServiceTest extends IntegrationTestSupport { + + @Autowired + private ChecklistQuestionService checklistQuestionService; + + @Autowired + private ChecklistQuestionRepository checklistQuestionRepository; + + @Autowired + private CustomChecklistQuestionRepository customChecklistQuestionRepository; + + @Autowired + private ChecklistRepository checklistRepository; + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private UserRepository userRepository; + + @DisplayName("질문 작성 성공") + @Test + void createQuestions() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistQuestions = ChecklistQuestionFixture.CHECKLIST1_QUESTIONS(checklist, QuestionFixture.QUESTION1_CATEGORY1, QuestionFixture.QUESTION2_CATEGORY1); + + //when + checklistQuestionService.createQuestions(checklistQuestions); + + //then + assertThat(checklistQuestionRepository.findAllByChecklistId(checklist.getId())).hasSize( + checklistQuestions.size()); + } + + @DisplayName("질문 작성 실패: 질문 id가 중복일 경우") + @Test + void createQuestions_duplicateId_exception() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistQuestions = ChecklistQuestionFixture.CHECKLIST1_DUPLICATE(checklist, QuestionFixture.QUESTION1_CATEGORY1); + + // when & then + assertThatThrownBy( + () -> checklistQuestionService.createQuestions(checklistQuestions)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.QUESTION_DUPLICATED.getMessage()); + } + + @DisplayName("체크리스트 아이디로 모든 질문 삭제 성공") + @Test + void deleteAllByChecklistId() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistQuestions = List.of( + ChecklistQuestionFixture.CHECKLIST1_QUESTION1(checklist, QuestionFixture.QUESTION1_CATEGORY1), + ChecklistQuestionFixture.CHECKLIST1_QUESTION2(checklist, QuestionFixture.QUESTION2_CATEGORY1) + ); + checklistQuestionService.createQuestions(checklistQuestions); + + // when + checklistQuestionService.deleteAllByChecklistId(checklist.getId()); + + // then + assertThat(checklistQuestionRepository.findAllByChecklistId(checklist.getId())).hasSize(0); + + } + + @DisplayName("질문 수정 성공") + @Test + void updateQuestions() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistQuestions = ChecklistQuestionFixture.CHECKLIST1_QUESTIONS(checklist, QuestionFixture.QUESTION1_CATEGORY1, QuestionFixture.QUESTION2_CATEGORY1); + checklistQuestionService.createQuestions(checklistQuestions); + + //when + List updateQuestions = ChecklistQuestionFixture.CHECKLIST1_QUESTIONS_UPDATE(checklist, QuestionFixture.QUESTION1_CATEGORY1, QuestionFixture.QUESTION2_CATEGORY1); + checklistQuestionService.updateQuestions(checklistQuestions, updateQuestions); + + //then + assertThat(checklistQuestionRepository.findAllByChecklistId(checklist.getId()).get(1).getAnswer()).isEqualTo( + Answer.BAD); + } + + @DisplayName("질문 수정 실패: 질문 id가 중복일 경우") + @Test + void updateQuestions_duplicateId_exception() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistQuestions = ChecklistQuestionFixture.CHECKLIST1_QUESTIONS(checklist, QuestionFixture.QUESTION1_CATEGORY1, QuestionFixture.QUESTION2_CATEGORY1); + checklistQuestionService.createQuestions(checklistQuestions); + + //when & then + List updateQuestions = ChecklistQuestionFixture.CHECKLIST1_DUPLICATE(checklist, QuestionFixture.QUESTION1_CATEGORY1); + assertThatThrownBy( + () -> checklistQuestionService.updateQuestions(checklistQuestions, updateQuestions)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.QUESTION_DUPLICATED.getMessage()); + } + + @DisplayName("질문 수정 실패 : 기존의 질문과 질문 길이가 다를 경우") + @Test + void updateQuestions_differentQuestionLength_exception() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistQuestions = ChecklistQuestionFixture.CHECKLIST1_QUESTIONS(checklist, QuestionFixture.QUESTION1_CATEGORY1, QuestionFixture.QUESTION2_CATEGORY1); + checklistQuestionService.createQuestions(checklistQuestions); + + //when & then + List updateQuestions = ChecklistQuestionFixture.CHECKLIST1_QUESTIONS_DIFFERENT_LENGTH( + checklist, QuestionFixture.QUESTION1_CATEGORY1); + assertThatThrownBy( + () -> checklistQuestionService.updateQuestions(checklistQuestions, updateQuestions)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.QUESTION_DIFFERENT.getMessage()); + } + + @DisplayName("질문 수정 실패 : 기존의 체크리스트와 질문이 다를 경우") + @Test + void updateQuestions_differentQuestion_exception() { + //given + Room room = roomRepository.save(RoomFixture.ROOM_1()); + User user = userRepository.save(UserFixture.USER1()); + + Checklist checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + List checklistQuestions = ChecklistQuestionFixture.CHECKLIST1_QUESTIONS(checklist, QuestionFixture.QUESTION1_CATEGORY1, QuestionFixture.QUESTION2_CATEGORY1); + checklistQuestionService.createQuestions(checklistQuestions); + + //when & then + List updateQuestions = ChecklistQuestionFixture.CHECKLIST1_QUESTIONS_DIFFERENT_QUESTION( + checklist, QuestionFixture.QUESTION2_CATEGORY1, QuestionFixture.QUESTION1_CATEGORY1); + assertThatThrownBy( + () -> checklistQuestionService.updateQuestions(checklistQuestions, updateQuestions)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.QUESTION_DIFFERENT.getMessage()); + } + + @DisplayName("커스텀 체크리스트 질문 조회 성공") + @Test + void readCustomChecklistQuestions() { + // given + User user = userRepository.save(UserFixture.USER1()); + CustomChecklistQuestion question1 = new CustomChecklistQuestion(user, QuestionFixture.QUESTION1_CATEGORY1); + CustomChecklistQuestion question2 = new CustomChecklistQuestion(user, QuestionFixture.QUESTION2_CATEGORY1); + List questions = List.of(question1, question2); + customChecklistQuestionRepository.saveAll(questions); + + // when + List customChecklistQuestions = checklistQuestionService.readCustomChecklistQuestions( + user); + + // then + Assertions.assertThat(customChecklistQuestions).hasSize(questions.size()); + } + + @DisplayName("커스텀 체크리스트 업데이트 성공") + @Test + void updateCustomChecklist() { + // given + User user = userRepository.save(UserFixture.USER1()); + List questions = List.of(QuestionFixture.QUESTION1_CATEGORY1, QuestionFixture.QUESTION3_CATEGORY2); + + // when + checklistQuestionService.updateCustomChecklist(user, questions); + + // then + assertThat(customChecklistQuestionRepository.findAllByUser(user)) + .hasSize(questions.size()); + } + + @DisplayName("커스텀 체크리스트 업데이트 실패 : 선택한 질문 개수가 0개일 때") + @Test + void updateCustomChecklist_empty_exception() { + // given + List questions = Collections.emptyList(); + + // when & then + assertThatThrownBy(() -> checklistQuestionService.updateCustomChecklist(UserFixture.USER1(), questions)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.CUSTOM_CHECKLIST_QUESTION_EMPTY.getMessage()); + } + + @DisplayName("커스텀 체크리스트 업데이트 실패 : 질문이 중복될 때") + @Test + void updateCustomChecklist_duplicatedQuestion_exception() { + // given + List questions = List.of(QuestionFixture.QUESTION1_CATEGORY1, QuestionFixture.QUESTION1_CATEGORY1); + + // when & then + assertThatThrownBy(() -> checklistQuestionService.updateCustomChecklist(UserFixture.USER1, questions)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.QUESTION_DUPLICATED.getMessage()); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/question/service/QuestionManageServiceTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/question/service/QuestionManageServiceTest.java new file mode 100644 index 000000000..6a6ab92e8 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/question/service/QuestionManageServiceTest.java @@ -0,0 +1,111 @@ +package com.bang_ggood.question.service; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import com.bang_ggood.question.CustomChecklistFixture; +import com.bang_ggood.question.domain.CustomChecklistQuestion; +import com.bang_ggood.question.domain.Question; +import com.bang_ggood.question.dto.request.CustomChecklistUpdateRequest; +import com.bang_ggood.question.dto.response.CategoryQuestionsResponse; +import com.bang_ggood.question.dto.response.CustomChecklistQuestionsResponse; +import com.bang_ggood.question.dto.response.QuestionResponse; +import com.bang_ggood.question.repository.CustomChecklistQuestionRepository; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class QuestionManageServiceTest extends IntegrationTestSupport { + + @Autowired + private QuestionManageService questionManageService; + + @Autowired + private CustomChecklistQuestionRepository customChecklistQuestionRepository; + + @Autowired + private UserRepository userRepository; + + @DisplayName("커스텀 체크리스트 질문 조회 성공") + @Test + void readChecklistQuestions() { + // given + User user = userRepository.save(UserFixture.USER1()); + List customChecklistQuestions = customChecklistQuestionRepository.saveAll( + CustomChecklistFixture.CUSTOM_CHECKLIST_QUESTION_DEFAULT(user)); + + // when + CustomChecklistQuestionsResponse customChecklistQuestionsResponse = + questionManageService.readCustomChecklistQuestions(user); + + // then + List defaultQuestionsIds = customChecklistQuestions.stream() + .map(CustomChecklistQuestion::getQuestion) + .map(Question::getId) + .toList(); + List responseQuestionsIds = customChecklistQuestionsResponse.categories().stream() + .map(CategoryQuestionsResponse::questions) + .flatMap(Collection::stream) + .map(QuestionResponse::getQuestionId) + .toList(); + + assertThat(responseQuestionsIds).containsExactlyElementsOf(defaultQuestionsIds); + } + + @DisplayName("커스텀 체크리스트 업데이트 성공") + @Test + void updateCustomChecklist() { + // given + User user = userRepository.save(UserFixture.USER1()); + CustomChecklistUpdateRequest request = CustomChecklistFixture.CUSTOM_CHECKLIST_UPDATE_REQUEST(); + + // when & then + assertThatCode(() -> questionManageService.updateCustomChecklist(user, request)) + .doesNotThrowAnyException(); + } + + @DisplayName("커스텀 체크리스트 업데이트 실패 : 선택한 질문 개수가 0개일 때") + @Test + void updateCustomChecklist_empty_exception() { + // given + CustomChecklistUpdateRequest request = CustomChecklistFixture.CUSTOM_CHECKLIST_UPDATE_REQUEST_EMPTY(); + + // when & then + assertThatThrownBy(() -> questionManageService.updateCustomChecklist(UserFixture.USER1(), request)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.CUSTOM_CHECKLIST_QUESTION_EMPTY.getMessage()); + } + + @DisplayName("커스텀 체크리스트 업데이트 실패 : 질문이 중복될 때") + @Test + void updateCustomChecklist_duplicatedQuestion_exception() { + // given + CustomChecklistUpdateRequest request = CustomChecklistFixture.CUSTOM_CHECKLIST_UPDATE_REQUEST_DUPLICATED(); + + // when & then + assertThatThrownBy(() -> questionManageService.updateCustomChecklist(UserFixture.USER1, request)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.QUESTION_DUPLICATED.getMessage()); + } + + @DisplayName("커스텀 체크리스트 업데이트 실패 : 질문 id가 유효하지 않을 때") + @Test + void updateCustomChecklist_invalidQuestionId_exception() { + // given + CustomChecklistUpdateRequest request = CustomChecklistFixture.CUSTOM_CHECKLIST_UPDATE_REQUEST_INVALID(); + + // when & then + assertThatThrownBy(() -> questionManageService.updateCustomChecklist(UserFixture.USER1, request)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.QUESTION_INVALID.getMessage()); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/room/RoomFixture.java b/backend/bang-ggood/src/test/java/com/bang_ggood/room/RoomFixture.java new file mode 100644 index 000000000..060cae5d1 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/room/RoomFixture.java @@ -0,0 +1,69 @@ +package com.bang_ggood.room; + +import com.bang_ggood.contract.domain.OccupancyMonth; +import com.bang_ggood.contract.domain.OccupancyPeriod; +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.room.dto.request.RoomRequest; +import java.util.List; + +public class RoomFixture { + + public static Room ROOM_1() { + return new Room( + "살기 좋은 방", "인천광역시 부평구", null, + "부개", 10, FloorLevel.GROUND, 3, + Structure.TWO_ROOM, 3.5 + ); + } + + public static Room ROOM_2() { + return new Room( + "살기 싫은 방", "대구광역시 중구", "롯데타워", + "대구", 10, FloorLevel.BASEMENT, null, + Structure.DIVIDED_ONE_ROOM, 4.0 + ); + } + + public static Room ROOM_3() { + return new Room( + "살기 애매한 방", "서울특별시 송파구", "루터회관", + "잠실", 5, FloorLevel.ROOFTOP, null, + Structure.DUPLEX, 5.5 + ); + } + + public static RoomRequest ROOM_CREATE_REQUEST() { + return new RoomRequest( + "방이름", "부산광역시 북구", "루터회관", "잠실", + 10, 1000, 50, 5, + List.of(1, 3), FloorLevel.GROUND.getName(), 10, + Structure.TWO_ROOM.getName(), 3.3, 12, OccupancyMonth.APRIL.getMonth(), + OccupancyPeriod.EARLY.getPeriod(), + "방끗공인중개사", "메모", "한줄평" + ); + } + + public static RoomRequest ROOM_UPDATE_REQUEST() { + return new RoomRequest( + "방이름", "부산광역시 루터회관", "잠실역", "루터회관", + 10, 1000, 50, 5, + List.of(1, 3), FloorLevel.GROUND.getName(), 10, + Structure.OPEN_ONE_ROOM.getName(), 3.3, 12, OccupancyMonth.APRIL.getMonth(), + OccupancyPeriod.EARLY.getPeriod(), + "방끗공인중개사", "메모추가", "한줄평" + ); + } + + public static RoomRequest ROOM_CREATE_REQUEST_NO_ROOM_NAME() { + return new RoomRequest( + null, "부산광역시 루터회관", "루터회관", "잠실역", + 10, 1000, 50, 5, + List.of(1, 3), FloorLevel.GROUND.getName(), 10, + Structure.TWO_ROOM.getName(), 3.3, 12, OccupancyMonth.APRIL.getMonth(), + OccupancyPeriod.EARLY.getPeriod(), + "방끗공인중개사", "메모", "한줄평" + ); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/room/domain/FloorLevelTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/room/domain/FloorLevelTest.java new file mode 100644 index 000000000..cca528dae --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/room/domain/FloorLevelTest.java @@ -0,0 +1,44 @@ +package com.bang_ggood.room.domain; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class FloorLevelTest { + + @DisplayName("name으로 FloorLevel 생성 성공") + @Test + void from() { + // given + String name = "반지하/지하"; + + // when & then + assertThat(FloorLevel.from(name)).isEqualTo(FloorLevel.BASEMENT); + } + + @DisplayName("name으로 FloorLevel 생성 성공: null일 경우") + @Test + void from_null() { + // given + String name = null; + + // when & then + assertThat(FloorLevel.from(name)).isEqualTo(FloorLevel.NONE); + } + + @DisplayName("name으로 FloorLevel 생성 실패 : 해당하지 않는 이름일 경우") + @Test + void from_invalidFloorLevel_exception() { + // given + String name = "우주"; + + // when & then + assertThatThrownBy(() -> FloorLevel.from(name)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.FLOOR_LEVEL_INVALID.getMessage()); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/room/domain/RoomTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/room/domain/RoomTest.java new file mode 100644 index 000000000..16469f8c2 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/room/domain/RoomTest.java @@ -0,0 +1,25 @@ +package com.bang_ggood.room.domain; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RoomTest { + + @DisplayName("방 생성 실패: 방이 지상층이 아닌데 floor를 입력했을 경우") + @Test + void createChecklist_roomFloorAndLevelInvalid_exception() { + //given & when & then + assertThatThrownBy(() -> { + new Room( + "방이름", "부산광역시 북구", "루터회관", "잠실", 12, + FloorLevel.BASEMENT, 10, Structure.TWO_ROOM, 7.5 + ); + }) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.ROOM_FLOOR_AND_LEVEL_INVALID.getMessage()); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/room/domain/StructureTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/room/domain/StructureTest.java new file mode 100644 index 000000000..19a269237 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/room/domain/StructureTest.java @@ -0,0 +1,45 @@ +package com.bang_ggood.room.domain; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class StructureTest { + + @DisplayName("name으로 Structure 생성 성공") + @Test + void from() { + // given + String name = "복층"; + + // when & then + assertThat(Structure.from(name)).isEqualTo(Structure.DUPLEX); + } + + @DisplayName("name으로 Structure 생성 성공: null일 경우") + @Test + void from_null() { + // given + String name = null; + + // when & then + assertThat(Structure.from(name)).isEqualTo(Structure.NONE); + } + + @DisplayName("name으로 Structure 생성 실패 : 해당하지 않는 이름일 경우") + @Test + void from_invalidStructure_exception() { + // given + String name = "제제"; + + // when & then + assertThatThrownBy(() -> Structure.from(name)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.STRUCTURE_INVALID.getMessage()); + } + +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/room/repository/RoomRepositoryTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/room/repository/RoomRepositoryTest.java new file mode 100644 index 000000000..b508cedcf --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/room/repository/RoomRepositoryTest.java @@ -0,0 +1,46 @@ +package com.bang_ggood.room.repository; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class RoomRepositoryTest extends IntegrationTestSupport { + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoomRepository roomRepository; + + + @BeforeEach + void setUp() { + userRepository.save(UserFixture.USER1()); + } + + + @DisplayName("방 논리적 삭제 성공") + @Test + void deleteById() { + //given + Room saved = roomRepository.save(RoomFixture.ROOM_1()); + + //when + roomRepository.deleteById(saved.getId()); + + //then + assertAll( + () -> assertThat(roomRepository.existsById(saved.getId())).isTrue(), + () -> assertThat(roomRepository.findById(saved.getId()).get().isDeleted()).isTrue() + ); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/room/service/RoomServiceTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/room/service/RoomServiceTest.java new file mode 100644 index 000000000..9007ba185 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/room/service/RoomServiceTest.java @@ -0,0 +1,67 @@ +package com.bang_ggood.room.service; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.repository.RoomRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class RoomServiceTest extends IntegrationTestSupport { + + @Autowired + private RoomService roomService; + + @Autowired + private RoomRepository roomRepository; + + @DisplayName("방 작성 성공") + @Test + void createRoom() { + //given + Room room = RoomFixture.ROOM_1(); + + //when + Room savedRoom = roomService.createRoom(room); + + //then + assertThat(savedRoom.getName()).isEqualTo(room.getName()); + } + + @DisplayName("방 삭제 성공") + @Test + void deleteById() { + //given + Room room = RoomFixture.ROOM_1(); + Room savedRoom = roomService.createRoom(room); + + //when + roomService.deleteById(savedRoom.getId()); + + //then + assertThat(roomRepository.findById(savedRoom.getId()).get().isDeleted()).isTrue(); + } + + @DisplayName("방 수정 성공") + @Test + void updateRoom() { + // given + Room room = RoomFixture.ROOM_1(); + Room updateRoom = RoomFixture.ROOM_2(); + roomService.createRoom(room); + + // when + roomService.updateRoom(room, updateRoom); + + // then + assertAll( + () -> assertThat(room.getName()).isEqualTo(updateRoom.getName()), + () -> assertThat(room.getAddress()).isEqualTo(updateRoom.getAddress()), + () -> assertThat(room.getBuildingName()).isEqualTo(updateRoom.getBuildingName()) + ); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/station/repository/ChecklistStationRepositoryTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/station/repository/ChecklistStationRepositoryTest.java new file mode 100644 index 000000000..81cfbf12b --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/station/repository/ChecklistStationRepositoryTest.java @@ -0,0 +1,82 @@ +package com.bang_ggood.station.repository; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.checklist.ChecklistFixture; +import com.bang_ggood.checklist.domain.Checklist; +import com.bang_ggood.checklist.repository.ChecklistRepository; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.repository.RoomRepository; +import com.bang_ggood.station.domain.ChecklistStation; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ChecklistStationRepositoryTest extends IntegrationTestSupport { + + @Autowired + RoomRepository roomRepository; + + @Autowired + ChecklistRepository checklistRepository; + + @Autowired + UserRepository userRepository; + + @Autowired + ChecklistStationRepository checklistStationRepository; + + Room room; + User user; + Checklist checklist; + + @BeforeEach + void setUp() { + room = roomRepository.save(RoomFixture.ROOM_1()); + user = userRepository.save(UserFixture.USER1()); + checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + } + + @DisplayName("체크리스트 아이디를 통한 조회 성공") + @Test + void findByChecklistId() { + // given + ChecklistStation checklistStation1 = new ChecklistStation(checklist, "잠실", "2호선", 5); + ChecklistStation checklistStation2 = new ChecklistStation(checklist, "잠실", "8호선", 6); + checklistStationRepository.saveAll(List.of(checklistStation1, checklistStation2)); + + // when & then + assertThat(checklistStationRepository.findByChecklist(checklist)) + .containsExactlyInAnyOrder(checklistStation1, checklistStation2); + } + + @DisplayName("체크리스트 아이디를 통한 조회 실패: 저장된 값이 없는 경우") + @Test + void findByChecklistId_noData_exception() { + // given & when & then + assertThat(checklistStationRepository.findByChecklist(checklist)) + .isEmpty(); + } + + @DisplayName("체크리스트 아이디를 통한 삭제 성공") + @Test + void deleteAllByChecklistId_noData_exception() { + // given + ChecklistStation checklistStation1 = new ChecklistStation(checklist, "잠실", "2호선", 5); + ChecklistStation checklistStation2 = new ChecklistStation(checklist, "잠실", "8호선", 6); + checklistStationRepository.saveAll(List.of(checklistStation1, checklistStation2)); + + // when + checklistStationRepository.deleteAllByChecklistId(checklist.getId()); + + // then + assertThat(checklistStationRepository.findByChecklist(checklist)).isEmpty(); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/station/service/ChecklistStationServiceTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/station/service/ChecklistStationServiceTest.java new file mode 100644 index 000000000..522788bfa --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/station/service/ChecklistStationServiceTest.java @@ -0,0 +1,73 @@ +package com.bang_ggood.station.service; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.checklist.ChecklistFixture; +import com.bang_ggood.checklist.domain.Checklist; +import com.bang_ggood.checklist.repository.ChecklistRepository; +import com.bang_ggood.room.RoomFixture; +import com.bang_ggood.room.domain.Room; +import com.bang_ggood.room.repository.RoomRepository; +import com.bang_ggood.station.domain.ChecklistStation; +import com.bang_ggood.station.repository.ChecklistStationRepository; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ChecklistStationServiceTest extends IntegrationTestSupport { + + @Autowired + RoomRepository roomRepository; + + @Autowired + ChecklistRepository checklistRepository; + + @Autowired + UserRepository userRepository; + + @Autowired + ChecklistStationService checklistStationService; + + @Autowired + ChecklistStationRepository checklistStationRepository; + + Room room; + User user; + Checklist checklist; + + @BeforeEach + void setUp() { + room = roomRepository.save(RoomFixture.ROOM_1()); + user = userRepository.save(UserFixture.USER1()); + checklist = checklistRepository.save(ChecklistFixture.CHECKLIST1_USER1(room, user)); + } + + @DisplayName("ChecklistStation 객체 생성 성공") + @Test + void createChecklistStations() { + // given & when + checklistStationService.createChecklistStations(checklist, 38, 127); + + // then + assertThat(checklistStationRepository.findByChecklist(checklist)).isNotEmpty(); + } + + @DisplayName("ChecklistStation 조회 성공") + @Test + void readChecklistStations() { + // given + ChecklistStation checklistStation1 = new ChecklistStation(checklist, "잠실", "2호선", 5); + ChecklistStation checklistStation2 = new ChecklistStation(checklist, "잠실", "8호선", 6); + checklistStationRepository.saveAll(List.of(checklistStation1, checklistStation2)); + + // when & then + assertThat(checklistStationService.readChecklistStationsByChecklist(checklist)) + .containsExactlyInAnyOrder(checklistStation1, checklistStation2); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/station/service/SubwayStationServiceTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/station/service/SubwayStationServiceTest.java new file mode 100644 index 000000000..e54ca72a8 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/station/service/SubwayStationServiceTest.java @@ -0,0 +1,66 @@ +package com.bang_ggood.station.service; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.station.dto.response.SubwayStationResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +public class SubwayStationServiceTest extends IntegrationTestSupport { + + @Autowired + SubwayStationService subwayStationService; + + // check data in "https://apis.map.kakao.com/web/sample/addMapClickEventWithMarker/" + private static Stream provideStationData() { + return Stream.of( + Arguments.of(37.517406150696104, 127.10333134512422, + new Station("잠실(송파구청)", List.of("2호선", "8호선")), + new Station("잠실나루", List.of("2호선"))), + Arguments.of(37.50808977595056, 127.04649615866747, + new Station("선정릉", List.of("9호선", "수인분당선")), + new Station("선릉", List.of("2호선", "수인분당선"))), + Arguments.of(37.538999998345446, 126.97201837726666, + new Station("남영", List.of("1호선")), + new Station("삼각지", List.of("4호선", "6호선"))) + ); + } + + @DisplayName("가까운 지하철 2개 조회 성공") + @ParameterizedTest + @MethodSource("provideStationData") + void readNearestStation(double latitude, double longitude, Station nearestStation, Station nextNearestStation) { + // given & when + List responses = subwayStationService.readNearestStation(latitude, longitude) + .getStations(); + assertThat(responses).hasSize(2); + SubwayStationResponse nearest = responses.get(0); + SubwayStationResponse nextNearest = responses.get(1); + + // then + assertAll(() -> { + assertThat(nearest.getStationName()).isEqualTo(nearestStation.name); + assertThat(nearest.getStationLine()).containsAll(nearestStation.lines); + assertThat(nextNearest.getStationName()).isEqualTo(nextNearestStation.name); + assertThat(nextNearest.getStationLine()).containsAll(nextNearestStation.lines); + } + ); + } + + private static class Station { + String name; + List lines; + + public Station(String name, List lines) { + this.name = name; + this.lines = lines; + } + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/user/UserFixture.java b/backend/bang-ggood/src/test/java/com/bang_ggood/user/UserFixture.java new file mode 100644 index 000000000..2e84fd375 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/user/UserFixture.java @@ -0,0 +1,54 @@ +package com.bang_ggood.user; + +import com.bang_ggood.auth.dto.response.KakaoAccountResponse; +import com.bang_ggood.auth.dto.response.OauthInfoApiResponse; +import com.bang_ggood.auth.dto.response.ProfileResponse; +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; + +public class UserFixture { + + public static User USER1; + + public static User USER1() { + return new User("방방이", "bang-bang01@gmail.com", "password1234", UserType.USER, LoginType.LOCAL); + } + + public static User USER2() { + return new User("빵빵이", "bbang-bbang@gmail.com", "password1234", UserType.USER, LoginType.LOCAL); + } + + public static User GUEST_USER1() { + return new User("빵빵이", "bbang-bbang1@gmail.com", UserType.GUEST, LoginType.LOCAL); + } + + public static User GUEST_USER2() { + return new User("빵빵이", "bbang-bbang2@gmail.com", UserType.GUEST, LoginType.LOCAL); + } + + public static User USER1_WITH_ID() { + return new User(1L, "방방이", "bang-bang@gmail.com"); + } + + public static User USER2_WITH_ID() { + return new User(2L, "빵빵이", "bbang-bbang@gmail.com"); + } + + public static OauthInfoApiResponse OAUTH_INFO_RESPONSE_USER1() { + return new OauthInfoApiResponse("", "", + new KakaoAccountResponse(USER1().getEmail().getValue(), USER1().getName(), + new ProfileResponse("", "", ""))); + } + + public static OauthInfoApiResponse OAUTH_INFO_RESPONSE_USER2() { + return new OauthInfoApiResponse("", "", + new KakaoAccountResponse(USER2().getEmail().getValue(), USER2().getName(), + new ProfileResponse("", "", ""))); + } + + public static void init(UserRepository userRepository) { + USER1 = userRepository.save(new User("방방이", "bang-bang@gmail.com", UserType.USER, LoginType.LOCAL)); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/user/controller/UserE2ETest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/user/controller/UserE2ETest.java new file mode 100644 index 000000000..39047a2c4 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/user/controller/UserE2ETest.java @@ -0,0 +1,35 @@ +package com.bang_ggood.user.controller; + +import com.bang_ggood.AcceptanceTest; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.dto.UserResponse; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class UserE2ETest extends AcceptanceTest { + + @DisplayName("유저 정보 조회 성공") + @Test + void readUserInfo() { + User user = this.getAuthenticatedUser(); + UserResponse response = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .headers(this.headers) + .when().get("/user/me") + .then().log().all() + .statusCode(200) + .extract() + .as(UserResponse.class); + + assertAll( + () -> assertThat(response.userId()).isEqualTo(user.getId()), + () -> assertThat(response.userName()).isEqualTo(user.getName()), + () -> assertThat(response.userEmail()).isEqualTo(user.getEmail().getValue()) + ); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/user/domain/EmailTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/user/domain/EmailTest.java new file mode 100644 index 000000000..2160828e9 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/user/domain/EmailTest.java @@ -0,0 +1,66 @@ +package com.bang_ggood.user.domain; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class EmailTest { + + @DisplayName("이메일 형식에 맞는 이메일 생성 성공") + @Test + void createEmail() { + assertThatCode(() -> new Email("abc@gmail.com")) + .doesNotThrowAnyException(); + } + + @DisplayName("서브 도메인 존재할 시 이메일 생성 성공") + @Test + void createEmail_subDomain() { + assertThatCode(() -> new Email("abc@mail.gmail.com")) + .doesNotThrowAnyException(); + } + + @DisplayName("이메일 생성 실패 : local-part가 존재하지 않는 경우") + @Test + void createPassword_emptyLocalPart_exception() { + assertThatThrownBy(() -> new Email("@gmail.com")) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.EMAIL_INVALID_FORMAT.getMessage()); + } + + @DisplayName("이메일 생성 실패 : 2차 도메인이 존재하지 않는 경우") + @Test + void createPassword_emptySecondLevelDomain_exception() { + assertThatThrownBy(() -> new Email("abc@.com")) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.EMAIL_INVALID_FORMAT.getMessage()); + } + + @DisplayName("이메일 생성 실패 : 최상위 도메인이 존재하지 않는 경우") + @Test + void createPassword_emptyTopLevelDomain_exception() { + assertThatThrownBy(() -> new Email("abc@gmail")) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.EMAIL_INVALID_FORMAT.getMessage()); + } + + @DisplayName("이메일 생성 실패 : 최상위 도메인이 한글자인 경우") + @Test + void createPassword_oneWordTopLevelDomain_exception() { + assertThatThrownBy(() -> new Email("abc@gmail.g")) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.EMAIL_INVALID_FORMAT.getMessage()); + } + + @DisplayName("이메일 생성 실패 : 공백이 포함된 경우") + @Test + void createPassword_withWhitespace_exception() { + assertThatThrownBy(() -> new Email("abc @gmail.com")) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.EMAIL_INVALID_FORMAT.getMessage()); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/user/domain/PasswordTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/user/domain/PasswordTest.java new file mode 100644 index 000000000..9a5afacea --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/user/domain/PasswordTest.java @@ -0,0 +1,105 @@ +package com.bang_ggood.user.domain; + +import com.bang_ggood.global.exception.BangggoodException; +import com.bang_ggood.global.exception.ExceptionCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PasswordTest { + + @DisplayName("영어와 숫자를 모두 포함하고 6자 이상인 비밀번호 생성 성공") + @Test + void createPassword() { + assertThatCode(() -> new Password("password1234")) + .doesNotThrowAnyException(); + } + + @DisplayName("영어, 숫자, 특수문자를 모두 포함하고 6자 이상인 비밀번호 생성 성공") + @Test + void createPassword_containSpecialCharacter() { + assertThatCode(() -> new Password("password1234!@")) + .doesNotThrowAnyException(); + } + + @DisplayName("비밀번호 생성 실패 : 허용되지 않은 특수문자일 경우") + @Test + void createPassword_InvalidSpecialCharacter_exception() { + assertThatThrownBy(() -> new Password("password1234]")) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.PASSWORD_INVALID_FORMAT.getMessage()); + } + + @DisplayName("비밀번호 생성 실패 : 공백이 포함되는 경우") + @Test + void createPassword_containEmpty_exception() { + assertThatThrownBy(() -> new Password("password 1234")) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.PASSWORD_INVALID_FORMAT.getMessage()); + } + + @DisplayName("비밀번호 생성 실패 : null일 경우") + @Test + void createPassword_null_exception() { + assertThatThrownBy(() -> new Password(null)) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.PASSWORD_INVALID_FORMAT.getMessage()); + } + + @DisplayName("비밀번호 생성 실패 : 영어가 포함되지 않은 경우") + @Test + void createPassword_notContainEnglish_exception() { + assertThatThrownBy(() -> new Password("123456")) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.PASSWORD_INVALID_FORMAT.getMessage()); + } + + @DisplayName("비밀번호 생성 실패 : 숫자가 포함되지 않은 경우") + @Test + void createPassword_notContainNumber_exception() { + assertThatThrownBy(() -> new Password("password")) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.PASSWORD_INVALID_FORMAT.getMessage()); + } + + @DisplayName("비밀번호 생성 실패 : 영어와 숫자가 포함되지 않은 경우") + @Test + void createPassword_notContainEnglishAndNumber_exception() { + assertThatThrownBy(() -> new Password("!@#$%^")) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.PASSWORD_INVALID_FORMAT.getMessage()); + } + + @DisplayName("비밀번호 생성 실패 : 길이가 6자 미만인 경우") + @Test + void createPassword_lessThan6_exception() { + assertThatThrownBy(() -> new Password("pas12")) + .isInstanceOf(BangggoodException.class) + .hasMessage(ExceptionCode.PASSWORD_INVALID_FORMAT.getMessage()); + } + + @DisplayName("비밀번호 다름 확인: 일치하는 경우") + @Test + void isDifferent_false() { + // given + String passwordValue = "password1234"; + Password password = new Password(passwordValue); + + // when & then + assertThat(password.isDifferent(passwordValue)).isFalse(); + } + + @DisplayName("비밀번호 다름 확인: 일치하지 않는 경우") + @Test + void isDifferent_true() { + // given + String passwordValue = "password1234"; + Password password = new Password(passwordValue); + + // when & then + assertThat(password.isDifferent(passwordValue)).isFalse(); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/user/domain/UserTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/user/domain/UserTest.java new file mode 100644 index 000000000..96cb72e2d --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/user/domain/UserTest.java @@ -0,0 +1,32 @@ +package com.bang_ggood.user.domain; + +import com.bang_ggood.user.UserFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class UserTest { + + @DisplayName("비밀번호 불일치 확인 성공: 일치하는 경우") + @Test + void passwordEqual_false() { + //given + User user = UserFixture.USER1(); + String password = "password1234"; + + //when & then + assertThat(user.isDifferent(password)).isFalse(); + } + + @DisplayName("비밀번호 불일치 확인 성공: 일치하지 않는 경우") + @Test + void passwordEqual_true() { + //given + User user = UserFixture.USER1(); + String password = "passwords12345"; + + //when & then + assertThat(user.isDifferent(password)).isTrue(); + } +} diff --git a/backend/bang-ggood/src/test/java/com/bang_ggood/user/repository/UserRepositoryTest.java b/backend/bang-ggood/src/test/java/com/bang_ggood/user/repository/UserRepositoryTest.java new file mode 100644 index 000000000..9ae7d06b4 --- /dev/null +++ b/backend/bang-ggood/src/test/java/com/bang_ggood/user/repository/UserRepositoryTest.java @@ -0,0 +1,98 @@ +package com.bang_ggood.user.repository; + +import com.bang_ggood.IntegrationTestSupport; +import com.bang_ggood.user.UserFixture; +import com.bang_ggood.user.domain.User; +import com.bang_ggood.user.domain.UserType; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class UserRepositoryTest extends IntegrationTestSupport { + + @Autowired + private UserRepository userRepository; + + @DisplayName("유저 이메일 및 로그인 타입 조회 성공 : 유저 삭제 후 유저를 조회하면(논리적 삭제) 조회되지 않는다.") + @Test + void findByEmail() { + // given + User user = userRepository.save(UserFixture.USER1()); + userRepository.deleteById(user.getId()); + + // when + Optional findUser = userRepository.findByEmailAndLoginType(user.getEmail(), user.getLoginType()); + + // then + assertThat(findUser).isEmpty(); + } + + @DisplayName("유저 타입으로 조회 성공") + @Test + void findByType() { + // given + User expectedUser = userRepository.save(UserFixture.GUEST_USER1()); + + // when + List users = userRepository.findUserByUserType(UserType.GUEST); + + // then + assertThat(users).containsExactly(expectedUser); + } + + @DisplayName("논리적 삭제 성공") + @Test + void deleteById() { + // given + User user = userRepository.save(UserFixture.USER1()); + + // when + userRepository.deleteById(user.getId()); + + // then + Optional findUser = userRepository.findById(user.getId()); + assertThat(findUser).isEmpty(); + } + + @DisplayName("이메일과 로그인 타입으로 재저장 성공") + @Test + void resaveByEmailAndLoginType() { + // given + User user = userRepository.save(UserFixture.USER1()); + userRepository.deleteById(user.getId()); + + // when + userRepository.resaveByEmailAndLoginType(user.getEmail(), user.getLoginType()); + + // then + Optional deletedUser = userRepository.findByEmailAndLoginType(user.getEmail(), user.getLoginType()); + assertAll( + () -> assertThat(deletedUser).isPresent(), + () -> assertThat(deletedUser.get().isDeleted()).isFalse() + ); + } + + @DisplayName("논리적 삭제된 유저를 포함해 이메일과 로그인 타입으로 조회 성공") + @Test + void findByEmailAndLoginTypeWithDeleted() { + // given + User user = userRepository.save(UserFixture.USER1()); + userRepository.deleteById(user.getId()); + + // when + Optional deletedUser = userRepository.findByEmailAndLoginTypeWithDeleted(user.getEmail(), user.getLoginType()); + + // then + assertAll( + () -> assertThat(deletedUser).isPresent(), + () -> assertThat(deletedUser.get().isDeleted()).isTrue() + ); + } +} diff --git a/backend/bang-ggood/src/test/resources/data-test.sql b/backend/bang-ggood/src/test/resources/data-test.sql new file mode 100644 index 000000000..84761f800 --- /dev/null +++ b/backend/bang-ggood/src/test/resources/data-test.sql @@ -0,0 +1,103 @@ +-- 비밀번호 : password1234 +INSERT INTO users(name, email, password, user_type, login_type, created_at, modified_at, deleted) +VALUES ('방방이', 'bang-ggood@gmail.com', + 'xDNYKEJqE/36U0Dt3nXRMFPNEMEgjCYM7R/A4B29baOsv4KYQ9MGgcO3HUa11sNKCFb9ZXyYBqJqxNglvBzFvg==:7yejAszEpxBb7AyZNKvAqpmMEJiKFXIa8JKwAx3n4loB2DRcAC2pfwkgo/dzKzRvBX4RbrATWaIlPYrgAhbHZQ==', + 'USER', 'LOCAL', '2024-07-22 07:56:42', '2024-07-22 07:56:42', false); + +INSERT INTO category (name) +VALUES + ('방 컨디션'), + ('창문'), + ('화장실'), + ('보안'), + ('외부'); + +INSERT INTO question (category_id, title, subtitle, is_default) +VALUES + (1, '곰팡이가 핀 곳 없이 깨끗한가요?', '천장, 벽면, 가구 뒤, 장판을 확인하세요.', true), + (1, '불쾌한 냄새 없이 쾌적한가요?', null, true), + (1, '벌레가 나온 흔적 없이 깔끔한가요?', '벌레 퇴치약이 부착되어 있는지 확인하세요.', true), + (1, '물건을 충분히 수납할 수 있는 공간이 있나요?', null, true), + (1, '방 인테리어는 괜찮나요?', null, true), + (1, '에어컨의 상태는 괜찮은가요?', '에어컨을 틀어서 불쾌한 냄새가 나진 않는지 확인하세요.', false), + (1, '보일러가 잘 동작하나요?', null, false), + (1, '콘센트 위치와 개수가 적절한가요?', null, false), + (1, '벽지 상태가 양호한가요?', null, false), + (2, '창 밖의 뷰가 가로막힘 없이 트여있나요?', null, true), + (2, '창문 상태가 괜찮나요?', null, true), + (2, '환기가 잘 되는 구조인가요?', '창문 크기와 방향을 확인하세요.', true), + (2, '햇빛이 잘 들어오나요?', null, true), + (2, '창문이 이중창인가요?', null, false), + (2, '창문 밖에 쓰레기통 등 냄새가 나는 요소가 있나요?', null, false), + (3, '화장실이 깨끗한가요?', '청소 가능한 얼룩인지 확인하세요.', true), + (3, '수압 및 물 빠짐이 괜찮은가요?', '화장실에서 수도와 변기를 동시에 사용해보세요.', true), + (3, '환기 시설이 있나요?', null, true), + (3, '내부에 창문이 있나요?', null, false), + (3, '온수가 잘 나오나요?', null, false), + (4, '잠금장치가 있는 공동 현관문이 있나요?', null, true), + (4, '출입구와 복도에 CCTV가 설치되어 있나요?', null, true), + (4, '관리자분이 함께 상주하시나요?', '관리자분이 24시간 상주하시는지 확인하세요.', true), + (4, '보안 시설이 잘 갖추어져 있나요?', '도어락, 창문 잠금장치 등이 있는지 확인하세요.', false), + (4, '화면이 달린 인터폰이 제공되나요?', null, false), + (4, '현관문에 걸쇠가 있나요?', null, false), + (5, '주변 도로가 밤에도 충분히 밝은가요?', null, false), + (5, '주변에 소음 시설이 있는지 확인했나요?', '유흥시설, 놀이터, 공사장이 있는지 확인하세요.', false), + (5, '1층에 음식점이 있는지 확인했나요?', null, false), + (5, '집 가는 길이 언덕 없이 완만한가요?', null, false), + (5, '옆 건물에서 보이는 구조인지 확인했나요?', null, false), + (5, '주차할 수 있는 시설이 있나요?', null, false) +; +INSERT INTO highlight (question_id, name) +VALUES + (1, '곰팡이'), + (2, '불쾌한 냄새'), + (3, '벌레'), + (4, '수납할 수 있는 공간'), + (5, '방 인테리어'), + (6, '에어컨'), + (7, '보일러'), + (8, '콘센트'), + (9, '벽지 상태'), + (10, '창 밖의 뷰'), + (11, '창문 상태'), + (12, '환기'), + (13, '햇빛'), + (14, '이중창'), + (15, '냄새가 나는 요소'), + (16, '깨끗'), + (17, '수압 및 물 빠짐'), + (18, '환기 시설'), + (19, '창문'), + (20, '온수'), + (21, '잠금장치'), + (21, '공동 현관문'), + (22, 'CCTV'), + (23, '관리자분'), + (24, '보안 시설'), + (25, '인터폰'), + (26, '걸쇠'), + (27, '주변 도로'), + (27, '밝은가요'), + (28, '소음 시설'), + (29, '음식점'), + (30, '언덕'), + (31, '보이는 구조'), + (32, '주차할 수 있는 시설') +; + +INSERT INTO custom_checklist_question(user_id, question, question_id, created_at, modified_at, deleted) +VALUES (1, 'ROOM_CONDITION_1',1, '2024-07-22 07:56:42', '2024-07-22 07:56:42', false), + (1, 'ROOM_CONDITION_2',2, '2024-07-22 07:56:43', '2024-07-22 07:56:43', false), + (1, 'ROOM_CONDITION_3',3, '2024-07-22 07:56:44', '2024-07-22 07:56:44', false), + (1, 'ROOM_CONDITION_4',4, '2024-07-22 07:56:45', '2024-07-22 07:56:45', false), + (1, 'ROOM_CONDITION_5',5, '2024-07-22 07:56:46', '2024-07-22 07:56:46', false), + (1, 'WINDOW_1',6, '2024-07-22 07:56:47', '2024-07-22 07:56:47', false), + (1, 'WINDOW_2',7, '2024-07-22 07:56:48', '2024-07-22 07:56:48', false), + (1, 'WINDOW_3',8, '2024-07-22 07:56:49', '2024-07-22 07:56:49', false), + (1, 'WINDOW_4',9, '2024-07-22 07:56:50', '2024-07-22 07:56:50', false), + (1, 'BATHROOM_1',10, '2024-07-22 07:56:51', '2024-07-22 07:56:51', false), + (1, 'BATHROOM_2',11, '2024-07-22 07:56:52', '2024-07-22 07:56:52', false), + (1, 'BATHROOM_3',12, '2024-07-22 07:56:53', '2024-07-22 07:56:53', false), + (1, 'SECURITY_1',13, '2024-07-22 07:56:54', '2024-07-22 07:56:54', false), + (1, 'SECURITY_2',14, '2024-07-22 07:56:55', '2024-07-22 07:56:55', false), + (1, 'SECURITY_3',15, '2024-07-22 07:56:56', '2024-07-22 07:56:56', false); diff --git a/backend/bang-ggood/src/test/resources/schema-test.sql b/backend/bang-ggood/src/test/resources/schema-test.sql new file mode 100644 index 000000000..67d0bdc92 --- /dev/null +++ b/backend/bang-ggood/src/test/resources/schema-test.sql @@ -0,0 +1,188 @@ +-- Drop tables if they exist +DROP TABLE IF EXISTS test_entity CASCADE; +DROP TABLE IF EXISTS checklist_station CASCADE; +DROP TABLE IF EXISTS checklist_like CASCADE; +DROP TABLE IF EXISTS custom_checklist_question CASCADE; +DROP TABLE IF EXISTS checklist_option CASCADE; +DROP TABLE IF EXISTS checklist_question CASCADE; +DROP TABLE IF EXISTS checklist_maintenance CASCADE; +DROP TABLE IF EXISTS checklist CASCADE; +DROP TABLE IF EXISTS article CASCADE; +DROP TABLE IF EXISTS users CASCADE; +DROP TABLE IF EXISTS room CASCADE; +DROP TABLE IF EXISTS highlight CASCADE; +DROP TABLE IF EXISTS question CASCADE; +DROP TABLE IF EXISTS category CASCADE; + +-- Create tables + +CREATE TABLE category +( + id INTEGER AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) +); + +CREATE TABLE question +( + id INTEGER AUTO_INCREMENT PRIMARY KEY, + category_id INTEGER, + title VARCHAR(255), + subtitle VARCHAR(255), + is_default BOOLEAN, + FOREIGN KEY (category_id) REFERENCES category (id) +); + +CREATE TABLE highlight +( + id INTEGER AUTO_INCREMENT PRIMARY KEY, + question_id INTEGER, + name VARCHAR(255), + FOREIGN KEY (question_id) REFERENCES question (id) +); + +CREATE TABLE room +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255), + address VARCHAR(255), + building_name VARCHAR(255), + station VARCHAR(255), + walking_time INTEGER, + floor_level VARCHAR(255), + floor INTEGER, + structure VARCHAR(255), + size DOUBLE, + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN +); + +CREATE TABLE users +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255), + email VARCHAR(255) NOT NULL, + password VARCHAR(255), + user_type VARCHAR(255) NOT NULL, + login_type VARCHAR(255) NOT NULL, + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN, + CONSTRAINT unique_email_login_type UNIQUE (email, login_type) +); + +CREATE TABLE checklist +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + room_id BIGINT NOT NULL UNIQUE, + user_id BIGINT NOT NULL, + deposit INTEGER, + rent INTEGER, + maintenance_fee INTEGER, + contract_term INTEGER, + occupancy_month VARCHAR(255), + occupancy_period VARCHAR(255), + real_estate VARCHAR(255), + memo VARCHAR(1000), + summary VARCHAR(255), + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN, + FOREIGN KEY (room_id) REFERENCES room (id), + FOREIGN KEY (user_id) REFERENCES users (id) +); + +CREATE TABLE checklist_maintenance +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + checklist_id BIGINT, + maintenance_item VARCHAR(255), + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN, + FOREIGN KEY (checklist_id) REFERENCES checklist (id) +); + +CREATE TABLE checklist_question +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + question VARCHAR(255), + question_id INTEGER NOT NULL, + checklist_id BIGINT NOT NULL, + answer VARCHAR(255), + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN, + FOREIGN KEY (checklist_id) REFERENCES checklist (id), + FOREIGN KEY (question_id) REFERENCES question (id) +); +CREATE TABLE checklist_option +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + option_id INTEGER NOT NULL, + checklist_id BIGINT NOT NULL, + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN, + FOREIGN KEY (checklist_id) REFERENCES checklist (id) +); + +CREATE TABLE custom_checklist_question +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT, + question VARCHAR(255), + question_id INTEGER, + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN, + FOREIGN KEY (user_id) references users (id), + FOREIGN KEY (question_id) references question (id) +); + +CREATE TABLE test_entity +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name varchar(255) not null, + created_at TIMESTAMP not null, + modified_at TIMESTAMP not null, + deleted BOOLEAN, + primary key (id) +); + +CREATE TABLE checklist_like +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + checklist_id BIGINT, + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN, + FOREIGN KEY (checklist_id) REFERENCES checklist (id) +); + +CREATE TABLE article +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255), + content TEXT, + keyword VARCHAR(255), + summary VARCHAR(255), + thumbnail VARCHAR(2500), + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN +); + +CREATE TABLE checklist_station +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + checklist_id BIGINT, + station_name VARCHAR(255), + station_line VARCHAR(255), + walking_time INTEGER, + created_at TIMESTAMP(6), + modified_at TIMESTAMP(6), + deleted BOOLEAN, + FOREIGN KEY (checklist_id) REFERENCES checklist (id) +); + diff --git a/backend/bang-ggood/src/test/resources/seoul_stations_240819.csv b/backend/bang-ggood/src/test/resources/seoul_stations_240819.csv new file mode 100644 index 000000000..21d1ae50f --- /dev/null +++ b/backend/bang-ggood/src/test/resources/seoul_stations_240819.csv @@ -0,0 +1,768 @@ +_ID,,ȣ,,浵 +4703,4.19ֹ,̽ż,37.649502,127.013684 +1907,,1ȣ,37.748577,127.044213 +340,,3ȣ,37.492245,127.117757 +2818,,8ȣ,37.492888,127.118398 +2748,д,7ȣ,37.480338,126.882656 +1702,д,1ȣ,37.481581,126.882581 +4107,,9ȣ,37.561391,126.854456 +4704,,̽ż,37.641537,127.016789 +3216,,õ2ȣ,37.484192,126.683673 +3211,(Ƽ),õ2ȣ,37.524649,126.675539 +3212,߾ӽ,õ2ȣ,37.517054,126.676672 +1265,,߾Ӽ,37.568491,126.915487 +1851,õ,д缱,37.448605,127.126697 +1323,,ἱ,37.814536,127.510739 +1816,,1ȣ,37.464737,126.694181 +3123,Ÿ,õ1ȣ,37.467048,126.707938 +1312,,ἱ,37.634118,127.114757 +3117,,õ1ȣ,37.517268,126.721514 +222,,2ȣ,37.49799,127.027912 +4307,,źд缱,37.496837,127.028104 +2732,û,7ȣ,37.517179,127.041255 +1849,û,д缱,37.517469,127.041151 +4502,,μ,37.270161,127.126033 +2549,,5ȣ,37.535804,127.132481 +2813,û,8ȣ,37.530341,127.120508 +1269,,߾Ӽ,37.612314,126.843223 +214,(͹̳),2ȣ,37.535095,127.094681 +9995,,5ȣ,37.55749,127.17593 +1326,,ἱ,37.805723,127.634146 +2559,,5ȣ,37.498079,127.13482 +1801,,1ȣ,37.494594,126.85968 +1027,,д缱,37.489116,127.06614 +4101,ȭ,9ȣ,37.578608,126.798153 +2512,ȭ,5ȣ,37.572399,126.806171 +2560,ſ,5ȣ,37.493105,127.14415 +212,ǴԱ,2ȣ,37.540373,127.069191 +2729,ǴԱ,7ȣ,37.540786,127.071011 +4925,Ϻ,,37.63165,126.705975 +3203,˴ܻŸ,õ2ȣ,37.60185,126.657108 +3201,˴ܿ(˴ܻ),õ2ȣ,37.594877,126.627178 +3208,˹,õ2ȣ,37.561405,126.677566 +4209,˾,ö1ȣ,37.569098,126.674007 +3207,˾,õ2ȣ,37.56866,126.675687 +1504,Ɽ,氭,37.399907,127.25263 +4610,⵵ûϺû,μ,37.75059,127.071495 +1451,渶,4ȣ,37.443885,127.007888 +317,溹(μû),3ȣ,37.575762,126.97353 +3115,αԱ,õ1ȣ,37.538157,126.722597 +4604,ö,μ,37.737202,127.043257 +341,,3ȣ,37.495918,127.12454 +3114,,õ1ȣ,37.543238,126.728128 +4208,,ö1ȣ,37.571662,126.7363 +3110,,õ1ȣ,37.571449,126.73578 +2553,,5ȣ,37.555004,127.154151 +2641,(),6ȣ,37.590508,127.036296 +1873,,μ,37.24963,126.980248 +329,͹̳,3ȣ,37.504891,127.004916 +2736,͹̳,7ȣ,37.503367,127.005068 +4123,͹̳,9ȣ,37.50598,127.004403 +1757,,4ȣ,37.316784,126.823144 +4513,,μ,37.24484,127.214251 +4928,,,37.601243,126.770345 +1272,,߾Ӽ,37.645676,126.801762 +4612,,μ,37.750471,127.083715 +1506,,氭,37.351315,127.34674 +2530,,5ȣ,37.544431,126.951372 +2627,,6ȣ,37.543555,126.951678 +1292,,߾Ӽ,37.542596,126.952099 +4202,,ö1ȣ,37.54253,126.952024 +2718,(б),7ȣ,37.625742,127.072896 +4103,׽,9ȣ,37.563726,126.810678 +4212,ȭû,ö1ȣ,37.459041,126.477516 +1453,õ,4ȣ,37.433021,126.996568 +1705,,1ȣ,37.419232,126.908706 +4411,ǻ(),Ÿ,37.4691018,126.9450639 +4319,(),źд缱,37.30211,127.044483 +4318,߾(ִ),źд缱,37.288617,127.051478 +2547,(Ŵ),5ȣ,37.545303,127.10357 +1750,,1ȣ,37.416182,126.884466 +2750,Ÿ,7ȣ,37.479252,126.854876 +1019,,1ȣ,37.623632,127.061835 +2534,ȭ(ȭȸ),5ȣ,37.571525,126.97717 +2625,â(),6ȣ,37.547456,126.931993 +223,(.û),2ȣ,37.493961,127.014667 +330,(.û),3ȣ,37.493025,127.013786 +4921,,,37.645384,126.628633 +1701,,1ȣ,37.503039,126.881966 +232,εд,2ȣ,37.485266,126.901401 +1026,,д缱,37.486839,127.058856 +1205,,߾Ӽ,37.603392,127.143869 +4121,,9ȣ,37.501364,126.987332 +2616,,6ȣ,37.611377,126.91727 +1863,,д缱,37.298969,127.105664 +9009,, ö,37.29913,127.10389 +213,(û),2ȣ,37.537077,127.085916 +1813,,1ȣ,37.496756,126.870793 +310,Ĺ,3ȣ,37.636763,126.918821 +1214,,߾Ӽ,37.516169,127.399367 +3138,,õ1ȣ,37.399907,126.630347 +4114,ȸǻ,9ȣ,37.528105,126.917874 +2545,(ɵ),5ȣ,37.557088,127.079577 +2727,(ɵ),7ȣ,37.556897,127.079338 +1709,,1ȣ,37.35356,126.948462 +1324,,ἱ,37.832067,127.557695 +2760,õ,7ȣ,37.506997,126.73128 +2551,ٸ(ȸ),5ȣ,37.545477,127.142853 +3111,,õ1ȣ,37.566379,126.742654 +1316,ݰ,ἱ,37.637382,127.207853 +1279,ݸ,߾Ӽ,37.751322,126.765347 +1708,,1ȣ,37.372221,126.943429 +1458,,4ȣ,37.372209,126.943417 +1703,õû,1ȣ,37.455626,126.89398 +1280,,߾Ӽ,37.766217,126.774644 +324,ȣ,3ȣ,37.548034,127.015872 +1865,,д缱,37.275061,127.11591 +4501,,μ,37.275449,127.116665 +2550,浿,5ȣ,37.537801,127.140004 +417,,4ȣ,37.603407,127.025053 +4511,跮,μ,37.237247,127.198781 +1327,,ἱ,37.818466,127.71434 +2513,,5ȣ,37.562384,126.801292 +4102,,9ȣ,37.561916,126.802152 +4207,,ö1ȣ,37.561842,126.801904 +4929,,,37.56236,126.801868 +1980,,ؼ,37.5617,126.8041 +2519,ġ,5ȣ,37.531768,126.846683 +2753,ġ,7ȣ,37.506207,126.810939 +227,,2ȣ,37.47693,126.963693 +2747,,7ȣ,37.486056,126.887249 +3225,û,õ2ȣ,37.448161,126.736939 +1883,δũ,μ,37.407722,126.695216 +331,͹̳(),3ȣ,37.485013,127.016189 +2739,,7ȣ,37.484596,126.971251 +1002,,1ȣ,37.541021,126.9713 +2828,,8ȣ,37.4624,127.13977 +1328,õ,ἱ,37.864007,127.723792 +434,·,4ȣ,37.463873,126.989134 +2823,ѻ꼺Ա(.û),8ȣ,37.451535,127.159816 +2737,,7ȣ,37.487618,126.993513 +4118,,9ȣ,37.512887,126.953222 +4117,뷮,9ȣ,37.513534,126.941005 +1004,뷮,1ȣ,37.514149,126.94271 +411,,4ȣ,37.65627,127.063276 +2715,,7ȣ,37.654836,127.060462 +313,,3ȣ,37.600927,126.935756 +2630,(걸û),6ȣ,37.534675,126.986695 +1908,,1ȣ,37.75938,127.042292 +1021,õ,1ȣ,37.644799,127.051269 +2734,,7ȣ,37.511093,127.021415 +4305,,źд缱,37.511093,127.021415 +1271,ɰ,߾Ӽ,37.618808,126.820783 +2824,ܴŸ,8ȣ,37.44521,127.156866 +4811,޹,ؼ,37.348847,126.809409 +1878,޿,μ,37.379681,126.745177 +2543,ʸ,5ȣ,37.566747,127.052704 +409,,4ȣ,37.670272,127.079066 +4407,,Ÿ,37.4902998,126.9275133 +237,,2ȣ,37.534946,126.902767 +4113,,9ȣ,37.533406,126.902809 +1729,,1ȣ,37.344285,126.948345 +9002,, ö,37.63191,126.81113 +1953,,3ȣ,37.631626,126.811024 +1452,,4ȣ,37.435675,127.006523 +233,븲(αû),2ȣ,37.493243,126.894932 +2746,븲(αû),7ȣ,37.493013,126.897075 +1028,Ա,д缱,37.491373,127.07272 +1005,,1ȣ,37.513342,126.926382 +4402,,Ÿ,37.5133059,126.9257265 +1320,뼺,ἱ,37.684071,127.379319 +1752,߹,4ȣ,37.328467,126.917332 +337,û,3ȣ,37.493514,127.079532 +335,ġ,3ȣ,37.494612,127.063642 +1958,ȭ,3ȣ,37.676087,126.747569 +2626,(),6ȣ,37.547771,126.942069 +1910,,1ȣ,37.818486,127.056486 +1208,,߾Ӽ,37.586781,127.208832 +1911,,1ȣ,37.843188,127.061277 +334,,3ȣ,37.490922,127.055452 +1025,,д缱,37.491224,127.055186 +1206,,߾Ӽ,37.608806,127.161153 +247,õ,2ȣ,37.514287,126.882768 +1902,,1ȣ,37.679563,127.045595 +2712,,7ȣ,37.689241,127.046509 +1903,,1ȣ,37.689534,127.046049 +1209,,߾Ӽ,37.579622,127.222672 +1817,,1ȣ,37.468446,126.642706 +1823,ȭ,1ȣ,37.46607,126.668672 +316,,3ȣ,37.574571,126.957748 +2614,,6ȣ,37.618456,126.933031 +1714,,1ȣ,37.466613,126.889249 +3206,,õ2ȣ,37.585212,126.675844 +2644,,6ȣ,37.610537,127.056431 +155,빮,1ȣ,37.571687,127.01093 +421,빮,4ȣ,37.57093,127.009287 +205,빮繮ȭ,2ȣ,37.565613,127.009054 +422,빮繮ȭ,4ȣ,37.565133,127.007885 +2537,빮繮ȭ,5ȣ,37.564665,127.005353 +322,Ա,3ȣ,37.559052,127.005602 +1915,õ,1ȣ,37.927878,127.05479 +1913,õ߾,1ȣ,37.901885,127.056482 +3132,,õ1ȣ,37.397878,126.674005 +159,,1ȣ,37.573197,127.01648 +2637,,6ȣ,37.572279,127.015653 +4505,,μ,37.269043,127.152716 +3121,,õ1ȣ,37.485312,126.718247 +1808,,1ȣ,37.471408,126.702896 +4608,,μ,37.745271,127.056947 +1811,õ,1ȣ,37.475276,126.632802 +431,(),4ȣ,37.502852,126.980347 +4120,(),9ȣ,37.502878,126.978153 +4314,õ,źд缱,37.337928,127.102976 +3131,,õ1ȣ,37.404737,126.681015 +9010,ź, ö,37.20034,127.09569 +1727,,1ȣ,36.833705,127.14896 +4515,,μ,37.267051,127.21364 +2555,̵,5ȣ,37.527788,127.136248 +4137,̿,9ȣ,37.519683,127.137989 +4109,,9ȣ,37.550632,126.865689 +2619,й̵Ƽ,6ȣ,37.576108,126.901391 +1294,й̵Ƽ,߾Ӽ,37.577475,126.900453 +4204,й̵Ƽ,ö1ȣ,37.576958,126.898609 +210,Ҽ,2ȣ,37.547184,127.047367 +2730,Ҽ,7ȣ,37.53154,127.066704 +2515,,5ȣ,37.560183,126.825448 +4105,,9ȣ,37.566778,126.82731 +4206,,ö1ȣ,37.565543,126.827378 +1955,,3ȣ,37.652206,126.77762 +2714,,7ȣ,37.66494,127.057675 +4922,,,37.640732,126.644344 +1319,,ἱ,37.652782,127.311767 +2542,,5ȣ,37.5661,127.042973 +3204,,õ2ȣ,37.597566,126.666998 +2561,õ,5ȣ,37.49499,127.152781 +2529,,5ȣ,37.539574,126.945932 +2621,û,6ȣ,37.563515,126.903343 +3224,,õ2ȣ,37.454911,126.732094 +1203,,߾Ӽ,37.59955,127.091909 +2622,,6ȣ,37.556094,126.910052 +1904,,1ȣ,37.709914,127.047455 +1869,,д缱,37.245795,127.057353 +1872,ű,д缱,37.265481,127.015678 +333,ź,3ȣ,37.486947,127.046769 +1870,źǼ,д缱,37.252759,127.040566 +2720,԰,7ȣ,37.610637,127.077725 +2723,,7ȣ,37.588579,127.087503 +424,,4ȣ,37.560989,126.986325 +2552,,5ȣ,37.55137,127.143999 +4510,,μ,37.237964,127.190294 +1707,,1ȣ,37.384653,126.935433 +2827,,8ȣ,37.433824,127.129837 +1853,,д缱,37.432052,127.129104 +3223,𷡳,õ2ȣ,37.45583,126.719298 +2521,,5ȣ,37.526065,126.864931 +2814,伺(ȭǹ),8ȣ,37.517409,127.112359 +315,,3ȣ,37.582299,126.950291 +235,,2ȣ,37.517933,126.89476 +1284,,߾Ӽ,37.854619,126.788047 +2819,,8ȣ,37.485855,127.1225 +3127,а,õ1ȣ,37.434935,126.698579 +1858,̱,д缱,37.350077,127.10891 +4313,̱,źд缱,37.349982,127.108918 +9996,̻,5ȣ,37.560927,127.193877 +415,̾(̹),4ȣ,37.62667,127.025983 +416,̾ƻŸ,4ȣ,37.613292,127.030053 +3112,,õ1ȣ,37.553703,126.745077 +1753,ݿ,4ȣ,37.312212,126.903524 +2735,,7ȣ,37.508178,127.011727 +4601,߰,μ,37.727048,127.052803 +2516,߻,5ȣ,37.558598,126.837668 +225,,2ȣ,37.481426,126.997596 +2557,,5ȣ,37.508857,127.126133 +1901,,1ȣ,37.667503,127.044273 +2511,ȭ,5ȣ,37.577446,126.812741 +1405,,1ȣ,36.777629,127.052991 +1273,鸶,߾Ӽ,37.658239,126.794461 +1954,鼮,3ȣ,37.643114,126.78787 +1325,縮,ἱ,37.830779,127.58933 +1807,,1ȣ,37.483664,126.707704 +2633,Ƽ,6ȣ,37.548013,127.007055 +1457,,4ȣ,37.389793,126.950806 +4603,,μ,37.728755,127.04353 +1313,,ἱ,37.64202,127.12684 +408,,4ȣ,37.66778,127.11581 +1716,,1ȣ,37.207503,127.032731 +2744,,7ȣ,37.499872,126.920428 +4404,,Ÿ,37.5002739,126.9204355 +4405,Ű,Ÿ,37.4955691,126.9180827 +4406,ź,Ÿ,37.4929598,126.9234964 +2639,,6ȣ,37.585274,127.019351 +4712,,̽ż,37.585286,127.019381 +1914,,1ȣ,37.913702,127.057277 +1861,,д缱,37.312752,127.108196 +4514,,μ,37.258965,127.218457 +2821,,8ȣ,37.471052,127.126732 +1031,,д缱,37.470345,127.126658 +1401,,1ȣ,36.801215,127.135763 +4129,,9ȣ,37.514219,127.060245 +229,õ,2ȣ,37.482362,126.941892 +2648,ȭ(Ƿ),6ȣ,37.617283,127.091401 +1815,ΰ,1ȣ,37.488418,126.74109 +1509,ι,氭,37.260192,127.490277 +1804,õ,1ȣ,37.48405,126.782686 +2757,õû,7ȣ,37.504631,126.763538 +2754,õտ,7ȣ,37.50538,126.797337 +1982,õտ,ؼ,37.505457,126.797289 +1806,,1ȣ,37.489445,126.724506 +3120,,õ1ȣ,37.490535,126.723453 +2761,û,7ȣ,37.507394,126.721599 +3118,û,õ1ȣ,37.508407,126.720555 +3122,Ÿ,õ1ȣ,37.477679,126.710208 +3119,,õ1ȣ,37.498383,126.722244 +4709,ѻ꺸,̽ż,37.612072,127.008251 +4701,ѻ,̽ż,37.662909,127.012706 +312,ұ,3ȣ,37.610553,126.92982 +2613,ұ,6ȣ,37.610873,126.92939 +2724,簡,7ȣ,37.580894,127.088478 +226,,2ȣ,37.476538,126.981544 +433,,4ȣ,37.476955,126.981651 +1315,縪,ἱ,37.65108,127.176933 +1877,縮,μ,37.28998,126.85685 +4926,(û),,37.620249,126.719731 +4124,,9ȣ,37.504206,127.015259 +3762,,7ȣ,37.5086,126.7035277 +1751,꺻,4ȣ,37.358101,126.933274 +2822,꼺,8ȣ,37.457122,127.149908 +4508,ﰡ,μ,37.242115,127.168075 +428,ﰢ,4ȣ,37.534075,126.9726 +2629,ﰢ,6ȣ,37.535534,126.974032 +1503,ﵿ,氭,37.409522,127.20336 +2759,ü,7ȣ,37.506411,126.742153 +3759,ü,7ȣ,37.50724,126.74179 +9006,Z, ö,37.50887,127.06324 +219,Z(),2ȣ,37.508844,127.06316 +4128,Z߾,9ȣ,37.513011,127.053282 +1950,,3ȣ,37.653083,126.895558 +4706,,̽ż,37.626914,127.018106 +4707,Ÿ,̽ż,37.621337,127.020473 +4131,,9ȣ,37.504738,127.088025 +1866,,д缱,37.26181,127.108847 +410,,4ȣ,37.660878,127.073572 +2741,,7ȣ,37.502834,126.94791 +3758,,7ȣ,37.505814,126.753163 +1754,ϼ,4ȣ,37.302795,126.866489 +2722,(ÿܹ͹̳),7ȣ,37.595577,127.085716 +1202,(ÿܹ͹̳),߾Ӽ,37.596678,127.08504 +2624,,6ȣ,37.547716,126.922852 +207,սʸ,2ȣ,37.564354,127.029354 +2643,(ѱб),6ȣ,37.606377,127.048491 +2554,ϵ,5ȣ,37.556712,127.166417 +1322,õ,ἱ,37.770246,127.454821 +4317,,źд缱,37.297664,127.069342 +4609,,μ,37.748885,127.06362 +2617,(Ż),6ȣ,37.591148,126.913629 +4116,,9ȣ,37.517274,126.928422 +4401,,Ÿ,37.5170969,126.929399 +1263,,߾Ӽ,37.551881,126.935711 +3210,û,õ2ȣ,37.543742,126.676787 +2533,빮,5ȣ,37.565773,126.966641 +1749,ź,1ȣ,37.195504,127.051672 +3214,οȸ,õ2ȣ,37.500168,126.675795 +1009,,߾Ӽ,37.519594,126.988537 +9005,, ö,37.55569,126.97296 +4410,뺥óŸ,Ÿ,37.4720019,126.9339351 +228,Ա(DZû),2ȣ,37.481247,126.952739 +1847,」,д缱,37.543617,127.044707 +426,↑,4ȣ,37.55281,126.972556 +1001,↑,1ȣ,37.554337,126.971134 +1291,↑,߾Ӽ,37.557231,126.97103 +4201,↑,ö1ȣ,37.553247,126.969769 +4403,溴û,Ÿ,37.5060464,126.9227083 +4409,,Ÿ,37.4782341,126.9330365 +1722,,1ȣ,37.056496,127.052819 +224,,2ȣ,37.491897,127.007917 +1855,,д缱,37.385126,127.123592 +2645,,6ȣ,37.614872,127.065595 +1018,,1ȣ,37.614532,127.065934 +3763,(źϽ),7ȣ,37.5062285,126.6762813 +3213,(źϽ),õ2ȣ,37.506193,126.676203 +3220,,õ2ȣ,37.457611,126.692575 +1704,,1ȣ,37.435047,126.902295 +3222,õŸ,õ2ȣ,37.456805,126.709986 +2816,,8ȣ,37.505557,127.106832 +4133,,9ȣ,37.505208,127.10704 +4132,̰,9ȣ,37.502558,127.097033 +220,,2ȣ,37.504286,127.048203 +1023,,д缱,37.504856,127.048807 +1450,,4ȣ,37.451673,127.002303 +4812,,ؼ,37.334353,126.809904 +4112,,9ȣ,37.53802,126.893525 +4127,,9ȣ,37.510297,127.043999 +1850,,д缱,37.510735,127.043677 +3128,,õ1ȣ,37.426684,126.698863 +1711,հ,1ȣ,37.300349,126.97075 +1512,,氭,37.39468,127.11945 +9008,, ö,37.39467,127.12058 +4316,,źд缱,37.313335,127.0801 +211,,2ȣ,37.544581,127.055961 +418,ſԱ(),4ȣ,37.592612,127.016441 +4711,ſԱ(),̽ż,37.592467,127.016516 +1725,ȯ,1ȣ,36.916076,127.126964 +1715,,1ȣ,37.245025,127.013222 +1717,,1ȣ,37.187533,127.04318 +1510,ո,氭,37.295309,127.570938 +3137,Ʈũ,õ1ȣ,37.393054,126.634729 +1880,ҷ,μ,37.40095,126.733522 +1814,һ,1ȣ,37.482753,126.79544 +4804,һ,ؼ,37.483279,126.795023 +4805,һ,ؼ,37.468467,126.797252 +1916,ҿ,1ȣ,37.9481,127.061034 +4702,ֹ,̽ż,37.65603,127.013273 +4708,ֻ,̽ż,37.620238,127.013626 +1805,۳,1ȣ,37.4876,126.753664 +1886,۵,μ,37.428514,126.657772 +3139,۵޺,õ1ȣ,37.407143,126.62597 +4614,ۻ,μ,37.737279,127.087159 +2514,,5ȣ,37.561184,126.811973 +1721,ź,1ȣ,37.075696,127.054301 +2817,,8ȣ,37.499703,127.112183 +4134,ij,9ȣ,37.510372,127.112216 +1856,,д缱,37.378455,127.114322 +2713,,7ȣ,37.67785,127.055315 +1763,,4ȣ,37.349801,126.925365 +1267,,߾Ӽ,37.580842,126.895611 +339,,3ȣ,37.487378,127.101907 +1030,,д缱,37.487472,127.101422 +9007,, ö,37.48637,127.10161 +1713,,1ȣ,37.266348,126.999561 +1846,,д缱,37.265917,126.999422 +1871,û,д缱,37.261911,127.030736 +414,(ϱû),4ȣ,37.638052,127.025732 +4315,û,źд缱,37.322702,127.095026 +2826,,8ȣ,37.437428,127.140722 +427,Ա(),4ȣ,37.54456,126.972106 +2740,ǴԱ(),7ȣ,37.496029,126.953822 +1889,,μ,37.460789,126.638297 +3219,ùΰ(ȭâ),õ2ȣ,37.458335,126.681192 +151,û,1ȣ,37.565715,126.977088 +201,û,2ȣ,37.563588,126.975411 +4509,û.δ,μ,37.239151,127.178406 +4810,ɰ,ؼ,37.369864,126.808573 +4806,,ؼ,37.450145,126.793041 +4809,û,ؼ,37.382223,126.805625 +1864,Ű,д缱,37.286102,127.111313 +2539,űȣ,5ȣ,37.554548,127.020331 +2526,ű,5ȣ,37.517623,126.914839 +1032,ű,1ȣ,37.516862,126.917865 +1760,űõ,4ȣ,37.338212,126.765844 +2649,ų,6ȣ,37.613174,127.102231 +1311,ų,ἱ,37.612887,127.103218 +4125,ų,9ȣ,37.504598,127.02506 +4306,ų,źд缱,37.504598,127.02506 +245,Ŵ,2ȣ,37.57004,127.046481 +206,Ŵ,2ȣ,37.56564,127.019614 +2636,Ŵ,6ȣ,37.566154,127.016146 +231,Ŵ,2ȣ,37.487462,126.913149 +2743,ŴŸ,7ȣ,37.499701,126.928276 +234,ŵ,2ȣ,37.508961,126.891084 +1007,ŵ,1ȣ,37.508787,126.891144 +1507,ŵе,氭,37.317185,127.40476 +230,Ÿ,2ȣ,37.484201,126.929715 +4408,Ÿ,Ÿ,37.4849266,126.9296159 +4111,Ÿ,9ȣ,37.544277,126.88308 +4122,Ź,9ȣ,37.503415,126.995925 +4104,Źȭ,9ȣ,37.567532,126.816601 +327,Ż,3ȣ,37.516334,127.020114 +4304,Ż,źд缱,37.516334,127.020114 +156,ż,1ȣ,37.576048,127.024634 +246,ż,2ȣ,37.574747,127.024932 +4713,ż,̽ż,37.576095,127.023242 +3129,ſ,õ1ȣ,37.41804,126.693863 +429,ſ,4ȣ,37.52917,126.967894 +1213,ſ,߾Ӽ,37.525545,127.372921 +1017,̹,1ȣ,37.601854,127.067325 +2520,(),5ȣ,37.524997,126.856191 +249,װŸ,2ȣ,37.520074,126.852912 +3756,ߵ,7ȣ,37.50282,126.77566 +1408,â(õ),1ȣ,36.769502,126.951108 +4807,õ,ؼ,37.439066,126.786788 +240,,2ȣ,37.555131,126.936926 +1252,,߾Ӽ,37.559733,126.942597 +1890,,μ,37.46874,126.623853 +2745,dz,7ȣ,37.50008,126.90993 +4808,,ؼ,37.409008,126.788017 +2825,,8ȣ,37.440918,127.147564 +413,ֹ,4ȣ,37.648627,127.034709 +1402,ֿ(緿),1ȣ,36.793759,127.1214 +1403,ƻ,1ȣ,36.792053,127.104361 +3209,ƽþƵ(̻Ÿ),õ2ȣ,37.5517,126.677122 +1215,ƽ,߾Ӽ,37.51382,127.443173 +2546,(̴Ĺ),5ȣ,37.551691,127.089761 +242,,2ȣ,37.557345,126.956141 +318,ȱ,3ȣ,37.576477,126.985443 +1759,Ȼ,4ȣ,37.327082,126.788532 +2640,Ⱦ(뺴),6ȣ,37.586272,127.029005 +1706,Ⱦ,1ȣ,37.401592,126.922874 +2811,ϻ,8ȣ,37.55021,127.127562 +326,б,3ȣ,37.527072,127.028461 +1848,бε,д缱,37.527381,127.040534 +2531,ֿ,5ȣ,37.553736,126.95682 +1277,ߴ,߾Ӽ,37.712327,126.761356 +1876,߸,μ,37.264179,126.879483 +1854,ž,д缱,37.411185,127.128715 +323,,3ȣ,37.554867,127.010541 +2634,,6ȣ,37.554263,127.010358 +1212,,߾Ӽ,37.545981,127.329098 +1204,,߾Ӽ,37.606596,127.107906 +332,(ʱû),3ȣ,37.484477,127.033902 +4308,(ʱû),źд缱,37.483809,127.034653 +4309,ùǽ(),źд缱,37.470023,127.03842 +1207,,߾Ӽ,37.60533,127.19364 +1909,,1ȣ,37.774381,127.044708 +248,õû,2ȣ,37.512398,126.865819 +4106,õⱳ,9ȣ,37.568381,126.841333 +4920,,,37.642379,126.614309 +2523,,5ȣ,37.525569,126.886129 +1217,,߾Ӽ,37.492773,127.491837 +4613,,μ,37.742802,127.085035 +2728,̴(),7ȣ,37.548014,127.074658 +4504,,μ,37.274917,127.143714 +1875,õ,μ,37.250102,126.90879 +4126,,9ȣ,37.507287,127.033868 +2528,dz,5ȣ,37.527098,126.932901 +2527,ǵ,5ȣ,37.521747,126.924357 +4115,ǵ,9ȣ,37.52176,126.92403 +1511,,氭,37.282308,127.628816 +1803,,1ȣ,37.485178,126.811502 +221,,2ȣ,37.500622,127.036456 +2612,,6ȣ,37.606021,126.922744 +1885,,μ,37.417804,126.67894 +311,ų,3ȣ,37.619229,126.921038 +2615,ų,6ȣ,37.618636,126.920625 +9004,ų, ö,37.61878,126.9213 +1919,õ,1ȣ,38.10073,127.07372 +4110,â,9ȣ,37.546936,126.874916 +1006,,1ȣ,37.515504,126.907628 +236,û,2ȣ,37.525706,126.89661 +2524,û,5ȣ,37.5242,126.89503 +2525,,5ȣ,37.522669,126.905139 +4217,,ö1ȣ,37.51202,126.524254 +1868,,д缱,37.251568,127.071394 +3125,ȸ,õ1ȣ,37.449396,126.701012 +342,,3ȣ,37.502129,127.128319 +2558,,5ȣ,37.502057,127.127938 +406,,4ȣ,37.705,127.19281 +1802,,1ȣ,37.494526,126.845365 +1859,,д缱,37.339824,127.108942 +2522,(񵿿),5ȣ,37.524496,126.875181 +1874,õ,μ,37.24304,126.963676 +1216,,߾Ӽ,37.506062,127.473868 +1719,,1ȣ,37.145885,127.06672 +1718,,1ȣ,37.168953,127.063197 +1762,̵,4ȣ,37.362357,126.738714 +325,,3ȣ,37.541684,127.017269 +1011,,߾Ӽ,37.540446,127.018672 +2752,¼(ȸԱ),7ȣ,37.492092,126.823023 +1821,¼(ȸԱ),1ȣ,37.492433,126.824086 +1407,¾õ,1ȣ,36.780483,127.003249 +2556,øȰ(ѱü),5ȣ,37.516201,127.130923 +4136,øȰ(ѱü),9ȣ,37.516269,127.130288 +3205,,õ2ȣ,37.592928,126.673203 +3202,ձ,õ2ȣ,37.59518,126.642696 +208,սʸ(û),2ȣ,37.561238,127.036954 +2541,սʸ(û),5ȣ,37.56184,127.037059 +1013,սʸ(û),߾Ӽ,37.561827,127.038352 +1016,ܴ,1ȣ,37.596073,127.063549 +244,,2ȣ,37.561904,127.050899 +250,(빮û),2ȣ,37.574028,127.038091 +2725,븶,7ȣ,37.573647,127.086727 +1219,빮,߾Ӽ,37.48223,127.594647 +1003,,1ȣ,37.529849,126.964561 +2517,,5ȣ,37.548768,126.836318 +1211,,߾Ӽ,37.554669,127.310115 +4512,.۴,μ,37.237845,127.209198 +4211,,ö1ȣ,37.492904,126.49379 +4924,,,37.653867,126.68393 +3227,(â),õ2ȣ,37.440127,126.75997 +1278,,߾Ӽ,37.725826,126.767257 +9000,, ö,37.71614,126.72841 +1286,õ,߾Ӽ,37.879942,126.769999 +4814,,ؼ,37.31321,126.796261 +1951,,3ȣ,37.653324,126.843041 +1218,,߾Ӽ,37.468672,127.547076 +4815,,ؼ,37.302371,126.786691 +1884,,μ,37.413049,126.686648 +3130,,õ1ȣ,37.412333,126.687869 +1981,,ؼ,37.5239,126.8049 +1948,,3ȣ,37.650658,126.872642 +1020,,1ȣ,37.633212,127.058831 +2642,(),6ȣ,37.601948,127.041518 +1879,,μ,37.391769,126.742699 +2620,Ű(),6ȣ,37.569532,126.899298 +1282,,߾Ӽ,37.796188,126.792587 +203,3,2ȣ,37.566306,126.991696 +320,3,3ȣ,37.566672,126.992548 +204,4,2ȣ,37.566595,126.997817 +2536,4,5ȣ,37.567352,126.998032 +202,Ա,2ȣ,37.566014,126.982618 +1012,,߾Ӽ,37.549946,127.034538 +2611,,6ȣ,37.598605,126.915577 +1710,ǿ,1ȣ,37.320852,126.948217 +1906,,1ȣ,37.738415,127.045958 +4605,νû,μ,37.739256,127.034781 +4607,߾,μ,37.743676,127.049565 +241,̴,2ȣ,37.556733,126.946013 +1502,̸,氭,37.394655,127.127819 +1860,̸,д缱,37.395371,127.128248 +2738,̼,7ȣ,37.485196,126.981605 +1508,õ,氭,37.265579,127.44226 +430,(߾ӹڹ),4ȣ,37.522295,126.974733 +1008,(߾ӹڹ),߾Ӽ,37.522427,126.973406 +2631,¿,6ȣ,37.534488,126.994302 +1455,δ,4ȣ,37.401553,126.976715 +1812,õ,1ȣ,37.476079,126.616801 +1891,õ,μ,37.476403,126.617326 +3215,õ,õ2ȣ,37.4897,126.675208 +4213,õ1͹̳,ö1ȣ,37.447464,126.452508 +4215,õ2͹̳,ö1ȣ,37.460699,126.441442 +1881,õ,μ,37.400614,126.722478 +3226,õ,õ2ȣ,37.448769,126.752618 +3136,õԱ,õ1ȣ,37.386007,126.639484 +3124,õû,õ1ȣ,37.457263,126.702143 +3221,õû,õ2ȣ,37.456833,126.701306 +3126,õ͹̳,õ1ȣ,37.442383,126.699706 +1888,ϴ,μ,37.448493,126.649619 +1275,ϻ,߾Ӽ,37.682077,126.769846 +338,Ͽ,3ȣ,37.483681,127.08439 +1285,,߾Ӽ,37.888421,126.746765 +3113,,õ1ȣ,37.545059,126.738665 +3116,,õ1ȣ,37.530415,126.722527 +216,(ıû),2ȣ,37.513262,127.100159 +2815,(ıû),8ȣ,37.514692,127.104338 +215,dz,2ȣ,37.520733,127.10379 +217,ǻ,2ȣ,37.511687,127.086162 +328,,3ȣ,37.512759,127.01122 +4923,,,37.643986,126.669017 +2742,¹,7ȣ,37.504898,126.93915 +2711,,7ȣ,37.700109,127.053196 +2820,,8ȣ,37.478703,127.126191 +2544,,5ȣ,37.56144,127.064623 +1918,,1ȣ,38.02458,127.0718 +4517,.,μ,37.285342,127.219561 +4710,,̽ż,37.603133,127.013396 +1956,߻,3ȣ,37.659477,126.773359 +1454,ΰõû,4ȣ,37.426513,126.98978 +1761,,4ȣ,37.351735,126.742989 +1857,,д缱,37.365994,127.10807 +4312,,źд缱,37.367098,127.108403 +157,⵿,1ȣ,37.578103,127.034893 +1810,,1ȣ,37.466769,126.656666 +152,,1ȣ,37.570161,126.982923 +153,3,1ȣ,37.570406,126.991847 +319,3,3ȣ,37.571605,126.991791 +2535,3,5ȣ,37.57254,126.990305 +154,5,1ȣ,37.570926,127.001849 +218,տ,2ȣ,37.511022,127.073704 +4130,տ,9ȣ,37.511426,127.076275 +1809,־,1ȣ,37.465047,126.679742 +3218,־,õ2ȣ,37.464992,126.679098 +3217,־ȱ,õ2ȣ,37.473703,126.68113 +1957,ֿ,3ȣ,37.670072,126.761334 +1862,,д缱,37.324753,127.107395 +2716,߰,7ȣ,37.644583,127.064303 +2726,߰,7ȣ,37.565923,127.08432 +1822,ߵ,1ȣ,37.486562,126.764843 +1201,߶,߾Ӽ,37.594917,127.076116 +1756,߾,4ȣ,37.315941,126.838573 +4138,߾Ӻƺ,9ȣ,37.529191,127.148739 +2721,ȭ,7ȣ,37.602545,127.079264 +4108,,9ȣ,37.557402,126.861939 +2618,(),6ȣ,37.583876,126.909645 +4503,,μ,37.269606,127.136515 +3135,,õ1ȣ,37.378384,126.645168 +1723,,1ȣ,37.0188,127.070444 +309,,3ȣ,37.648033,126.913917 +1220,,߾Ӽ,37.476393,127.629874 +1912,,1ȣ,37.892334,127.055716 +1726,,1ȣ,36.870593,127.143904 +1720,,1ȣ,37.109447,127.062278 +405,,4ȣ,37.7205,127.2034 +412,â,4ȣ,37.653088,127.047274 +1022,â,߾Ӽ,37.653007,127.047806 +2638,â,6ȣ,37.579661,127.015241 +1318,õ,ἱ,37.658978,127.285379 +1728,õ,1ȣ,36.810005,127.146826 +2751,õ,7ȣ,37.486637,126.838713 +2548,õȣ(dz伺),5ȣ,37.53864,127.123308 +2812,õȣ(dz伺),8ȣ,37.538113,127.123254 +2749,ö,7ȣ,37.47605,126.867911 +4310,ûԱ,źд缱,37.447211,127.055664 +2538,û,5ȣ,37.560276,127.013639 +2635,û,6ȣ,37.560608,127.013986 +2731,û,7ȣ,37.519365,127.05335 +4210,û,ö1ȣ,37.556409,126.624648 +158,û(øԱ),1ȣ,37.579956,127.044585 +1014,û(øԱ),߾Ӽ,37.580759,127.0483 +1867,û,д缱,37.259489,127.078934 +1321,û,ἱ,37.735488,127.42661 +4506,ʴ,μ,37.260752,127.159443 +1917,ʼ,1ȣ,37.98172,127.06912 +1505,ʿ,氭,37.374419,127.299 +4813,,ؼ,37.319619,126.808147 +1758,,4ȣ,37.320646,126.805913 +432,ѽŴԱ(̼),4ȣ,37.486263,126.981989 +3755,,7ȣ,37.50365,126.78828 +1329,õ,ἱ,37.885054,127.717023 +321,湫,3ȣ,37.56143,126.994072 +423,湫,4ȣ,37.561207,126.99408 +243,(Ա),2ȣ,37.559704,126.964378 +2532,(Ա),5ȣ,37.560236,126.9629 +3133,ķ۽Ÿ,õ1ȣ,37.387855,126.661673 +9001,Ųؽ, ö,37.66532,126.74843 +1276,ź,߾Ӽ,37.694023,126.761086 +4615,ž,μ,37.733579,127.088704 +1404,,1ȣ,36.78866,127.08485 +2646,¸Ա,6ȣ,37.617338,127.074735 +2719,¸Ա,7ȣ,37.618294,127.075397 +1852,,д缱,37.440019,127.127709 +3134,ũũ,õ1ȣ,37.382268,126.656365 +1314,,ἱ,37.648311,127.143952 +1283,,߾Ӽ,37.815298,126.792783 +1501,DZ,氭,37.394761,127.111217 +4311,DZ,źд缱,37.394761,127.112217 +1210,ȴ,߾Ӽ,37.547371,127.243939 +1317,ȣ,ἱ,37.653225,127.244493 +1456,,4ȣ,37.394287,126.963883 +1724,,1ȣ,36.990726,127.085159 +4927,dz,,37.612488,126.732387 +1274,dz,߾Ӽ,37.672346,126.786243 +2717,ϰ,7ȣ,37.636352,127.06799 +2565,ϳ˴ܻ,5ȣ,37.53972,127.22345 +2566,ϳû(dz-),5ȣ,37.54205,127.20612 +2564,ϳdz,5ȣ,37.552034,127.203864 +2733,е,7ȣ,37.514229,127.031656 +336,п,3ȣ,37.496663,127.070594 +2632,Ѱ,6ȣ,37.539631,127.001725 +1010,ѳ,߾Ӽ,37.52943,127.009169 +1755,Ѵ,4ȣ,37.309689,126.85344 +419,ѼԱ(Q),4ȣ,37.588458,127.006221 +4135,Ѽ,9ȣ,37.516404,127.116503 +209,Ѿ,2ȣ,37.555273,127.043655 +1024,Ƽ,д缱,37.496237,127.052873 +238,,2ȣ,37.549457,126.913808 +2623,,6ȣ,37.549209,126.913366 +2540,,5ȣ,37.557322,127.029476 +1270,,߾Ӽ,37.612102,126.834146 +420,ȭ,4ȣ,37.582336,127.001844 +1882,ȣ,μ,37.401637,126.708627 +239,ȫԱ,2ȣ,37.55679,126.923708 +1293,ȫԱ,߾Ӽ,37.557641,126.926683 +1264,ȫԱ,߾Ӽ,37.557641,126.926683 +4203,ȫԱ,ö1ȣ,37.557438,126.926715 +314,ȫ,3ȣ,37.589066,126.943736 +4705,ȭ,̽ż,37.634133,127.017511 +2518,ȭ,5ȣ,37.541513,126.840461 +2647,ȭ(←Ա),6ȣ,37.620064,127.084689 +1712,ȭ,1ȣ,37.283862,126.989627 +1268,ȭ,߾Ӽ,37.602888,126.868387 +1952,ȭ,3ȣ,37.634592,126.83265 +1015,ȸ,߾Ӽ,37.58946,127.057583 +1905,ȸ,1ȣ,37.724416,127.04736 +4602,ȸ,μ,37.725006,127.047073 +425,ȸ(빮),4ȣ,37.558514,126.978246 +4611,ȿ,μ,37.754025,127.076902 +2628,ȿâ,6ȣ,37.539233,126.961384 +1261,ȿâ,߾Ӽ,37.538579,126.96221 +4119,漮(߾ӴԱ),9ȣ,37.50877,126.963708 +4606,Q,μ,37.743302,127.037023 \ No newline at end of file diff --git a/backend/bang-ggood/src/test/resources/seoul_stations_240925.csv b/backend/bang-ggood/src/test/resources/seoul_stations_240925.csv new file mode 100644 index 000000000..da20d4abc --- /dev/null +++ b/backend/bang-ggood/src/test/resources/seoul_stations_240925.csv @@ -0,0 +1,768 @@ +_ID,,ȣ,,浵 +4703,4.19ֹ,̽ż,37.649502,127.013684 +1907,,1ȣ,37.748577,127.044213 +340,,3ȣ,37.492245,127.117757 +2818,,8ȣ,37.492888,127.118398 +2748,д,7ȣ,37.480338,126.882656 +1702,д,1ȣ,37.481581,126.882581 +4107,,9ȣ,37.561391,126.854456 +4704,,̽ż,37.641537,127.016789 +3216,,õ2ȣ,37.484192,126.683673 +3211,(Ƽ),õ2ȣ,37.524649,126.675539 +3212,߾ӽ,õ2ȣ,37.517054,126.676672 +1265,,߾Ӽ,37.568491,126.915487 +1851,õ,κд缱,37.448605,127.126697 +1323,,ἱ,37.814536,127.510739 +1816,,1ȣ,37.464737,126.694181 +3123,Ÿ,õ1ȣ,37.467048,126.707938 +1312,,ἱ,37.634118,127.114757 +3117,,õ1ȣ,37.517268,126.721514 +222,,2ȣ,37.49799,127.027912 +4307,,źд缱,37.496837,127.028104 +2732,û,7ȣ,37.517179,127.041255 +1849,û,κд缱,37.517469,127.041151 +4502,,ο,37.270161,127.126033 +2549,,5ȣ,37.535804,127.132481 +2813,û,8ȣ,37.530341,127.120508 +1269,,߾Ӽ,37.612314,126.843223 +214,(͹̳),2ȣ,37.535095,127.094681 +9995,,5ȣ,37.55749,127.17593 +1326,,ἱ,37.805723,127.634146 +2559,,5ȣ,37.498079,127.13482 +1801,,1ȣ,37.494594,126.85968 +1027,,κд缱,37.489116,127.06614 +4101,ȭ,9ȣ,37.578608,126.798153 +2512,ȭ,5ȣ,37.572399,126.806171 +2560,ſ,5ȣ,37.493105,127.14415 +212,ǴԱ,2ȣ,37.540373,127.069191 +2729,ǴԱ,7ȣ,37.540786,127.071011 +4925,Ϻ,,37.63165,126.705975 +3203,˴ܻŸ,õ2ȣ,37.60185,126.657108 +3201,˴ܿ(˴ܻ),õ2ȣ,37.594877,126.627178 +3208,˹,õ2ȣ,37.561405,126.677566 +4209,˾,ö,37.569098,126.674007 +3207,˾,õ2ȣ,37.56866,126.675687 +1504,Ɽ,氭,37.399907,127.25263 +4610,⵵ûϺû,ΰö,37.75059,127.071495 +1451,渶,4ȣ,37.443885,127.007888 +317,溹(μû),3ȣ,37.575762,126.97353 +3115,αԱ,õ1ȣ,37.538157,126.722597 +4604,ö,ΰö,37.737202,127.043257 +341,,3ȣ,37.495918,127.12454 +3114,,õ1ȣ,37.543238,126.728128 +4208,,ö,37.571662,126.7363 +3110,,õ1ȣ,37.571449,126.73578 +2553,,5ȣ,37.555004,127.154151 +2641,(),6ȣ,37.590508,127.036296 +1873,,κд缱,37.24963,126.980248 +329,͹̳,3ȣ,37.504891,127.004916 +2736,͹̳,7ȣ,37.503367,127.005068 +4123,͹̳,9ȣ,37.50598,127.004403 +1757,,4ȣ,37.316784,126.823144 +4513,,ο,37.24484,127.214251 +4928,,,37.601243,126.770345 +1272,,߾Ӽ,37.645676,126.801762 +4612,,ΰö,37.750471,127.083715 +1506,,氭,37.351315,127.34674 +2530,,5ȣ,37.544431,126.951372 +2627,,6ȣ,37.543555,126.951678 +1292,,߾Ӽ,37.542596,126.952099 +4202,,ö,37.54253,126.952024 +2718,(б),7ȣ,37.625742,127.072896 +4103,׽,9ȣ,37.563726,126.810678 +4212,ȭû,ö,37.459041,126.477516 +1453,õ,4ȣ,37.433021,126.996568 +1705,,1ȣ,37.419232,126.908706 +4411,ǻ(),Ÿ,37.4691018,126.9450639 +4319,(),źд缱,37.30211,127.044483 +4318,߾(ִ),źд缱,37.288617,127.051478 +2547,(Ŵ),5ȣ,37.545303,127.10357 +1750,,1ȣ,37.416182,126.884466 +2750,Ÿ,7ȣ,37.479252,126.854876 +1019,,1ȣ,37.623632,127.061835 +2534,ȭ(ȭȸ),5ȣ,37.571525,126.97717 +2625,â(),6ȣ,37.547456,126.931993 +223,(.û),2ȣ,37.493961,127.014667 +330,(.û),3ȣ,37.493025,127.013786 +4921,,,37.645384,126.628633 +1701,,1ȣ,37.503039,126.881966 +232,εд,2ȣ,37.485266,126.901401 +1026,,κд缱,37.486839,127.058856 +1205,,߾Ӽ,37.603392,127.143869 +4121,,9ȣ,37.501364,126.987332 +2616,,6ȣ,37.611377,126.91727 +1863,,κд缱,37.298969,127.105664 +9009,,GTX-A,37.29913,127.10389 +213,(û),2ȣ,37.537077,127.085916 +1813,,1ȣ,37.496756,126.870793 +310,Ĺ,3ȣ,37.636763,126.918821 +1214,,߾Ӽ,37.516169,127.399367 +3138,,õ1ȣ,37.399907,126.630347 +4114,ȸǻ,9ȣ,37.528105,126.917874 +2545,(ɵ),5ȣ,37.557088,127.079577 +2727,(ɵ),7ȣ,37.556897,127.079338 +1709,,1ȣ,37.35356,126.948462 +1324,,ἱ,37.832067,127.557695 +2760,õ,7ȣ,37.506997,126.73128 +2551,ٸ(ȸ),5ȣ,37.545477,127.142853 +3111,,õ1ȣ,37.566379,126.742654 +1316,ݰ,ἱ,37.637382,127.207853 +1279,ݸ,߾Ӽ,37.751322,126.765347 +1708,,1ȣ,37.372221,126.943429 +1458,,4ȣ,37.372209,126.943417 +1703,õû,1ȣ,37.455626,126.89398 +1280,,߾Ӽ,37.766217,126.774644 +324,ȣ,3ȣ,37.548034,127.015872 +1865,,κд缱,37.275061,127.11591 +4501,,ο,37.275449,127.116665 +2550,浿,5ȣ,37.537801,127.140004 +417,,4ȣ,37.603407,127.025053 +4511,跮,ο,37.237247,127.198781 +1327,,ἱ,37.818466,127.71434 +2513,,5ȣ,37.562384,126.801292 +4102,,9ȣ,37.561916,126.802152 +4207,,ö,37.561842,126.801904 +4929,,,37.56236,126.801868 +1980,,ؼ,37.5617,126.8041 +2519,ġ,5ȣ,37.531768,126.846683 +2753,ġ,7ȣ,37.506207,126.810939 +227,,2ȣ,37.47693,126.963693 +2747,,7ȣ,37.486056,126.887249 +3225,û,õ2ȣ,37.448161,126.736939 +1883,δũ,κд缱,37.407722,126.695216 +331,͹̳(),3ȣ,37.485013,127.016189 +2739,,7ȣ,37.484596,126.971251 +1002,,1ȣ,37.541021,126.9713 +2828,,8ȣ,37.4624,127.13977 +1328,õ,ἱ,37.864007,127.723792 +434,·,4ȣ,37.463873,126.989134 +2823,ѻ꼺Ա(.û),8ȣ,37.451535,127.159816 +2737,,7ȣ,37.487618,126.993513 +4118,,9ȣ,37.512887,126.953222 +4117,뷮,9ȣ,37.513534,126.941005 +1004,뷮,1ȣ,37.514149,126.94271 +411,,4ȣ,37.65627,127.063276 +2715,,7ȣ,37.654836,127.060462 +313,,3ȣ,37.600927,126.935756 +2630,(걸û),6ȣ,37.534675,126.986695 +1908,,1ȣ,37.75938,127.042292 +1021,õ,1ȣ,37.644799,127.051269 +2734,,7ȣ,37.511093,127.021415 +4305,,źд缱,37.511093,127.021415 +1271,ɰ,߾Ӽ,37.618808,126.820783 +2824,ܴŸ,8ȣ,37.44521,127.156866 +4811,޹,ؼ,37.348847,126.809409 +1878,޿,κд缱,37.379681,126.745177 +2543,ʸ,5ȣ,37.566747,127.052704 +409,,4ȣ,37.670272,127.079066 +4407,,Ÿ,37.4902998,126.9275133 +237,,2ȣ,37.534946,126.902767 +4113,,9ȣ,37.533406,126.902809 +1729,,1ȣ,37.344285,126.948345 +9002,,GTX-A,37.63191,126.81113 +1953,,3ȣ,37.631626,126.811024 +1452,,4ȣ,37.435675,127.006523 +233,븲(αû),2ȣ,37.493243,126.894932 +2746,븲(αû),7ȣ,37.493013,126.897075 +1028,Ա,κд缱,37.491373,127.07272 +1005,,1ȣ,37.513342,126.926382 +4402,,Ÿ,37.5133059,126.9257265 +1320,뼺,ἱ,37.684071,127.379319 +1752,߹,4ȣ,37.328467,126.917332 +337,û,3ȣ,37.493514,127.079532 +335,ġ,3ȣ,37.494612,127.063642 +1958,ȭ,3ȣ,37.676087,126.747569 +2626,(),6ȣ,37.547771,126.942069 +1910,,1ȣ,37.818486,127.056486 +1208,,߾Ӽ,37.586781,127.208832 +1911,,1ȣ,37.843188,127.061277 +334,,3ȣ,37.490922,127.055452 +1025,,κд缱,37.491224,127.055186 +1206,,߾Ӽ,37.608806,127.161153 +247,õ,2ȣ,37.514287,126.882768 +1902,,1ȣ,37.679563,127.045595 +2712,,7ȣ,37.689241,127.046509 +1903,,1ȣ,37.689534,127.046049 +1209,,߾Ӽ,37.579622,127.222672 +1817,,1ȣ,37.468446,126.642706 +1823,ȭ,1ȣ,37.46607,126.668672 +316,,3ȣ,37.574571,126.957748 +2614,,6ȣ,37.618456,126.933031 +1714,,1ȣ,37.466613,126.889249 +3206,,õ2ȣ,37.585212,126.675844 +2644,,6ȣ,37.610537,127.056431 +155,빮,1ȣ,37.571687,127.01093 +421,빮,4ȣ,37.57093,127.009287 +205,빮繮ȭ,2ȣ,37.565613,127.009054 +422,빮繮ȭ,4ȣ,37.565133,127.007885 +2537,빮繮ȭ,5ȣ,37.564665,127.005353 +322,Ա,3ȣ,37.559052,127.005602 +1915,õ,1ȣ,37.927878,127.05479 +1913,õ߾,1ȣ,37.901885,127.056482 +3132,,õ1ȣ,37.397878,126.674005 +159,,1ȣ,37.573197,127.01648 +2637,,6ȣ,37.572279,127.015653 +4505,,ο,37.269043,127.152716 +3121,,õ1ȣ,37.485312,126.718247 +1808,,1ȣ,37.471408,126.702896 +4608,,ΰö,37.745271,127.056947 +1811,õ,1ȣ,37.475276,126.632802 +431,(),4ȣ,37.502852,126.980347 +4120,(),9ȣ,37.502878,126.978153 +4314,õ,źд缱,37.337928,127.102976 +3131,,õ1ȣ,37.404737,126.681015 +9010,ź,GTX-A,37.20034,127.09569 +1727,,1ȣ,36.833705,127.14896 +4515,,ο,37.267051,127.21364 +2555,̵,5ȣ,37.527788,127.136248 +4137,̿,9ȣ,37.519683,127.137989 +4109,,9ȣ,37.550632,126.865689 +2619,й̵Ƽ,6ȣ,37.576108,126.901391 +1294,й̵Ƽ,߾Ӽ,37.577475,126.900453 +4204,й̵Ƽ,ö,37.576958,126.898609 +210,Ҽ,2ȣ,37.547184,127.047367 +2730,Ҽ,7ȣ,37.53154,127.066704 +2515,,5ȣ,37.560183,126.825448 +4105,,9ȣ,37.566778,126.82731 +4206,,ö,37.565543,126.827378 +1955,,3ȣ,37.652206,126.77762 +2714,,7ȣ,37.66494,127.057675 +4922,,,37.640732,126.644344 +1319,,ἱ,37.652782,127.311767 +2542,,5ȣ,37.5661,127.042973 +3204,,õ2ȣ,37.597566,126.666998 +2561,õ,5ȣ,37.49499,127.152781 +2529,,5ȣ,37.539574,126.945932 +2621,û,6ȣ,37.563515,126.903343 +3224,,õ2ȣ,37.454911,126.732094 +1203,,߾Ӽ,37.59955,127.091909 +2622,,6ȣ,37.556094,126.910052 +1904,,1ȣ,37.709914,127.047455 +1869,,κд缱,37.245795,127.057353 +1872,ű,κд缱,37.265481,127.015678 +333,ź,3ȣ,37.486947,127.046769 +1870,źǼ,κд缱,37.252759,127.040566 +2720,԰,7ȣ,37.610637,127.077725 +2723,,7ȣ,37.588579,127.087503 +424,,4ȣ,37.560989,126.986325 +2552,,5ȣ,37.55137,127.143999 +4510,,ο,37.237964,127.190294 +1707,,1ȣ,37.384653,126.935433 +2827,,8ȣ,37.433824,127.129837 +1853,,κд缱,37.432052,127.129104 +3223,𷡳,õ2ȣ,37.45583,126.719298 +2521,,5ȣ,37.526065,126.864931 +2814,伺(ȭǹ),8ȣ,37.517409,127.112359 +315,,3ȣ,37.582299,126.950291 +235,,2ȣ,37.517933,126.89476 +1284,,߾Ӽ,37.854619,126.788047 +2819,,8ȣ,37.485855,127.1225 +3127,а,õ1ȣ,37.434935,126.698579 +1858,̱,κд缱,37.350077,127.10891 +4313,̱,źд缱,37.349982,127.108918 +9996,̻,5ȣ,37.560927,127.193877 +415,̾(̹),4ȣ,37.62667,127.025983 +416,̾ƻŸ,4ȣ,37.613292,127.030053 +3112,,õ1ȣ,37.553703,126.745077 +1753,ݿ,4ȣ,37.312212,126.903524 +2735,,7ȣ,37.508178,127.011727 +4601,߰,ΰö,37.727048,127.052803 +2516,߻,5ȣ,37.558598,126.837668 +225,,2ȣ,37.481426,126.997596 +2557,,5ȣ,37.508857,127.126133 +1901,,1ȣ,37.667503,127.044273 +2511,ȭ,5ȣ,37.577446,126.812741 +1405,,1ȣ,36.777629,127.052991 +1273,鸶,߾Ӽ,37.658239,126.794461 +1954,鼮,3ȣ,37.643114,126.78787 +1325,縮,ἱ,37.830779,127.58933 +1807,,1ȣ,37.483664,126.707704 +2633,Ƽ,6ȣ,37.548013,127.007055 +1457,,4ȣ,37.389793,126.950806 +4603,,ΰö,37.728755,127.04353 +1313,,ἱ,37.64202,127.12684 +408,,4ȣ,37.66778,127.11581 +1716,,1ȣ,37.207503,127.032731 +2744,,7ȣ,37.499872,126.920428 +4404,,Ÿ,37.5002739,126.9204355 +4405,Ű,Ÿ,37.4955691,126.9180827 +4406,ź,Ÿ,37.4929598,126.9234964 +2639,,6ȣ,37.585274,127.019351 +4712,,̽ż,37.585286,127.019381 +1914,,1ȣ,37.913702,127.057277 +1861,,κд缱,37.312752,127.108196 +4514,,ο,37.258965,127.218457 +2821,,8ȣ,37.471052,127.126732 +1031,,κд缱,37.470345,127.126658 +1401,,1ȣ,36.801215,127.135763 +4129,,9ȣ,37.514219,127.060245 +229,õ,2ȣ,37.482362,126.941892 +2648,ȭ(Ƿ),6ȣ,37.617283,127.091401 +1815,ΰ,1ȣ,37.488418,126.74109 +1509,ι,氭,37.260192,127.490277 +1804,õ,1ȣ,37.48405,126.782686 +2757,õû,7ȣ,37.504631,126.763538 +2754,õտ,7ȣ,37.50538,126.797337 +1982,õտ,ؼ,37.505457,126.797289 +1806,,1ȣ,37.489445,126.724506 +3120,,õ1ȣ,37.490535,126.723453 +2761,û,7ȣ,37.507394,126.721599 +3118,û,õ1ȣ,37.508407,126.720555 +3122,Ÿ,õ1ȣ,37.477679,126.710208 +3119,,õ1ȣ,37.498383,126.722244 +4709,ѻ꺸,̽ż,37.612072,127.008251 +4701,ѻ,̽ż,37.662909,127.012706 +312,ұ,3ȣ,37.610553,126.92982 +2613,ұ,6ȣ,37.610873,126.92939 +2724,簡,7ȣ,37.580894,127.088478 +226,,2ȣ,37.476538,126.981544 +433,,4ȣ,37.476955,126.981651 +1315,縪,ἱ,37.65108,127.176933 +1877,縮,κд缱,37.28998,126.85685 +4926,(û),,37.620249,126.719731 +4124,,9ȣ,37.504206,127.015259 +3762,,7ȣ,37.5086,126.7035277 +1751,꺻,4ȣ,37.358101,126.933274 +2822,꼺,8ȣ,37.457122,127.149908 +4508,ﰡ,ο,37.242115,127.168075 +428,ﰢ,4ȣ,37.534075,126.9726 +2629,ﰢ,6ȣ,37.535534,126.974032 +1503,ﵿ,氭,37.409522,127.20336 +2759,ü,7ȣ,37.506411,126.742153 +3759,ü,7ȣ,37.50724,126.74179 +9006,Z,GTX-A,37.50887,127.06324 +219,Z(),2ȣ,37.508844,127.06316 +4128,Z߾,9ȣ,37.513011,127.053282 +1950,,3ȣ,37.653083,126.895558 +4706,,̽ż,37.626914,127.018106 +4707,Ÿ,̽ż,37.621337,127.020473 +4131,,9ȣ,37.504738,127.088025 +1866,,κд缱,37.26181,127.108847 +410,,4ȣ,37.660878,127.073572 +2741,,7ȣ,37.502834,126.94791 +3758,,7ȣ,37.505814,126.753163 +1754,ϼ,4ȣ,37.302795,126.866489 +2722,(ÿܹ͹̳),7ȣ,37.595577,127.085716 +1202,(ÿܹ͹̳),߾Ӽ,37.596678,127.08504 +2624,,6ȣ,37.547716,126.922852 +207,սʸ,2ȣ,37.564354,127.029354 +2643,(ѱб),6ȣ,37.606377,127.048491 +2554,ϵ,5ȣ,37.556712,127.166417 +1322,õ,ἱ,37.770246,127.454821 +4317,,źд缱,37.297664,127.069342 +4609,,ΰö,37.748885,127.06362 +2617,(Ż),6ȣ,37.591148,126.913629 +4116,,9ȣ,37.517274,126.928422 +4401,,Ÿ,37.5170969,126.929399 +1263,,߾Ӽ,37.551881,126.935711 +3210,û,õ2ȣ,37.543742,126.676787 +2533,빮,5ȣ,37.565773,126.966641 +1749,ź,1ȣ,37.195504,127.051672 +3214,οȸ,õ2ȣ,37.500168,126.675795 +1009,,߾Ӽ,37.519594,126.988537 +9005,,GTX-A,37.55569,126.97296 +4410,뺥óŸ,Ÿ,37.4720019,126.9339351 +228,Ա(DZû),2ȣ,37.481247,126.952739 +1847,」,κд缱,37.543617,127.044707 +426,↑,4ȣ,37.55281,126.972556 +1001,↑,1ȣ,37.554337,126.971134 +1291,↑,߾Ӽ,37.557231,126.97103 +4201,↑,ö,37.553247,126.969769 +4403,溴û,Ÿ,37.5060464,126.9227083 +4409,,Ÿ,37.4782341,126.9330365 +1722,,1ȣ,37.056496,127.052819 +224,,2ȣ,37.491897,127.007917 +1855,,κд缱,37.385126,127.123592 +2645,,6ȣ,37.614872,127.065595 +1018,,1ȣ,37.614532,127.065934 +3763,(źϽ),7ȣ,37.5062285,126.6762813 +3213,(źϽ),õ2ȣ,37.506193,126.676203 +3220,,õ2ȣ,37.457611,126.692575 +1704,,1ȣ,37.435047,126.902295 +3222,õŸ,õ2ȣ,37.456805,126.709986 +2816,,8ȣ,37.505557,127.106832 +4133,,9ȣ,37.505208,127.10704 +4132,̰,9ȣ,37.502558,127.097033 +220,,2ȣ,37.504286,127.048203 +1023,,κд缱,37.504856,127.048807 +1450,,4ȣ,37.451673,127.002303 +4812,,ؼ,37.334353,126.809904 +4112,,9ȣ,37.53802,126.893525 +4127,,9ȣ,37.510297,127.043999 +1850,,κд缱,37.510735,127.043677 +3128,,õ1ȣ,37.426684,126.698863 +1711,հ,1ȣ,37.300349,126.97075 +1512,,氭,37.39468,127.11945 +9008,,GTX-A,37.39467,127.12058 +4316,,źд缱,37.313335,127.0801 +211,,2ȣ,37.544581,127.055961 +418,ſԱ(),4ȣ,37.592612,127.016441 +4711,ſԱ(),̽ż,37.592467,127.016516 +1725,ȯ,1ȣ,36.916076,127.126964 +1715,,1ȣ,37.245025,127.013222 +1717,,1ȣ,37.187533,127.04318 +1510,ո,氭,37.295309,127.570938 +3137,Ʈũ,õ1ȣ,37.393054,126.634729 +1880,ҷ,κд缱,37.40095,126.733522 +1814,һ,1ȣ,37.482753,126.79544 +4804,һ,ؼ,37.483279,126.795023 +4805,һ,ؼ,37.468467,126.797252 +1916,ҿ,1ȣ,37.9481,127.061034 +4702,ֹ,̽ż,37.65603,127.013273 +4708,ֻ,̽ż,37.620238,127.013626 +1805,۳,1ȣ,37.4876,126.753664 +1886,۵,κд缱,37.428514,126.657772 +3139,۵޺,õ1ȣ,37.407143,126.62597 +4614,ۻ,ΰö,37.737279,127.087159 +2514,,5ȣ,37.561184,126.811973 +1721,ź,1ȣ,37.075696,127.054301 +2817,,8ȣ,37.499703,127.112183 +4134,ij,9ȣ,37.510372,127.112216 +1856,,κд缱,37.378455,127.114322 +2713,,7ȣ,37.67785,127.055315 +1763,,4ȣ,37.349801,126.925365 +1267,,߾Ӽ,37.580842,126.895611 +339,,3ȣ,37.487378,127.101907 +1030,,κд缱,37.487472,127.101422 +9007,,GTX-A,37.48637,127.10161 +1713,,1ȣ,37.266348,126.999561 +1846,,κд缱,37.265917,126.999422 +1871,û,κд缱,37.261911,127.030736 +414,(ϱû),4ȣ,37.638052,127.025732 +4315,û,źд缱,37.322702,127.095026 +2826,,8ȣ,37.437428,127.140722 +427,Ա(),4ȣ,37.54456,126.972106 +2740,ǴԱ(),7ȣ,37.496029,126.953822 +1889,,κд缱,37.460789,126.638297 +3219,ùΰ(ȭâ),õ2ȣ,37.458335,126.681192 +151,û,1ȣ,37.565715,126.977088 +201,û,2ȣ,37.563588,126.975411 +4509,û.δ,ο,37.239151,127.178406 +4810,ɰ,ؼ,37.369864,126.808573 +4806,,ؼ,37.450145,126.793041 +4809,û,ؼ,37.382223,126.805625 +1864,Ű,κд缱,37.286102,127.111313 +2539,űȣ,5ȣ,37.554548,127.020331 +2526,ű,5ȣ,37.517623,126.914839 +1032,ű,1ȣ,37.516862,126.917865 +1760,űõ,4ȣ,37.338212,126.765844 +2649,ų,6ȣ,37.613174,127.102231 +1311,ų,ἱ,37.612887,127.103218 +4125,ų,9ȣ,37.504598,127.02506 +4306,ų,źд缱,37.504598,127.02506 +245,Ŵ,2ȣ,37.57004,127.046481 +206,Ŵ,2ȣ,37.56564,127.019614 +2636,Ŵ,6ȣ,37.566154,127.016146 +231,Ŵ,2ȣ,37.487462,126.913149 +2743,ŴŸ,7ȣ,37.499701,126.928276 +234,ŵ,2ȣ,37.508961,126.891084 +1007,ŵ,1ȣ,37.508787,126.891144 +1507,ŵе,氭,37.317185,127.40476 +230,Ÿ,2ȣ,37.484201,126.929715 +4408,Ÿ,Ÿ,37.4849266,126.9296159 +4111,Ÿ,9ȣ,37.544277,126.88308 +4122,Ź,9ȣ,37.503415,126.995925 +4104,Źȭ,9ȣ,37.567532,126.816601 +327,Ż,3ȣ,37.516334,127.020114 +4304,Ż,źд缱,37.516334,127.020114 +156,ż,1ȣ,37.576048,127.024634 +246,ż,2ȣ,37.574747,127.024932 +4713,ż,̽ż,37.576095,127.023242 +3129,ſ,õ1ȣ,37.41804,126.693863 +429,ſ,4ȣ,37.52917,126.967894 +1213,ſ,߾Ӽ,37.525545,127.372921 +1017,̹,1ȣ,37.601854,127.067325 +2520,(),5ȣ,37.524997,126.856191 +249,װŸ,2ȣ,37.520074,126.852912 +3756,ߵ,7ȣ,37.50282,126.77566 +1408,â(õ),1ȣ,36.769502,126.951108 +4807,õ,ؼ,37.439066,126.786788 +240,,2ȣ,37.555131,126.936926 +1252,,߾Ӽ,37.559733,126.942597 +1890,,κд缱,37.46874,126.623853 +2745,dz,7ȣ,37.50008,126.90993 +4808,,ؼ,37.409008,126.788017 +2825,,8ȣ,37.440918,127.147564 +413,ֹ,4ȣ,37.648627,127.034709 +1402,ֿ(緿),1ȣ,36.793759,127.1214 +1403,ƻ,1ȣ,36.792053,127.104361 +3209,ƽþƵ(̻Ÿ),õ2ȣ,37.5517,126.677122 +1215,ƽ,߾Ӽ,37.51382,127.443173 +2546,(̴Ĺ),5ȣ,37.551691,127.089761 +242,,2ȣ,37.557345,126.956141 +318,ȱ,3ȣ,37.576477,126.985443 +1759,Ȼ,4ȣ,37.327082,126.788532 +2640,Ⱦ(뺴),6ȣ,37.586272,127.029005 +1706,Ⱦ,1ȣ,37.401592,126.922874 +2811,ϻ,8ȣ,37.55021,127.127562 +326,б,3ȣ,37.527072,127.028461 +1848,бε,κд缱,37.527381,127.040534 +2531,ֿ,5ȣ,37.553736,126.95682 +1277,ߴ,߾Ӽ,37.712327,126.761356 +1876,߸,κд缱,37.264179,126.879483 +1854,ž,κд缱,37.411185,127.128715 +323,,3ȣ,37.554867,127.010541 +2634,,6ȣ,37.554263,127.010358 +1212,,߾Ӽ,37.545981,127.329098 +1204,,߾Ӽ,37.606596,127.107906 +332,(ʱû),3ȣ,37.484477,127.033902 +4308,(ʱû),źд缱,37.483809,127.034653 +4309,ùǽ(),źд缱,37.470023,127.03842 +1207,,߾Ӽ,37.60533,127.19364 +1909,,1ȣ,37.774381,127.044708 +248,õû,2ȣ,37.512398,126.865819 +4106,õⱳ,9ȣ,37.568381,126.841333 +4920,,,37.642379,126.614309 +2523,,5ȣ,37.525569,126.886129 +1217,,߾Ӽ,37.492773,127.491837 +4613,,ΰö,37.742802,127.085035 +2728,̴(),7ȣ,37.548014,127.074658 +4504,,ο,37.274917,127.143714 +1875,õ,κд缱,37.250102,126.90879 +4126,,9ȣ,37.507287,127.033868 +2528,dz,5ȣ,37.527098,126.932901 +2527,ǵ,5ȣ,37.521747,126.924357 +4115,ǵ,9ȣ,37.52176,126.92403 +1511,,氭,37.282308,127.628816 +1803,,1ȣ,37.485178,126.811502 +221,,2ȣ,37.500622,127.036456 +2612,,6ȣ,37.606021,126.922744 +1885,,κд缱,37.417804,126.67894 +311,ų,3ȣ,37.619229,126.921038 +2615,ų,6ȣ,37.618636,126.920625 +9004,ų,GTX-A,37.61878,126.9213 +1919,õ,1ȣ,38.10073,127.07372 +4110,â,9ȣ,37.546936,126.874916 +1006,,1ȣ,37.515504,126.907628 +236,û,2ȣ,37.525706,126.89661 +2524,û,5ȣ,37.5242,126.89503 +2525,,5ȣ,37.522669,126.905139 +4217,,ö,37.51202,126.524254 +1868,,κд缱,37.251568,127.071394 +3125,ȸ,õ1ȣ,37.449396,126.701012 +342,,3ȣ,37.502129,127.128319 +2558,,5ȣ,37.502057,127.127938 +406,,4ȣ,37.705,127.19281 +1802,,1ȣ,37.494526,126.845365 +1859,,κд缱,37.339824,127.108942 +2522,(񵿿),5ȣ,37.524496,126.875181 +1874,õ,κд缱,37.24304,126.963676 +1216,,߾Ӽ,37.506062,127.473868 +1719,,1ȣ,37.145885,127.06672 +1718,,1ȣ,37.168953,127.063197 +1762,̵,4ȣ,37.362357,126.738714 +325,,3ȣ,37.541684,127.017269 +1011,,߾Ӽ,37.540446,127.018672 +2752,¼(ȸԱ),7ȣ,37.492092,126.823023 +1821,¼(ȸԱ),1ȣ,37.492433,126.824086 +1407,¾õ,1ȣ,36.780483,127.003249 +2556,øȰ(ѱü),5ȣ,37.516201,127.130923 +4136,øȰ(ѱü),9ȣ,37.516269,127.130288 +3205,,õ2ȣ,37.592928,126.673203 +3202,ձ,õ2ȣ,37.59518,126.642696 +208,սʸ(û),2ȣ,37.561238,127.036954 +2541,սʸ(û),5ȣ,37.56184,127.037059 +1013,սʸ(û),߾Ӽ,37.561827,127.038352 +1016,ܴ,1ȣ,37.596073,127.063549 +244,,2ȣ,37.561904,127.050899 +250,(빮û),2ȣ,37.574028,127.038091 +2725,븶,7ȣ,37.573647,127.086727 +1219,빮,߾Ӽ,37.48223,127.594647 +1003,,1ȣ,37.529849,126.964561 +2517,,5ȣ,37.548768,126.836318 +1211,,߾Ӽ,37.554669,127.310115 +4512,.۴,ο,37.237845,127.209198 +4211,,ö,37.492904,126.49379 +4924,,,37.653867,126.68393 +3227,(â),õ2ȣ,37.440127,126.75997 +1278,,߾Ӽ,37.725826,126.767257 +9000,,GTX-A,37.71614,126.72841 +1286,õ,߾Ӽ,37.879942,126.769999 +4814,,ؼ,37.31321,126.796261 +1951,,3ȣ,37.653324,126.843041 +1218,,߾Ӽ,37.468672,127.547076 +4815,,ؼ,37.302371,126.786691 +1884,,κд缱,37.413049,126.686648 +3130,,õ1ȣ,37.412333,126.687869 +1981,,ؼ,37.5239,126.8049 +1948,,3ȣ,37.650658,126.872642 +1020,,1ȣ,37.633212,127.058831 +2642,(),6ȣ,37.601948,127.041518 +1879,,κд缱,37.391769,126.742699 +2620,Ű(),6ȣ,37.569532,126.899298 +1282,,߾Ӽ,37.796188,126.792587 +203,3,2ȣ,37.566306,126.991696 +320,3,3ȣ,37.566672,126.992548 +204,4,2ȣ,37.566595,126.997817 +2536,4,5ȣ,37.567352,126.998032 +202,Ա,2ȣ,37.566014,126.982618 +1012,,߾Ӽ,37.549946,127.034538 +2611,,6ȣ,37.598605,126.915577 +1710,ǿ,1ȣ,37.320852,126.948217 +1906,,1ȣ,37.738415,127.045958 +4605,νû,ΰö,37.739256,127.034781 +4607,߾,ΰö,37.743676,127.049565 +241,̴,2ȣ,37.556733,126.946013 +1502,̸,氭,37.394655,127.127819 +1860,̸,κд缱,37.395371,127.128248 +2738,̼,7ȣ,37.485196,126.981605 +1508,õ,氭,37.265579,127.44226 +430,(߾ӹڹ),4ȣ,37.522295,126.974733 +1008,(߾ӹڹ),߾Ӽ,37.522427,126.973406 +2631,¿,6ȣ,37.534488,126.994302 +1455,δ,4ȣ,37.401553,126.976715 +1812,õ,1ȣ,37.476079,126.616801 +1891,õ,κд缱,37.476403,126.617326 +3215,õ,õ2ȣ,37.4897,126.675208 +4213,õ1͹̳,ö,37.447464,126.452508 +4215,õ2͹̳,ö,37.460699,126.441442 +1881,õ,κд缱,37.400614,126.722478 +3226,õ,õ2ȣ,37.448769,126.752618 +3136,õԱ,õ1ȣ,37.386007,126.639484 +3124,õû,õ1ȣ,37.457263,126.702143 +3221,õû,õ2ȣ,37.456833,126.701306 +3126,õ͹̳,õ1ȣ,37.442383,126.699706 +1888,ϴ,κд缱,37.448493,126.649619 +1275,ϻ,߾Ӽ,37.682077,126.769846 +338,Ͽ,3ȣ,37.483681,127.08439 +1285,,߾Ӽ,37.888421,126.746765 +3113,,õ1ȣ,37.545059,126.738665 +3116,,õ1ȣ,37.530415,126.722527 +216,(ıû),2ȣ,37.513262,127.100159 +2815,(ıû),8ȣ,37.514692,127.104338 +215,dz,2ȣ,37.520733,127.10379 +217,ǻ,2ȣ,37.511687,127.086162 +328,,3ȣ,37.512759,127.01122 +4923,,,37.643986,126.669017 +2742,¹,7ȣ,37.504898,126.93915 +2711,,7ȣ,37.700109,127.053196 +2820,,8ȣ,37.478703,127.126191 +2544,,5ȣ,37.56144,127.064623 +1918,,1ȣ,38.02458,127.0718 +4517,.,ο,37.285342,127.219561 +4710,,̽ż,37.603133,127.013396 +1956,߻,3ȣ,37.659477,126.773359 +1454,ΰõû,4ȣ,37.426513,126.98978 +1761,,4ȣ,37.351735,126.742989 +1857,,κд缱,37.365994,127.10807 +4312,,źд缱,37.367098,127.108403 +157,⵿,1ȣ,37.578103,127.034893 +1810,,1ȣ,37.466769,126.656666 +152,,1ȣ,37.570161,126.982923 +153,3,1ȣ,37.570406,126.991847 +319,3,3ȣ,37.571605,126.991791 +2535,3,5ȣ,37.57254,126.990305 +154,5,1ȣ,37.570926,127.001849 +218,տ,2ȣ,37.511022,127.073704 +4130,տ,9ȣ,37.511426,127.076275 +1809,־,1ȣ,37.465047,126.679742 +3218,־,õ2ȣ,37.464992,126.679098 +3217,־ȱ,õ2ȣ,37.473703,126.68113 +1957,ֿ,3ȣ,37.670072,126.761334 +1862,,κд缱,37.324753,127.107395 +2716,߰,7ȣ,37.644583,127.064303 +2726,߰,7ȣ,37.565923,127.08432 +1822,ߵ,1ȣ,37.486562,126.764843 +1201,߶,߾Ӽ,37.594917,127.076116 +1756,߾,4ȣ,37.315941,126.838573 +4138,߾Ӻƺ,9ȣ,37.529191,127.148739 +2721,ȭ,7ȣ,37.602545,127.079264 +4108,,9ȣ,37.557402,126.861939 +2618,(),6ȣ,37.583876,126.909645 +4503,,ο,37.269606,127.136515 +3135,,õ1ȣ,37.378384,126.645168 +1723,,1ȣ,37.0188,127.070444 +309,,3ȣ,37.648033,126.913917 +1220,,߾Ӽ,37.476393,127.629874 +1912,,1ȣ,37.892334,127.055716 +1726,,1ȣ,36.870593,127.143904 +1720,,1ȣ,37.109447,127.062278 +405,,4ȣ,37.7205,127.2034 +412,â,4ȣ,37.653088,127.047274 +1022,â,߾Ӽ,37.653007,127.047806 +2638,â,6ȣ,37.579661,127.015241 +1318,õ,ἱ,37.658978,127.285379 +1728,õ,1ȣ,36.810005,127.146826 +2751,õ,7ȣ,37.486637,126.838713 +2548,õȣ(dz伺),5ȣ,37.53864,127.123308 +2812,õȣ(dz伺),8ȣ,37.538113,127.123254 +2749,ö,7ȣ,37.47605,126.867911 +4310,ûԱ,źд缱,37.447211,127.055664 +2538,û,5ȣ,37.560276,127.013639 +2635,û,6ȣ,37.560608,127.013986 +2731,û,7ȣ,37.519365,127.05335 +4210,û,ö,37.556409,126.624648 +158,û(øԱ),1ȣ,37.579956,127.044585 +1014,û(øԱ),߾Ӽ,37.580759,127.0483 +1867,û,κд缱,37.259489,127.078934 +1321,û,ἱ,37.735488,127.42661 +4506,ʴ,ο,37.260752,127.159443 +1917,ʼ,1ȣ,37.98172,127.06912 +1505,ʿ,氭,37.374419,127.299 +4813,,ؼ,37.319619,126.808147 +1758,,4ȣ,37.320646,126.805913 +432,ѽŴԱ(̼),4ȣ,37.486263,126.981989 +3755,,7ȣ,37.50365,126.78828 +1329,õ,ἱ,37.885054,127.717023 +321,湫,3ȣ,37.56143,126.994072 +423,湫,4ȣ,37.561207,126.99408 +243,(Ա),2ȣ,37.559704,126.964378 +2532,(Ա),5ȣ,37.560236,126.9629 +3133,ķ۽Ÿ,õ1ȣ,37.387855,126.661673 +9001,Ųؽ,GTX-A,37.66532,126.74843 +1276,ź,߾Ӽ,37.694023,126.761086 +4615,ž,ΰö,37.733579,127.088704 +1404,,1ȣ,36.78866,127.08485 +2646,¸Ա,6ȣ,37.617338,127.074735 +2719,¸Ա,7ȣ,37.618294,127.075397 +1852,,κд缱,37.440019,127.127709 +3134,ũũ,õ1ȣ,37.382268,126.656365 +1314,,ἱ,37.648311,127.143952 +1283,,߾Ӽ,37.815298,126.792783 +1501,DZ,氭,37.394761,127.111217 +4311,DZ,źд缱,37.394761,127.112217 +1210,ȴ,߾Ӽ,37.547371,127.243939 +1317,ȣ,ἱ,37.653225,127.244493 +1456,,4ȣ,37.394287,126.963883 +1724,,1ȣ,36.990726,127.085159 +4927,dz,,37.612488,126.732387 +1274,dz,߾Ӽ,37.672346,126.786243 +2717,ϰ,7ȣ,37.636352,127.06799 +2565,ϳ˴ܻ,5ȣ,37.53972,127.22345 +2566,ϳû(dz-),5ȣ,37.54205,127.20612 +2564,ϳdz,5ȣ,37.552034,127.203864 +2733,е,7ȣ,37.514229,127.031656 +336,п,3ȣ,37.496663,127.070594 +2632,Ѱ,6ȣ,37.539631,127.001725 +1010,ѳ,߾Ӽ,37.52943,127.009169 +1755,Ѵ,4ȣ,37.309689,126.85344 +419,ѼԱ(Q),4ȣ,37.588458,127.006221 +4135,Ѽ,9ȣ,37.516404,127.116503 +209,Ѿ,2ȣ,37.555273,127.043655 +1024,Ƽ,κд缱,37.496237,127.052873 +238,,2ȣ,37.549457,126.913808 +2623,,6ȣ,37.549209,126.913366 +2540,,5ȣ,37.557322,127.029476 +1270,,߾Ӽ,37.612102,126.834146 +420,ȭ,4ȣ,37.582336,127.001844 +1882,ȣ,κд缱,37.401637,126.708627 +239,ȫԱ,2ȣ,37.55679,126.923708 +1293,ȫԱ,߾Ӽ,37.557641,126.926683 +1264,ȫԱ,߾Ӽ,37.557641,126.926683 +4203,ȫԱ,ö,37.557438,126.926715 +314,ȫ,3ȣ,37.589066,126.943736 +4705,ȭ,̽ż,37.634133,127.017511 +2518,ȭ,5ȣ,37.541513,126.840461 +2647,ȭ(←Ա),6ȣ,37.620064,127.084689 +1712,ȭ,1ȣ,37.283862,126.989627 +1268,ȭ,߾Ӽ,37.602888,126.868387 +1952,ȭ,3ȣ,37.634592,126.83265 +1015,ȸ,߾Ӽ,37.58946,127.057583 +1905,ȸ,1ȣ,37.724416,127.04736 +4602,ȸ,ΰö,37.725006,127.047073 +425,ȸ(빮),4ȣ,37.558514,126.978246 +4611,ȿ,ΰö,37.754025,127.076902 +2628,ȿâ,6ȣ,37.539233,126.961384 +1261,ȿâ,߾Ӽ,37.538579,126.96221 +4119,漮(߾ӴԱ),9ȣ,37.50877,126.963708 +4606,Q,ΰö,37.743302,127.037023 \ No newline at end of file diff --git a/frontend/playwright/tests/api/renderTab.spec.ts b/frontend/playwright/tests/api/renderTab.spec.ts index bc25eabbf..852cdff7a 100644 --- a/frontend/playwright/tests/api/renderTab.spec.ts +++ b/frontend/playwright/tests/api/renderTab.spec.ts @@ -47,7 +47,7 @@ test('체크리스트 편집 페이지에 들어가면 탭과 질문들이 잘 await expect(page.getByText('체크리스트 편집')).toBeVisible(); const tabs = page.locator('.tab'); - await expect(tabs).toHaveCount(6, { timeout: 3000 }); + await expect(tabs).toHaveCount(5, { timeout: 3000 }); for (let i = 2; i < DefaultChecklistTabsNames.length; i++) { await expect(tabs.nth(i)).toContainText(DefaultChecklistTabsNames[i].name); diff --git a/frontend/playwright/tests/mock/postNewChecklist.spec.ts b/frontend/playwright/tests/mock/postNewChecklist.spec.ts index d6c507b28..139b54778 100644 --- a/frontend/playwright/tests/mock/postNewChecklist.spec.ts +++ b/frontend/playwright/tests/mock/postNewChecklist.spec.ts @@ -6,5 +6,5 @@ test('빈 체크리스트를 제출할 수 있다.', async ({ page }) => { await page.goto(ROUTE_PATH.checklistNew); await page.getByRole('button', { name: '저장' }).click(); await page.getByRole('button', { name: '체크리스트 저장하기' }).click(); - await page.waitForURL(ROUTE_PATH.checklistList); + await page.waitForURL(`/checklist/*`); }); diff --git a/frontend/src/components/ChecklistQuestionSelect/ChecklistQuestionSelectTabs.tsx b/frontend/src/components/ChecklistQuestionSelect/ChecklistQuestionSelectTabs.tsx index 45a3152b6..e944e69d0 100644 --- a/frontend/src/components/ChecklistQuestionSelect/ChecklistQuestionSelectTabs.tsx +++ b/frontend/src/components/ChecklistQuestionSelect/ChecklistQuestionSelectTabs.tsx @@ -5,15 +5,15 @@ import useGetAllChecklistQuestionQuery from '@/hooks/query/useGetAllChecklistQue import useTabs from '@/hooks/useTabs'; export const ChecklistQuestionSelectTabs = () => { - const { data: checklistQuestions, isFetched } = useGetAllChecklistQuestionQuery(); + const { data: checklistQuestions, isSuccess } = useGetAllChecklistQuestionQuery(); const { getTabs } = useTabs(); const selectTabs = useMemo(() => { - if (isFetched && checklistQuestions) { + if (isSuccess && checklistQuestions) { return getTabs(checklistQuestions); } return []; - }, [isFetched, getTabs]); + }, [isSuccess]); return ( diff --git a/frontend/src/components/ChecklistQuestionSelect/QuestionListTemplate/QuestionListTemplate.tsx b/frontend/src/components/ChecklistQuestionSelect/QuestionListTemplate/QuestionListTemplate.tsx index dbd2594b2..527d4b47c 100644 --- a/frontend/src/components/ChecklistQuestionSelect/QuestionListTemplate/QuestionListTemplate.tsx +++ b/frontend/src/components/ChecklistQuestionSelect/QuestionListTemplate/QuestionListTemplate.tsx @@ -4,11 +4,12 @@ import { useEffect } from 'react'; import CounterBox from '@/components/_common/CounterBox/CounterBox'; import { useTabContext } from '@/components/_common/Tabs/TabContext'; import QuestionCardList from '@/components/ChecklistQuestionSelect/QuestionCardList/QuestionCardList'; +import SKQuestionSelectList from '@/components/skeleton/QuestionSelect/SKQuestionSelectList'; import useGetAllChecklistQuestionQuery from '@/hooks/query/useGetAllChecklistQuestionsQuery'; import useChecklistQuestionSelectStore from '@/store/useChecklistQuestionSelectStore'; const QuestionListTemplate = () => { - const { data: checklistQuestions } = useGetAllChecklistQuestionQuery(); + const { data: checklistQuestions, isLoading } = useGetAllChecklistQuestionQuery(); const { checklistAllQuestionList, selectedQuestions, setChecklistAllQuestionList, getCategoryQuestions } = useChecklistQuestionSelectStore(); const { currentTabId } = useTabContext(); @@ -20,6 +21,8 @@ const QuestionListTemplate = () => { setChecklistAllQuestionList(checklistQuestions || []); }, [checklistQuestions]); + if (isLoading) return ; + return ( diff --git a/frontend/src/components/EditChecklist/ChecklistContent/EditChecklistContent.tsx b/frontend/src/components/EditChecklist/ChecklistContent/EditChecklistContent.tsx new file mode 100644 index 000000000..bb3398ea8 --- /dev/null +++ b/frontend/src/components/EditChecklist/ChecklistContent/EditChecklistContent.tsx @@ -0,0 +1,40 @@ +import styled from '@emotion/styled'; +import { Suspense } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; + +import ListErrorFallback from '@/components/_common/errorBoundary/ListErrorFallback'; +import { useTabContext } from '@/components/_common/Tabs/TabContext'; +import EditChecklistQuestionTemplate from '@/components/EditChecklist/ChecklistContent/EditChecklistQuestionTemplate'; +import RoomInfoTemplate from '@/components/NewChecklist/NewRoomInfoForm/RoomInfoTemplate'; +import OptionTemplate from '@/components/NewChecklist/Option/OptionTemplate'; + +const EditChecklistContent = () => { + const { currentTabId } = useTabContext(); + + return ( + + {/*방 기본정보 템플릿 */} + {currentTabId === -1 && } + {/* 옵션 선택 템플릿 */} + {currentTabId === 0 && } + {/* 체크리스트 템플릿 */} + {currentTabId > 0 && ( + + + + + + )} + + ); +}; + +const S = { + Container: styled.main` + position: relative; + width: 100%; + height: 100%; + `, +}; + +export default EditChecklistContent; diff --git a/frontend/src/components/EditChecklist/ChecklistContent/EditChecklistQuestionTemplate.tsx b/frontend/src/components/EditChecklist/ChecklistContent/EditChecklistQuestionTemplate.tsx new file mode 100644 index 000000000..15b691735 --- /dev/null +++ b/frontend/src/components/EditChecklist/ChecklistContent/EditChecklistQuestionTemplate.tsx @@ -0,0 +1,58 @@ +import styled from '@emotion/styled'; + +import Divider from '@/components/_common/Divider/Divider'; +import Layout from '@/components/_common/layout/Layout'; +import { useTabContext } from '@/components/_common/Tabs/TabContext'; +import ChecklistQuestionItem from '@/components/NewChecklist/ChecklistQuestion/ChecklistQuestion'; +import useChecklistStore from '@/store/useChecklistStore'; +import { flexColumn } from '@/styles/common'; +import theme from '@/styles/theme'; +import { ChecklistQuestion } from '@/types/checklist'; + +const EditChecklistQuestionTemplate = () => { + useChecklistStore(store => store.checklistCategoryQnA); + + const { currentTabId } = useTabContext(); + const checklistActions = useChecklistStore(store => store.actions); + const questions = checklistActions.getCategory(currentTabId); + + return ( + + + {questions?.questions.map((question: ChecklistQuestion, index) => { + const answer = checklistActions.getQuestionAnswer({ + categoryId: currentTabId, + questionId: question.questionId, + }); + const isLastQuestion = questions?.questions.length - 1 === index; + return ( + <> + + {!isLastQuestion && } + + ); + })} + + + ); +}; + +export default EditChecklistQuestionTemplate; + +const S = { + ContentBox: styled.div` + ${flexColumn} + margin-bottom: 2rem; + border-radius: 0.8rem; + + background-color: ${({ theme }) => theme.palette.white}; + gap: 0.2rem; + `, + QuestionBox: styled.div` + background-color: ${({ theme }) => theme.palette.white}; + `, +}; diff --git a/frontend/src/components/EditChecklist/ChecklistTab/EditChecklistTab.tsx b/frontend/src/components/EditChecklist/ChecklistTab/EditChecklistTab.tsx index f0b5b77fc..4af39950b 100644 --- a/frontend/src/components/EditChecklist/ChecklistTab/EditChecklistTab.tsx +++ b/frontend/src/components/EditChecklist/ChecklistTab/EditChecklistTab.tsx @@ -1,28 +1,29 @@ import { useMemo } from 'react'; -import ChecklistTabFallback from '@/components/_common/errorBoundary/ChecklistTabFallback'; +import ChecklistTabSuspense from '@/components/_common/errorBoundary/ChecklistTabSuspense'; import Tabs from '@/components/_common/Tabs/Tabs'; import useGetChecklistDetailQuery from '@/hooks/query/useGetChecklistDetailQuery'; import useTabs from '@/hooks/useTabs'; import useChecklistStore from '@/store/useChecklistStore'; +import isSameCategory from '@/utils/isSameCategory'; interface Props { checklistId: string; } const EditChecklistTab = ({ checklistId }: Props) => { - const { data: checklist, isFetched, isLoading } = useGetChecklistDetailQuery(checklistId); + const { data: checklist, isSuccess, isLoading } = useGetChecklistDetailQuery(checklistId); const checklistStore = useChecklistStore(state => state.checklistCategoryQnA); const { getTabsForChecklist } = useTabs(); const categoryTabs = useMemo(() => { - if (isFetched && checklist && checklistStore.length) { + if (isSuccess && isSameCategory(checklist.categories, checklistStore)) { return getTabsForChecklist(checklist.categories); } return []; - }, [isFetched, getTabsForChecklist, checklistStore]); + }, [isSuccess, checklistStore]); - if (isLoading) return ; + if (isLoading) return ; return ; }; diff --git a/frontend/src/components/NewChecklist/ChecklistTab/NewChecklistTab.tsx b/frontend/src/components/NewChecklist/ChecklistTab/NewChecklistTab.tsx index 078deb92c..06caa21fb 100644 --- a/frontend/src/components/NewChecklist/ChecklistTab/NewChecklistTab.tsx +++ b/frontend/src/components/NewChecklist/ChecklistTab/NewChecklistTab.tsx @@ -1,24 +1,25 @@ import { useMemo } from 'react'; -import ChecklistTabFallback from '@/components/_common/errorBoundary/ChecklistTabFallback'; +import ChecklistTabSuspense from '@/components/_common/errorBoundary/ChecklistTabSuspense'; import Tabs from '@/components/_common/Tabs/Tabs'; import useInitialChecklist from '@/hooks/useInitialChecklist'; import useTabs from '@/hooks/useTabs'; import useChecklistStore from '@/store/useChecklistStore'; +import isSameCategory from '@/utils/isSameCategory'; const NewChecklistTab = () => { - const { data: checklist, isFetched, isLoading } = useInitialChecklist(); + const { data: checklist, isSuccess, isLoading } = useInitialChecklist(); const checklistStore = useChecklistStore(state => state.checklistCategoryQnA); const { getTabsForChecklist } = useTabs(); const categoryTabs = useMemo(() => { - if (isFetched && checklist) { + if (isSuccess && isSameCategory(checklist, checklistStore)) { return getTabsForChecklist(checklist); } return []; - }, [isFetched, getTabsForChecklist, checklistStore]); + }, [isSuccess, checklistStore]); - if (isLoading) return ; + if (isLoading) return ; return ; }; diff --git a/frontend/src/components/_common/Like/LikeButton.tsx b/frontend/src/components/_common/Like/LikeButton.tsx index 16d92375b..c0505fac2 100644 --- a/frontend/src/components/_common/Like/LikeButton.tsx +++ b/frontend/src/components/_common/Like/LikeButton.tsx @@ -10,15 +10,14 @@ interface Props { const LikeButton = ({ isLiked = false, checklistId }: Props) => { const { mutate: toggleLike, variables, isPending } = useToggleLikeQuery(); - const handleClick = () => toggleLike({ checklistId, isLiked: !isLiked }); + const fill = isPending ? variables.isLiked : isLiked; + const handleClick = () => toggleLike({ checklistId, isLiked: !fill }); const handleClickLike = (e: React.MouseEvent) => { handleClick(); e.stopPropagation(); }; - const fill = isPending ? variables.isLiked : isLiked; - return ( { const { stationName, stationLine, walkingTime } = station; return ( - + {stationLine?.map(oneLine => )} diff --git a/frontend/src/components/_common/errorBoundary/ChecklistTabSuspense.tsx b/frontend/src/components/_common/errorBoundary/ChecklistTabSuspense.tsx new file mode 100644 index 000000000..e131c8b7c --- /dev/null +++ b/frontend/src/components/_common/errorBoundary/ChecklistTabSuspense.tsx @@ -0,0 +1,13 @@ +import Tabs from '@/components/_common/Tabs/Tabs'; +import { TabWithCompletion } from '@/types/tab'; + +const ChecklistTabSuspense = () => { + const tabsWithFailed: TabWithCompletion[] = [ + { id: -1, name: '기본정보', isCompleted: true }, + { id: 0, name: '옵션', isCompleted: true }, + ]; + + return ; +}; + +export default ChecklistTabSuspense; diff --git a/frontend/src/components/skeleton/Main/SkChecklistSection.tsx b/frontend/src/components/skeleton/Main/SkChecklistSection.tsx index 86d09ab3a..877212b25 100644 --- a/frontend/src/components/skeleton/Main/SkChecklistSection.tsx +++ b/frontend/src/components/skeleton/Main/SkChecklistSection.tsx @@ -1,13 +1,12 @@ import styled from '@emotion/styled'; +import { MAX_CHECKLISTS_DISPLAY_COUNT } from '@/constants/system'; import { flexColumn, Skeleton } from '@/styles/common'; -export const SHOW_CHECKLIST_COUNT = 3; - const SkChecklistSection = () => { return ( <> - {Array.from({ length: SHOW_CHECKLIST_COUNT }).map((_, index) => ( + {Array.from({ length: MAX_CHECKLISTS_DISPLAY_COUNT }).map((_, index) => ( ))} diff --git a/frontend/src/components/skeleton/QuestionSelect/SKQuestionSelectList.tsx b/frontend/src/components/skeleton/QuestionSelect/SKQuestionSelectList.tsx new file mode 100644 index 000000000..aeca66fa9 --- /dev/null +++ b/frontend/src/components/skeleton/QuestionSelect/SKQuestionSelectList.tsx @@ -0,0 +1,39 @@ +import styled from '@emotion/styled'; + +import { flexColumn, flexRow, Skeleton } from '@/styles/common'; + +const SHOW_SELECT_QUESTIONS = 7; + +const SKQuestionSelectList = () => { + return ( + + {Array.from({ length: SHOW_SELECT_QUESTIONS }, (_, index) => ( + + ))} + + ); +}; + +export default SKQuestionSelectList; + +const S = { + Container: styled.article` + ${flexColumn} + box-sizing: border-box; + width: 100%; + margin-top: 1rem; + padding: 1rem; + border-radius: 0.8rem; + + gap: 1rem; + + background-color: white; + `, + Row: styled.div` + ${Skeleton} + width: 100%; + height: 5rem; + ${flexRow} + gap: 1rem; + `, +}; diff --git a/frontend/src/hooks/useInitialChecklist.ts b/frontend/src/hooks/useInitialChecklist.ts index 646f22141..bb47f917e 100644 --- a/frontend/src/hooks/useInitialChecklist.ts +++ b/frontend/src/hooks/useInitialChecklist.ts @@ -1,11 +1,17 @@ +import { useEffect } from 'react'; + import useGetChecklistQuestionQuery from '@/hooks/query/useGetChecklistQuestionQuery'; import useChecklistStore from '@/store/useChecklistStore'; const useInitialChecklist = () => { const result = useGetChecklistQuestionQuery(); - const initAnswerSheetIfEmpty = useChecklistStore(state => state.actions.initAnswerSheetIfEmpty); - initAnswerSheetIfEmpty(result.data ?? []); // 체크리스트 질문에 대한 답안지 객체 생성 + + useEffect(() => { + if (result.isSuccess && result.data) { + initAnswerSheetIfEmpty(result.data); // 체크리스트 질문에 대한 답안지 객체 생성 + } + }, [result.isSuccess, result.data, initAnswerSheetIfEmpty]); return result; }; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 93d0f9d08..abd7334c2 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -10,6 +10,7 @@ async function enableMocking() { if (process.env.API_ENV !== 'msw') { return; } + const { worker } = await import('./mocks/browser'); await worker.start({ serviceWorker: { diff --git a/frontend/src/pages/EditChecklistPage.tsx b/frontend/src/pages/EditChecklistPage.tsx index 25c927d06..8bca5ccd2 100644 --- a/frontend/src/pages/EditChecklistPage.tsx +++ b/frontend/src/pages/EditChecklistPage.tsx @@ -6,8 +6,8 @@ import Button from '@/components/_common/Button/Button'; import ChecklistTabFallback from '@/components/_common/errorBoundary/ChecklistTabFallback'; import Header from '@/components/_common/Header/Header'; import { TabProvider } from '@/components/_common/Tabs/TabContext'; +import EditChecklistContent from '@/components/EditChecklist/ChecklistContent/EditChecklistContent'; import EditChecklistTab from '@/components/EditChecklist/ChecklistTab/EditChecklistTab'; -import ChecklistContent from '@/components/NewChecklist/ChecklistContent'; import MemoButton from '@/components/NewChecklist/MemoModal/MemoButton'; import MemoModal from '@/components/NewChecklist/MemoModal/MemoModal'; import SubmitModalWithSummary from '@/components/NewChecklist/SubmitModalWithSummary/SubmitModalWithSummary'; @@ -82,7 +82,7 @@ const EditChecklistPage = () => { }> - {checklist && } + {checklist && } {/* 메모 모달 */} diff --git a/frontend/src/pages/NewChecklistPage.tsx b/frontend/src/pages/NewChecklistPage.tsx index 6f29995ee..8aa6e9d02 100644 --- a/frontend/src/pages/NewChecklistPage.tsx +++ b/frontend/src/pages/NewChecklistPage.tsx @@ -1,7 +1,7 @@ import { ErrorBoundary } from 'react-error-boundary'; import { useNavigate } from 'react-router-dom'; -import { useStore } from 'zustand'; +import { useStore } from 'zustand'; import Button from '@/components/_common/Button/Button'; import ChecklistTabFallback from '@/components/_common/errorBoundary/ChecklistTabFallback'; import Header from '@/components/_common/Header/Header'; @@ -20,17 +20,15 @@ import useModal from '@/hooks/useModal'; import { trackNotCompleteChecklist, trackSaveChecklist } from '@/service/amplitude/trackEvent'; import roomInfoNonValidatedStore from '@/store/roomInfoNonValidatedStore'; import roomInfoStore from '@/store/roomInfoStore'; -import useChecklistStore from '@/store/useChecklistStore'; -import useSelectedOptionStore from '@/store/useSelectedOptionStore'; const NewChecklistPage = () => { const navigate = useNavigate(); const roomInfoActions = useStore(roomInfoStore, state => state.actions); const roomInfoNonValidatedActions = useStore(roomInfoNonValidatedStore, state => state.actions); - // TODO: useStore 포맷 맞추기 - const checklistActions = useChecklistStore(state => state.actions); - const selectedOptionActions = useSelectedOptionStore(state => state.actions); + // // TODO: useStore 포맷 맞추기 + // const checklistActions = useChecklistStore(state => state.actions); + // const selectedOptionActions = useSelectedOptionStore(state => state.actions); const { resetShowTip } = useHandleTip('OPTION'); // 메모 모달 @@ -43,12 +41,12 @@ const NewChecklistPage = () => { const { isModalOpen: isLoginModalOpen, openModal: openLoginModal, closeModal: closeLoginModal } = useModal(); const resetChecklist = () => { - roomInfoActions.reset(); - roomInfoNonValidatedActions.resetAll(); - checklistActions.reset(); - selectedOptionActions.reset(); + // roomInfoActions.reset(); + // roomInfoNonValidatedActions.resetAll(); + // checklistActions.reset(); + // selectedOptionActions.reset(); resetShowTip(); - navigate(ROUTE_PATH.checklistList); + // navigate(ROUTE_PATH.checklistList); }; const handleSaveChecklistButton = () => { @@ -57,8 +55,11 @@ const NewChecklistPage = () => { }; const handleNotCompleteChecklist = () => { + roomInfoActions.reset(); + roomInfoNonValidatedActions.resetAll(); trackNotCompleteChecklist(); resetChecklist(); + navigate(ROUTE_PATH.checklistList); }; return ( diff --git a/frontend/src/store/useChecklistStore.ts b/frontend/src/store/useChecklistStore.ts index 7fd768920..0984dde69 100644 --- a/frontend/src/store/useChecklistStore.ts +++ b/frontend/src/store/useChecklistStore.ts @@ -49,8 +49,6 @@ const useChecklistStore = create()( * 받은 질문들을 바탕으로 answer 를 추가한 답안지 객체를 생성합니다. */ initAnswerSheetIfEmpty: (questions: ChecklistCategory[]) => { - if (get().checklistCategoryQnA.length !== 0) return; - const checklistCategoryQnA: ChecklistCategoryWithAnswer[] = questions.map(category => ({ categoryId: category.categoryId, categoryName: category.categoryName, diff --git a/frontend/src/utils/isSameCategory.ts b/frontend/src/utils/isSameCategory.ts new file mode 100644 index 000000000..a25725429 --- /dev/null +++ b/frontend/src/utils/isSameCategory.ts @@ -0,0 +1,10 @@ +import { Category } from '@/types/category'; + +const isSameCategory = (arr1: Category[], arr2: Category[]): boolean => { + const ids1 = arr1.map(item => item.categoryId).sort((a, b) => a - b); + const ids2 = arr2.map(item => item.categoryId).sort((a, b) => a - b); + + return ids1.length === ids2.length && ids1.every((id, index) => id === ids2[index]); +}; + +export default isSameCategory;