diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000..a797c2b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,21 @@ +--- +name: Bug +about: 어떤 버그인지 설명해주세요 +title: 'Bug:' +labels: bug +assignees: '' + +--- + +## 어떤 버그인가요? + +어떤 버그인지 간결하게 설명해주세요 + +## 어떤 상황에서 발생한 버그인가요? +(가능하면) Given-When-Then 형식으로 서술해주세요 + +## 예상 결과 + +예상했던 정상적인 결과가 어떤 것이었는지 설명해주세요 + +## 참고할만한 자료(선택) diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 0000000..c1401fe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,19 @@ +--- +name: Feature +about: feature 작업 상황에 대해 설명해주세요 +title: 'Feature:' +labels: feature +assignees: '' + +--- + +## 어떤 기능인가요? + +추가하려는 기능에 대해 간결하게 설명해주세요 + +## Todo + +- [ ] todo +- [ ] todo + +## 참고할만한 자료(선택) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..7e1420f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +- 제목 : Feat(issue 번호): 기능명 + ex) Feat(17): pull request template 작성 + (확인 후 지워주세요) + +## 📝작업 내용 + +이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능) + +### 작업에 대한 스크린샷 (선택) + +## 💬리뷰 요구사항(선택) + +리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요 + +ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요? + +## #️⃣연관된 이슈 + +ex) #이슈번호, #이슈번호 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..042bcc7 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,85 @@ +name: deploy + +on: + push: + branches: [ "main", "develop" ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Github Repository 에 올린 파일들을 볼러오기 + uses: actions/checkout@v4 + + - name: JDK 17 버전 설치 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: application.yml 파일 만들기 + run: | + mkdir -p ./src/main/resources + echo "${{ secrets.APPLICATION_YML }}" > ./src/main/resources/application.yml + echo "${{ secrets.APPLICATION_COMMON }}" > ./src/main/resources/application-common.yml + echo "${{ secrets.APPLICATION_DEV }}" > ./src/main/resources/application-dev.yml + echo "${{ secrets.APPLICATION_PROD }}" > ./src/main/resources/application-prod.yml + echo "${{ secrets.APPLICATION_LOCAL }}" > ./src/main/resources/application-local.yml + + - name: gradlew 실행 권한 부여 + run: chmod +x ./gradlew + + - name: 빌드하기 (main 브랜치) + if: github.ref == 'refs/heads/main' + run: ./gradlew clean build -x test -PspringProfile=prod --warning-mode all --scan + + - name: 빌드하기 (develop 브랜치) + if: github.ref == 'refs/heads/develop' + run: | + ./gradlew clean build -x test -PspringProfile=dev --warning-mode all --scan + + - name: 빌드된 파일 이름 변경 + run: mv ./build/libs/*SNAPSHOT.jar ./project.jar + + - name: SCP로 EC2에 빌드된 파일 전송 + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USERNAME }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + source: project.jar + target: /home/ubuntu/solitour-server/tobe + + - name: SSH로 EC2 접속 + if: github.ref == 'refs/heads/main' + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USERNAME }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + script_stop: true + script: | + rm -rf /home/ubuntu/solitour-server/current + mkdir /home/ubuntu/solitour-server/current + mv /home/ubuntu/solitour-server/tobe/project.jar /home/ubuntu/solitour-server/current/project.jar + cd /home/ubuntu/solitour-server/current + sudo fuser -k -n tcp 8080 || true + nohup java -jar -Dspring.profiles.active=prod project.jar > ./output.log 2>&1 & + rm -rf /home/ubuntu/solitour-server/tobe + + - name: SSH로 EC2 접속 + if: github.ref == 'refs/heads/develop' + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USERNAME }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + script_stop: true + script: | + rm -rf /home/ubuntu/solitour-server/current + mkdir /home/ubuntu/solitour-server/current + mv /home/ubuntu/solitour-server/tobe/project.jar /home/ubuntu/solitour-server/current/project.jar + cd /home/ubuntu/solitour-server/current + sudo fuser -k -n tcp 8081 || true + nohup java -jar -Dspring.profiles.active=dev project.jar > ./output.log 2>&1 & + rm -rf /home/ubuntu/solitour-server/tobe diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..e466f8f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,48 @@ +name: test + +on: + pull_request: + branches: [ "main", "develop" ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Github Repository 에 올린 파일들을 볼러오기 + uses: actions/checkout@v4 + + - name: JDK 17 버전 설치 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: application.yml 파일 만들기 + run: | + mkdir -p ./src/main/resources + echo "${{ secrets.APPLICATION_YML }}" > ./src/main/resources/application.yml + echo "${{ secrets.APPLICATION_COMMON }}" > ./src/main/resources/application-common.yml + echo "${{ secrets.APPLICATION_TEST }}" > ./src/main/resources/application-test.yml + + + - name: gradlew 실행 권한 부여 + run: chmod +x ./gradlew + + - name: Gradle packages 캐시 + uses: actions/cache@v3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle + + - name: Gradle wrapper 캐시 + uses: actions/cache@v3 + with: + path: ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }} + + - name: 테스트 + run : ./gradlew clean test -PspringProfile=test --info + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58497c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,185 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ +src/main/resources/application.properties + +# AWS User-specific +.idea/**/aws.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/artifacts +.idea/compiler.xml +.idea/jarRepositories.xml +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Sonar template +#Sonar generated dir +/.sonar/ + +### SonarQube template +# SonarQube ignore files. +# +# https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner +# Sonar Scanner working directories +.sonar/ +.sonarqube/ +.scannerwork/ + +# http://www.sonarlint.org/commandline/ +# SonarLint working directories, configuration files (including credentials) +.sonarlint/ + +### Windows template +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# idea 전부 무시 +.idea/** + +# local docker db 파일 +/docker/db/ + +# 설정 파일 +/src/main/resources/**.yml +/src/main/resources/**.properties +src/main/resources/docs/index.html diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..bb9c4e9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "gradle/wrapper/config"] + path = gradle/wrapper/config + url = https://github.com/TripInfoWeb/config.git diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..bbde385 --- /dev/null +++ b/build.gradle @@ -0,0 +1,91 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.0' + id 'io.spring.dependency-management' version '1.1.5' + id 'org.asciidoctor.jvm.convert' version '3.3.2' +} + +group = 'solitour-backend' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } + asciidoctorExt +} + +repositories { + mavenCentral() +} + +ext { + set('snippetsDir', file("src/dos")) // 변경된 경로 +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + + implementation 'org.mapstruct:mapstruct:1.5.5.Final' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' + + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + runtimeOnly 'com.mysql:mysql-connector-j' + asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' + testImplementation 'com.h2database:h2' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + outputs.dir snippetsDir + useJUnitPlatform() +} + +tasks.named('asciidoctor') { + inputs.dir snippetsDir + // dependsOn test 를 제거하여 순환 종속성을 피합니다. +} + +asciidoctor { + configurations 'asciidoctorExt' + baseDirFollowsSourceFile() + inputs.dir snippetsDir +} + +asciidoctor.doFirst { + delete file('src/main/resources/docs') +} + +task copyDocument(type: Copy) { + from file("build/docs/asciidoc") + into file("src/main/resources/docs") +} + +// build 작업에 copyDocument를 추가합니다. +build { + dependsOn copyDocument +} \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..f6541f8 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3' +services: + solitour: + image: library/mysql:8.3 + container_name: solitour-db + restart: always + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: 1234 + MYSQL_DATABASE: solitour + MYSQL_USER: soli + MYSQL_PASSWORD: 1234 + TZ: Asia/Seoul + DB_HOST: host.docker.internal + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + volumes: + - ./db/mysql/data:/var/lib/mysql + - ./db/mysql/config:/etc/mysql/conf.d + - ./db/mysql/init:/docker-entrypoint-initdb.d + platform: linux/x86_64 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..0aaefbc --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..f5feea6 --- /dev/null +++ b/gradlew @@ -0,0 +1,252 @@ +#!/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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# 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 -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || 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/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@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/settings.gradle b/settings.gradle new file mode 100644 index 0000000..4ce2c50 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'solitour' diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc new file mode 100644 index 0000000..e8aebec --- /dev/null +++ b/src/docs/asciidoc/index.adoc @@ -0,0 +1,11 @@ += Solitour -Resource + +:doctype: book +:toc: left +:toclevels: 3 +:sectnums: +:numbered: + + +// [[api]] +// includ::ee.adoc[] \ No newline at end of file diff --git a/src/main/java/solitour_backend/solitour/SolitourApplication.java b/src/main/java/solitour_backend/solitour/SolitourApplication.java new file mode 100644 index 0000000..2014552 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/SolitourApplication.java @@ -0,0 +1,19 @@ +package solitour_backend.solitour; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.web.config.EnableSpringDataWebSupport; + +@SpringBootApplication +@EnableJpaAuditing +@ConfigurationPropertiesScan +@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO) +public class SolitourApplication { + + public static void main(String[] args) { + SpringApplication.run(SolitourApplication.class, args); + } + +} diff --git a/src/main/java/solitour_backend/solitour/admin/controller/AdminController.java b/src/main/java/solitour_backend/solitour/admin/controller/AdminController.java new file mode 100644 index 0000000..5e43e2b --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/controller/AdminController.java @@ -0,0 +1,26 @@ +package solitour_backend.solitour.admin.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import solitour_backend.solitour.admin.dto.UserListWithPage; +import solitour_backend.solitour.admin.service.AdminService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/admin") +public class AdminController { + + private final AdminService adminService; + + @GetMapping("/user/list") + public ResponseEntity getUserInfoList(@RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "") String nickname) { + UserListWithPage response = adminService.getUserInfoList(nickname, page); + return ResponseEntity.ok(response); + } + +} diff --git a/src/main/java/solitour_backend/solitour/admin/controller/BannerController.java b/src/main/java/solitour_backend/solitour/admin/controller/BannerController.java new file mode 100644 index 0000000..2aa5e24 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/controller/BannerController.java @@ -0,0 +1,40 @@ +package solitour_backend.solitour.admin.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import solitour_backend.solitour.admin.entity.Banner; +import solitour_backend.solitour.admin.service.BannerService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/banner") +public class BannerController { + private final BannerService bannerService; + + @RequestMapping(method = RequestMethod.POST) + public ResponseEntity createBanner(@RequestPart("imageFile") MultipartFile imageFile, + @RequestPart("directory") String directory) { + Banner banner = bannerService.createBanner(imageFile, directory); + return ResponseEntity + .status(HttpStatus.OK) + .body(banner); + } + + @RequestMapping(method = RequestMethod.GET) + public ResponseEntity getBannerList() { + return bannerService.getBannerList(); + } + + @RequestMapping(method = RequestMethod.DELETE, value = "/{id}") + public ResponseEntity deleteBanner(@PathVariable Long id) { + return bannerService.deleteBanner(id); + } + +} diff --git a/src/main/java/solitour_backend/solitour/admin/controller/NoticeController.java b/src/main/java/solitour_backend/solitour/admin/controller/NoticeController.java new file mode 100644 index 0000000..614e799 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/controller/NoticeController.java @@ -0,0 +1,74 @@ +package solitour_backend.solitour.admin.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import solitour_backend.solitour.admin.dto.request.NoticeModifyRequest; +import solitour_backend.solitour.admin.dto.request.NoticeRegisterRequest; +import solitour_backend.solitour.admin.entity.Notice; +import solitour_backend.solitour.admin.service.NoticeService; +import solitour_backend.solitour.auth.config.Authenticated; +import solitour_backend.solitour.auth.config.AuthenticationPrincipal; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/notice") +public class NoticeController { + + private final NoticeService noticeService; + + // Create a new notice + @Authenticated + @PostMapping + public ResponseEntity createNotice(@AuthenticationPrincipal Long userId, @RequestBody NoticeRegisterRequest noticeRegisterRequest) { + Long id = noticeService.createNotice(noticeRegisterRequest, userId); + return ResponseEntity.status(HttpStatus.CREATED).body(id); + } + + // Retrieve a list of notices (excluding deleted ones) + @GetMapping + public ResponseEntity> getNotices(@RequestParam(defaultValue = "0") int page) { + Page notices = noticeService.getNotices(page); + return ResponseEntity.ok(notices); + } + + // Retrieve a list of notices (excluding deleted ones) + @Authenticated + @GetMapping("/admin") + public ResponseEntity> getNoticesForAdmin(@AuthenticationPrincipal Long userId, @RequestParam(defaultValue = "0") int page) { + Page notices = noticeService.getNoticesForAdmin(page, userId); + return ResponseEntity.ok(notices); + } + + @GetMapping("/{id}") + public ResponseEntity getNotice(@PathVariable Long id) { + Notice notice = noticeService.getNotice(id); + return ResponseEntity.ok(notice); + } + + @Authenticated + @GetMapping("/admin/{id}") + public ResponseEntity getNoticeForAdmin(@AuthenticationPrincipal Long userId, @PathVariable Long id) { + Notice notice = noticeService.getNoticeForAdmin(userId, id); + return ResponseEntity.ok(notice); + } + + @Authenticated + @PutMapping + public ResponseEntity updateNotice(@AuthenticationPrincipal Long userId, @RequestBody NoticeModifyRequest noticeModifyRequest) { + boolean result = noticeService.updateNotice(userId, noticeModifyRequest); + if(result) { + return ResponseEntity.ok().build(); + } + return ResponseEntity.badRequest().build(); + } + + @Authenticated + @DeleteMapping("/{id}") + public ResponseEntity deleteNotice(@AuthenticationPrincipal Long userId, @PathVariable Long id) { + noticeService.deleteNotice(userId, id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/solitour_backend/solitour/admin/controller/QnAController.java b/src/main/java/solitour_backend/solitour/admin/controller/QnAController.java new file mode 100644 index 0000000..a28e301 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/controller/QnAController.java @@ -0,0 +1,94 @@ +package solitour_backend.solitour.admin.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import solitour_backend.solitour.admin.dto.request.AnswerRegisterRequest; +import solitour_backend.solitour.admin.dto.request.QnARegisterRequest; +import solitour_backend.solitour.admin.dto.request.QuestionRegisterRequest; +import solitour_backend.solitour.admin.dto.response.QnaListResponseDto; +import solitour_backend.solitour.admin.dto.response.QnaResponseDto; +import solitour_backend.solitour.admin.entity.QnAMessage; +import solitour_backend.solitour.admin.entity.QnA; +import solitour_backend.solitour.admin.service.QnAService; +import solitour_backend.solitour.auth.config.Authenticated; +import solitour_backend.solitour.auth.config.AuthenticationPrincipal; + +@RestController +@RequestMapping("/api/qna") +public class QnAController { + + @Autowired + private QnAService qnaService; + + @Authenticated + @GetMapping("/manage-list") + public ResponseEntity> getPagedQnAsByUserId( + @AuthenticationPrincipal Long userId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "ALL") String status, + @RequestParam(required = false) String keyword + ) { + Page qnaPage = qnaService.getQnAsByPageStatusAndKeyword(page, status, keyword, userId); + + return ResponseEntity + .status(HttpStatus.OK) + .body(qnaPage); + } + + @Authenticated + @PostMapping + public ResponseEntity createQnA(@AuthenticationPrincipal Long userId, @RequestBody QnARegisterRequest qnARegisterRequest) { + Long qnaId = qnaService.createQnA(qnARegisterRequest, userId); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(qnaId); + } + + @Authenticated + @GetMapping + public Page getPagedQnAsByUserId( + @AuthenticationPrincipal Long userId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + return qnaService.getPagedQnAsByUserId(userId, page, size); + } + + @Authenticated + @GetMapping("/{id}") + public ResponseEntity getQnAById(@AuthenticationPrincipal Long userId, @PathVariable Long id) { + QnaResponseDto qna = qnaService.getQnAById(id, userId); + return ResponseEntity + .status(HttpStatus.OK) + .body(qna); + } + + @Authenticated + @DeleteMapping("/{id}") + public ResponseEntity closeQnAStatus(@AuthenticationPrincipal Long userId, @PathVariable Long id) { + qnaService.closeQnA(id, userId); + return ResponseEntity + .status(HttpStatus.OK) + .build(); + } + + @Authenticated + @PostMapping("/question") + public ResponseEntity createQuestion(@AuthenticationPrincipal Long userId, @RequestBody QuestionRegisterRequest questionRegisterRequest) { + QnAMessage qnaMessage = qnaService.createQuestion(questionRegisterRequest, userId); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(qnaMessage); + } + + @Authenticated + @PostMapping("/answer") + public ResponseEntity createAnswer(@AuthenticationPrincipal Long userId, @RequestBody AnswerRegisterRequest answerRegisterRequest) { + QnAMessage qnaMessage = qnaService.createAnswer(answerRegisterRequest, userId); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(qnaMessage); + } +} \ No newline at end of file diff --git a/src/main/java/solitour_backend/solitour/admin/dto/UserListResponseDTO.java b/src/main/java/solitour_backend/solitour/admin/dto/UserListResponseDTO.java new file mode 100644 index 0000000..ec325fd --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/dto/UserListResponseDTO.java @@ -0,0 +1,33 @@ +package solitour_backend.solitour.admin.dto; + +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import solitour_backend.solitour.user.user_status.UserStatus; + + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserListResponseDTO { + + private Long id; + private UserStatus userStatus; + private String oauthId; + private String provider; + private String nickname; + private String name; + private Integer age; + private String sex; + private String email; + private String phoneNumber; + private Boolean isAdmin; + private LocalDateTime latestLoginAt; + private LocalDateTime createdAt; + private LocalDateTime deletedAt; +} diff --git a/src/main/java/solitour_backend/solitour/admin/dto/UserListWithPage.java b/src/main/java/solitour_backend/solitour/admin/dto/UserListWithPage.java new file mode 100644 index 0000000..b62e6c6 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/dto/UserListWithPage.java @@ -0,0 +1,19 @@ +package solitour_backend.solitour.admin.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Setter +@Builder +public class UserListWithPage { + + private Long count; + private List users; +} diff --git a/src/main/java/solitour_backend/solitour/admin/dto/request/AnswerRegisterRequest.java b/src/main/java/solitour_backend/solitour/admin/dto/request/AnswerRegisterRequest.java new file mode 100644 index 0000000..ca91aec --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/dto/request/AnswerRegisterRequest.java @@ -0,0 +1,18 @@ +package solitour_backend.solitour.admin.dto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class AnswerRegisterRequest { + + @NotNull + @Min(1) + private String content; + + @NotNull + private Long qnaId; +} diff --git a/src/main/java/solitour_backend/solitour/admin/dto/request/NoticeModifyRequest.java b/src/main/java/solitour_backend/solitour/admin/dto/request/NoticeModifyRequest.java new file mode 100644 index 0000000..57c5c61 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/dto/request/NoticeModifyRequest.java @@ -0,0 +1,26 @@ +package solitour_backend.solitour.admin.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class NoticeModifyRequest { + + @NotNull + private Long id; + + @NotBlank + @Size(min = 1, max = 50) + private String title; + + @NotBlank + @Size(min = 1, max = 1000) + private String content; + + @NotNull + private String category; +} diff --git a/src/main/java/solitour_backend/solitour/admin/dto/request/NoticeRegisterRequest.java b/src/main/java/solitour_backend/solitour/admin/dto/request/NoticeRegisterRequest.java new file mode 100644 index 0000000..0b9a43e --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/dto/request/NoticeRegisterRequest.java @@ -0,0 +1,23 @@ +package solitour_backend.solitour.admin.dto.request; + + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class NoticeRegisterRequest { + @NotBlank + @Size(min = 1, max = 50) + private String title; + + @NotBlank + @Size(min = 1, max = 1000) + private String content; + + @NotNull + private String category; +} diff --git a/src/main/java/solitour_backend/solitour/admin/dto/request/QnARegisterRequest.java b/src/main/java/solitour_backend/solitour/admin/dto/request/QnARegisterRequest.java new file mode 100644 index 0000000..1e5ff00 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/dto/request/QnARegisterRequest.java @@ -0,0 +1,22 @@ +package solitour_backend.solitour.admin.dto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class QnARegisterRequest { + + @NotNull + @Min(1) + private String title; + + @NotNull + @Min(1) + private String content; + + @NotNull + private String categoryName; +} diff --git a/src/main/java/solitour_backend/solitour/admin/dto/request/QuestionRegisterRequest.java b/src/main/java/solitour_backend/solitour/admin/dto/request/QuestionRegisterRequest.java new file mode 100644 index 0000000..f5f2bcb --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/dto/request/QuestionRegisterRequest.java @@ -0,0 +1,18 @@ +package solitour_backend.solitour.admin.dto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class QuestionRegisterRequest { + + @NotNull + @Min(1) + private String content; + + @NotNull + private Long qnaId; +} diff --git a/src/main/java/solitour_backend/solitour/admin/dto/response/QnaListResponseDto.java b/src/main/java/solitour_backend/solitour/admin/dto/response/QnaListResponseDto.java new file mode 100644 index 0000000..7851f6e --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/dto/response/QnaListResponseDto.java @@ -0,0 +1,23 @@ +package solitour_backend.solitour.admin.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import solitour_backend.solitour.admin.entity.QnAMessage; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Builder +@AllArgsConstructor +public class QnaListResponseDto { + private Long id; + private String title; + private LocalDateTime createdAt; + private String status; + private LocalDateTime updatedAt; + private String categoryName; + private Long userId; + private String userNickname; +} diff --git a/src/main/java/solitour_backend/solitour/admin/dto/response/QnaResponseDto.java b/src/main/java/solitour_backend/solitour/admin/dto/response/QnaResponseDto.java new file mode 100644 index 0000000..c4268c2 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/dto/response/QnaResponseDto.java @@ -0,0 +1,24 @@ +package solitour_backend.solitour.admin.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import solitour_backend.solitour.admin.entity.QnAMessage; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Builder +@AllArgsConstructor +public class QnaResponseDto { + private Long id; + private String title; + private LocalDateTime createdAt; + private String status; + private LocalDateTime updatedAt; + private String categoryName; + private Long userId; + private String userNickname; + private List qnaMessages; +} diff --git a/src/main/java/solitour_backend/solitour/admin/entity/Banner.java b/src/main/java/solitour_backend/solitour/admin/entity/Banner.java new file mode 100644 index 0000000..62fed97 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/entity/Banner.java @@ -0,0 +1,28 @@ +package solitour_backend.solitour.admin.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Table(name = "banner") +public class Banner { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable = false) + private String name; + @Column(name = "url", nullable = false) + private String url; +} diff --git a/src/main/java/solitour_backend/solitour/admin/entity/Notice.java b/src/main/java/solitour_backend/solitour/admin/entity/Notice.java new file mode 100644 index 0000000..ef8d2c2 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/entity/Notice.java @@ -0,0 +1,40 @@ +package solitour_backend.solitour.admin.entity; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@Table(name = "notice") +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EntityListeners(AuditingEntityListener.class) +public class Notice { + @Id + @Column(name = "notice_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "notice_title") + private String title; + + @Column(name = "notice_content") + private String content; + + @CreatedDate + @Column(name = "notice_created_at") + private LocalDateTime createdAt; + + @Column(name = "notice_category_name") + private String categoryName; + + @Builder.Default + @Column(name = "notice_is_deleted") + private Boolean isDeleted = false; +} diff --git a/src/main/java/solitour_backend/solitour/admin/entity/QnA.java b/src/main/java/solitour_backend/solitour/admin/entity/QnA.java new file mode 100644 index 0000000..f9ee798 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/entity/QnA.java @@ -0,0 +1,52 @@ +package solitour_backend.solitour.admin.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "qna") +@Setter +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class QnA { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "qna_id") + private Long id; + + @Column(name = "qna_title") + private String title; + + @CreatedDate + @Column(name = "qna_created_at") + private LocalDateTime createdAt; + + @Column(name = "qna_status") + private String status; + + @LastModifiedDate + @Column(name = "qna_updated_at") + private LocalDateTime updatedAt; + + @Column(name = "qna_category_name") + private String categoryName; + + @Column(name = "user_id") + private Long userId; + + @Builder.Default + @OneToMany(mappedBy = "qna", fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @JsonManagedReference + private List qnaMessages = new ArrayList<>(); +} diff --git a/src/main/java/solitour_backend/solitour/admin/entity/QnAMessage.java b/src/main/java/solitour_backend/solitour/admin/entity/QnAMessage.java new file mode 100644 index 0000000..0e0f715 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/entity/QnAMessage.java @@ -0,0 +1,38 @@ +package solitour_backend.solitour.admin.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "qna_message") +@Setter +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class QnAMessage { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "qna_id") + @JsonBackReference + private QnA qna; + + @CreatedDate + @Column(name = "qna_message_created_at") + private LocalDateTime createdAt; + + @Column(name = "qna_message_user_id") + private Long userId; + + @Column(name = "qna_message_content") + private String content; +} diff --git a/src/main/java/solitour_backend/solitour/admin/repository/AdminRepository.java b/src/main/java/solitour_backend/solitour/admin/repository/AdminRepository.java new file mode 100644 index 0000000..cd65522 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/repository/AdminRepository.java @@ -0,0 +1,12 @@ +package solitour_backend.solitour.admin.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import solitour_backend.solitour.user.entity.User; + +public interface AdminRepository extends JpaRepository { + + Page findAllByNicknameContainingIgnoreCase(String nickname, Pageable pageable); + +} diff --git a/src/main/java/solitour_backend/solitour/admin/repository/AnswerRepository.java b/src/main/java/solitour_backend/solitour/admin/repository/AnswerRepository.java new file mode 100644 index 0000000..b5da452 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/repository/AnswerRepository.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.admin.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import solitour_backend.solitour.admin.entity.QnAMessage; + +public interface AnswerRepository extends JpaRepository { +} diff --git a/src/main/java/solitour_backend/solitour/admin/repository/BannerRepository.java b/src/main/java/solitour_backend/solitour/admin/repository/BannerRepository.java new file mode 100644 index 0000000..acc5784 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/repository/BannerRepository.java @@ -0,0 +1,11 @@ +package solitour_backend.solitour.admin.repository; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import solitour_backend.solitour.admin.entity.Banner; + +public interface BannerRepository extends JpaRepository { + + + List findAllByOrderById(); +} diff --git a/src/main/java/solitour_backend/solitour/admin/repository/NoticeRepository.java b/src/main/java/solitour_backend/solitour/admin/repository/NoticeRepository.java new file mode 100644 index 0000000..04189d9 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/repository/NoticeRepository.java @@ -0,0 +1,17 @@ +package solitour_backend.solitour.admin.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import solitour_backend.solitour.admin.entity.Notice; + +import java.util.Optional; + +@Repository +public interface NoticeRepository extends JpaRepository { + + Page findAllByIsDeletedFalse(Pageable pageable); + + Optional findByIdAndIsDeletedFalse(Long id); +} diff --git a/src/main/java/solitour_backend/solitour/admin/repository/QnARepository.java b/src/main/java/solitour_backend/solitour/admin/repository/QnARepository.java new file mode 100644 index 0000000..9b2a525 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/repository/QnARepository.java @@ -0,0 +1,38 @@ +package solitour_backend.solitour.admin.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import solitour_backend.solitour.admin.dto.response.QnaListResponseDto; +import solitour_backend.solitour.admin.entity.QnA; + +public interface QnARepository extends JpaRepository { + + @Query("SELECT new solitour_backend.solitour.admin.dto.response.QnaListResponseDto(q.id, q.title, q.createdAt, q.status, q.updatedAt, q.categoryName, u.id, u.nickname) " + + "FROM QnA q JOIN User u ON q.userId = u.id WHERE q.userId = :userId ORDER BY q.updatedAt ASC") + Page findByUserId(Long userId, Pageable pageable); + + // 상태에 관계없이 모든 QnA 항목 검색 (updatedAt 기준 오름차순) + @Query("SELECT new solitour_backend.solitour.admin.dto.response.QnaListResponseDto(q.id, q.title, q.createdAt, q.status, q.updatedAt, q.categoryName, u.id, u.nickname) " + + "FROM QnA q JOIN User u ON q.userId = u.id ORDER BY q.updatedAt ASC") + Page findAllByOrderByUpdatedAtAsc(Pageable pageable); + + // 상태를 기준으로 QnA 항목 검색 (updatedAt 기준 오름차순) + @Query("SELECT new solitour_backend.solitour.admin.dto.response.QnaListResponseDto(q.id, q.title, q.createdAt, q.status, q.updatedAt, q.categoryName, u.id, u.nickname) " + + "FROM QnA q JOIN User u ON q.userId = u.id WHERE q.status = :status ORDER BY q.updatedAt ASC") + Page findByStatusOrderByUpdatedAtAsc(@Param("status") String status, Pageable pageable); + + // 상태와 무관하게 키워드와 유저 닉네임을 기준으로 QnA 항목 검색 (updatedAt 기준 오름차순) + @Query("SELECT new solitour_backend.solitour.admin.dto.response.QnaListResponseDto(q.id, q.title, q.createdAt, q.status, q.updatedAt, q.categoryName, u.id, u.nickname) " + + "FROM QnA q JOIN User u ON q.userId = u.id WHERE (q.title LIKE %:keyword% OR q.categoryName LIKE %:keyword% OR u.nickname LIKE %:keyword%) " + + "ORDER BY q.updatedAt ASC") + Page findByKeyword(@Param("keyword") String keyword, Pageable pageable); + + // 상태와 키워드, 유저 닉네임을 기준으로 QnA 항목 검색 (updatedAt 기준 오름차순) + @Query("SELECT new solitour_backend.solitour.admin.dto.response.QnaListResponseDto(q.id, q.title, q.createdAt, q.status, q.updatedAt, q.categoryName, u.id, u.nickname) " + + "FROM QnA q JOIN User u ON q.userId = u.id WHERE q.status = :status AND (q.title LIKE %:keyword% OR q.categoryName LIKE %:keyword% OR u.nickname LIKE %:keyword%) " + + "ORDER BY q.updatedAt ASC") + Page findByStatusAndKeyword(@Param("status") String status, @Param("keyword") String keyword, Pageable pageable); +} diff --git a/src/main/java/solitour_backend/solitour/admin/service/AdminService.java b/src/main/java/solitour_backend/solitour/admin/service/AdminService.java new file mode 100644 index 0000000..6c0f2bd --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/service/AdminService.java @@ -0,0 +1,52 @@ +package solitour_backend.solitour.admin.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import solitour_backend.solitour.admin.dto.UserListResponseDTO; +import solitour_backend.solitour.admin.dto.UserListWithPage; +import solitour_backend.solitour.admin.repository.AdminRepository; +import solitour_backend.solitour.user.entity.User; + +@Service +@Slf4j +@RequiredArgsConstructor +public class AdminService { + + private final AdminRepository adminRepository; + + public UserListWithPage getUserInfoList(String nickname, int page) { + Pageable pageable = PageRequest.of(page - 1, 10); + UserListWithPage reponseUserListWithPage = new UserListWithPage(); + Page users; + if (StringUtils.hasText(nickname)) { + users = adminRepository.findAllByNicknameContainingIgnoreCase(nickname, pageable); + } else { + users = adminRepository.findAll(pageable); + } + List userListResponse = users.map(user -> new UserListResponseDTO( + user.getId(), + user.getUserStatus(), + user.getOauthId(), + user.getProvider(), + user.getNickname(), + user.getName(), + user.getAge(), + user.getSex(), + user.getEmail(), + user.getPhoneNumber(), + user.getIsAdmin(), + user.getLatestLoginAt(), + user.getCreatedAt(), + user.getDeletedAt() + )).toList(); + reponseUserListWithPage.setUsers(userListResponse); + reponseUserListWithPage.setCount(adminRepository.count()); + return reponseUserListWithPage; + } +} diff --git a/src/main/java/solitour_backend/solitour/admin/service/BannerService.java b/src/main/java/solitour_backend/solitour/admin/service/BannerService.java new file mode 100644 index 0000000..b9c77da --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/service/BannerService.java @@ -0,0 +1,86 @@ +package solitour_backend.solitour.admin.service; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import java.io.IOException; +import java.util.Date; +import java.util.List; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; +import solitour_backend.solitour.admin.entity.Banner; +import solitour_backend.solitour.admin.repository.BannerRepository; +import solitour_backend.solitour.util.TimeUtil; + +@Service +@Slf4j +@RequiredArgsConstructor +public class BannerService { + + private final AmazonS3Client amazonS3Client; + private final BannerRepository bannerRepository; + private final TimeUtil timeUtil; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + public ResponseEntity getBannerList() { + List banners = bannerRepository.findAllByOrderById(); + HttpHeaders headers = new HttpHeaders(); + headers.setCacheControl("max-age=" + timeUtil.getSecondsUntilMidnightInKST()); + return new ResponseEntity<>(banners, HttpStatus.OK); + } + + public ResponseEntity deleteBanner(Long id) { + Banner banner = bannerRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Banner not found")); + + try { + amazonS3Client.deleteObject(bucket, banner.getName()); + } catch (Exception e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to delete image from S3"); + } + + bannerRepository.deleteById(id); + // Step 4: Return a response + return new ResponseEntity<>("Banner deleted successfully", HttpStatus.OK); + } + + public Banner createBanner(MultipartFile multipartFile, String dirName) { + String fileName = dirName + "/" + new Date().toString(); + try { + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(multipartFile.getSize()); + metadata.setContentType(multipartFile.getContentType()); + + amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, multipartFile.getInputStream(), metadata)); + + } catch (IOException e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드에 실패했습니다."); + } + String imageUrl = amazonS3Client.getUrl(bucket, fileName).toString(); + Banner banner = bannerRepository.save(Banner.builder().name(fileName).url(imageUrl).build()); + return banner; + } + + private String createFileName(String fileName) { + return UUID.randomUUID().toString().concat(getFileExtension(fileName)); + } + + private String getFileExtension(String fileName) { + try { + return fileName.substring(fileName.lastIndexOf(".")); + } catch (StringIndexOutOfBoundsException se) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "잘못된 형식의 파일 입니다."); + } + } + +} diff --git a/src/main/java/solitour_backend/solitour/admin/service/NoticeService.java b/src/main/java/solitour_backend/solitour/admin/service/NoticeService.java new file mode 100644 index 0000000..3c0c4af --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/service/NoticeService.java @@ -0,0 +1,136 @@ +package solitour_backend.solitour.admin.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import solitour_backend.solitour.admin.dto.request.NoticeModifyRequest; +import solitour_backend.solitour.admin.dto.request.NoticeRegisterRequest; +import solitour_backend.solitour.admin.entity.Notice; +import solitour_backend.solitour.admin.repository.NoticeRepository; +import solitour_backend.solitour.user.entity.User; +import solitour_backend.solitour.user.repository.UserRepository; + +@Service +@RequiredArgsConstructor +public class NoticeService { + + private final NoticeRepository noticeRepository; + private final UserRepository userRepository; + + // 공지사항 생성 + public Long createNotice(NoticeRegisterRequest noticeRegisterRequest, Long userId) { + User user = userRepository.findByUserId(userId); + + if (user == null) { + throw new RuntimeException("사용자를 찾을 수 없습니다."); + } + + if (!user.getIsAdmin()) { + throw new RuntimeException("관리자 권한이 필요합니다."); + } + + Notice notice = noticeRepository.save(Notice.builder() + .title(noticeRegisterRequest.getTitle()) + .content(noticeRegisterRequest.getContent()) + .categoryName(noticeRegisterRequest.getCategory()) + .build()); + + return notice.getId(); + } + + // 공지사항 목록 조회 + public Page getNotices(int page) { + Pageable pageable = PageRequest.of(page, 10, Sort.by(Sort.Order.desc("id"))); + return noticeRepository.findAllByIsDeletedFalse(pageable); + } + + // 공지사항 목록 조회 + public Page getNoticesForAdmin(int page, Long userId) { + Pageable pageable = PageRequest.of(page, 10, Sort.by(Sort.Order.desc("id"))); + + User user = userRepository.findByUserId(userId); + + if (user == null) { + throw new RuntimeException("사용자를 찾을 수 없습니다."); + } + + if (!user.getIsAdmin()) { + throw new RuntimeException("관리자 권한이 필요합니다."); + } + + return noticeRepository.findAll(pageable); + } + + // 공지사항 싱세 조회 + public Notice getNotice(Long id) { + return noticeRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new RuntimeException("공지사항을 찾을 수 없습니다.")); + } + + // 관리자를 위한 공지사항 조회, 관리자는 삭제된 공지사항도 볼 수 있음 + public Notice getNoticeForAdmin(Long userId, Long id) { + + User user = userRepository.findByUserId(userId); + + if (user == null) { + throw new RuntimeException("사용자를 찾을 수 없습니다."); + } + + if (!user.getIsAdmin()) { + throw new RuntimeException("관리자 권한이 필요합니다."); + } + + return noticeRepository.findById(id) + .orElseThrow(() -> new RuntimeException("공지사항을 찾을 수 없습니다.")); + } + + // 공지사항 수정 + @Transactional + public boolean updateNotice(Long userId, NoticeModifyRequest noticeModifyRequest) { + User user = userRepository.findByUserId(userId); + + if (user == null) { + throw new RuntimeException("사용자를 찾을 수 없습니다."); + } + + if (!user.getIsAdmin()) { + throw new RuntimeException("관리자 권한이 필요합니다."); + } + + Notice notice = noticeRepository.findByIdAndIsDeletedFalse(noticeModifyRequest.getId()) + .orElseThrow(() -> new RuntimeException("공지사항을 찾을 수 없습니다.")); + + notice.setTitle(noticeModifyRequest.getTitle()); + notice.setContent(noticeModifyRequest.getContent()); + notice.setCategoryName(noticeModifyRequest.getCategory()); + + noticeRepository.save(notice); + return true; + } + + // 공지사항 삭제 (soft delete) + @Transactional + public boolean deleteNotice(Long userId, Long id) { + User user = userRepository.findByUserId(userId); + + if (user == null) { + throw new RuntimeException("사용자를 찾을 수 없습니다."); + } + + if (!user.getIsAdmin()) { + throw new RuntimeException("관리자 권한이 필요합니다."); + } + + Notice notice = noticeRepository.findByIdAndIsDeletedFalse(id) + .orElseThrow(() -> new RuntimeException("공지사항을 찾을 수 없습니다.")); + + notice.setIsDeleted(true); + noticeRepository.save(notice); + return true; + } +} + diff --git a/src/main/java/solitour_backend/solitour/admin/service/QnAService.java b/src/main/java/solitour_backend/solitour/admin/service/QnAService.java new file mode 100644 index 0000000..e42d028 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/admin/service/QnAService.java @@ -0,0 +1,168 @@ +package solitour_backend.solitour.admin.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import solitour_backend.solitour.admin.dto.request.AnswerRegisterRequest; +import solitour_backend.solitour.admin.dto.request.QnARegisterRequest; +import solitour_backend.solitour.admin.dto.request.QuestionRegisterRequest; +import solitour_backend.solitour.admin.dto.response.QnaListResponseDto; +import solitour_backend.solitour.admin.dto.response.QnaResponseDto; +import solitour_backend.solitour.admin.entity.QnA; +import solitour_backend.solitour.admin.entity.QnAMessage; +import solitour_backend.solitour.admin.repository.AnswerRepository; +import solitour_backend.solitour.admin.repository.QnARepository; +import solitour_backend.solitour.user.entity.User; +import solitour_backend.solitour.user.repository.UserRepository; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class QnAService { + + private final QnARepository qnaRepository; + private final AnswerRepository answerRepository; + private final UserRepository userRepository; + + public Long createQnA(QnARegisterRequest qnARegisterRequest, Long userId) { + +// title이 1글자보다 많은지 +// categoryName이 있는지 +// content가 1글자보다 큰지 + + User user = userRepository.findByUserId(userId); + + QnA _qna = qnaRepository.save(QnA.builder() + .title(qnARegisterRequest.getTitle()) + .status("WAIT") + .userId(userId) + .categoryName(qnARegisterRequest.getCategoryName()) + .build()); + +// _qna가 잘 저장되었는지 + + answerRepository.save(QnAMessage.builder() + .qna(_qna) + .content(qnARegisterRequest.getContent()) + .userId(userId) + .build()); + + return _qna.getId(); + } + + + public Page getPagedQnAsByUserId(Long userId, int page, int size) { + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Order.desc("id"))); + return qnaRepository.findByUserId(userId, pageable); + } + + @Transactional + public QnaResponseDto getQnAById(Long id, Long userId) { + + QnA qna = qnaRepository.findById(id).orElse(null); + if (qna == null) { + // QnA가 존재하지 않는 경우 처리 + return null; + } + + User user = userRepository.findByUserId(userId); + if (qna.getUserId() != userId && !user.getIsAdmin()) { + // 권한이 없는 경우 처리 + } +// qna.getQnaMessages().size(); + QnaResponseDto qnaResponseDto = QnaResponseDto.builder() + .id(qna.getId()) + .title(qna.getTitle()) + .createdAt(qna.getCreatedAt()) + .status(qna.getStatus()) + .updatedAt(qna.getUpdatedAt()) + .categoryName(qna.getCategoryName()) + .userId(qna.getUserId()) + .userNickname(user.getNickname()) + .qnaMessages(qna.getQnaMessages()) + .build(); + return qnaResponseDto; + } + + public void closeQnA(Long id, Long userId) { + QnA qna = qnaRepository.findById(id).orElse(null); + if(qna == null) { +// qna가 없는 경우에도 경고 + } + if (qna.getUserId() != userId) { + // 권한이 없는 경우 처리 +// throw new AccessDeniedException("권한이 없습니다."); + } + qna.setStatus("CLOSED"); + qnaRepository.save(qna); + } + + public Page getQnAsByPageStatusAndKeyword(int page, String status, String keyword, Long userId) { + User user = userRepository.findByUserId(userId); + // 유저가 없거나 관리자가 아니면 밴 + if(!user.getIsAdmin()) { + return null; + } + Pageable pageable = PageRequest.of(page, 10); + Page qnaPage; + + + if ("ALL".equals(status)) { + if (keyword != null && !keyword.isEmpty()) { + return qnaRepository.findByKeyword(keyword, PageRequest.of(page, 10, Sort.by("updatedAt").ascending())); + } else { + return qnaRepository.findAllByOrderByUpdatedAtAsc(PageRequest.of(page, 10, Sort.by("updatedAt").ascending())); + } + } else { + if (keyword != null && !keyword.isEmpty()) { + return qnaRepository.findByStatusAndKeyword(status, keyword, PageRequest.of(page, 10, Sort.by("updatedAt").ascending())); + } else { + return qnaRepository.findByStatusOrderByUpdatedAtAsc(status, PageRequest.of(page, 10, Sort.by("updatedAt").ascending())); + } + } + } + + public QnAMessage createQuestion(QuestionRegisterRequest questionRegisterRequest, Long userid) { + Optional _qna = qnaRepository.findById(questionRegisterRequest.getQnaId()); + if(_qna.isEmpty()) { +// 없을 경우 에러 처리 + return null; + } + QnAMessage qnaMessage = answerRepository.save(QnAMessage.builder() + .content(questionRegisterRequest.getContent()) + .qna(_qna.get()) + .userId(userid) + .build()); +// 새로운 글을 작성했다면 상태를 다시 대기중으로 변경 + _qna.get().setStatus("WAIT"); + qnaRepository.save(_qna.get()); + return qnaMessage; + } + + public QnAMessage createAnswer(AnswerRegisterRequest answerRegisterRequest, Long userId) { + Optional _qna = qnaRepository.findById(answerRegisterRequest.getQnaId()); + if(_qna.isEmpty()) { +// 없을 경우 에러 처리 + return null; + } + User user = userRepository.findByUserId(userId); + if(!user.getIsAdmin()) { + return null; + } + + QnAMessage qnaMessage = answerRepository.save(QnAMessage.builder() + .content(answerRegisterRequest.getContent()) + .qna(_qna.get()) + .userId(userId) + .build()); +// 답변을 달았으니 변경 + _qna.get().setStatus("ANSWER"); + qnaRepository.save(_qna.get()); + return qnaMessage; + } +} \ No newline at end of file diff --git a/src/main/java/solitour_backend/solitour/auth/config/AuthConfiguration.java b/src/main/java/solitour_backend/solitour/auth/config/AuthConfiguration.java new file mode 100644 index 0000000..f522392 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/config/AuthConfiguration.java @@ -0,0 +1,55 @@ +package solitour_backend.solitour.auth.config; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import solitour_backend.solitour.auth.entity.TokenRepository; +import solitour_backend.solitour.auth.support.JwtTokenProvider; + +@RequiredArgsConstructor +@Configuration +public class AuthConfiguration implements WebMvcConfigurer { + + private final JwtTokenProvider jwtTokenProvider; + private final TokenRepository tokenRepository; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new AuthInterceptor(jwtTokenProvider)) + .addPathPatterns("/api/**"); + + registry.addInterceptor(new RefreshTokenAuthInterceptor(jwtTokenProvider, tokenRepository)) + .addPathPatterns("/api/auth/oauth2/token/refresh"); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/*") + .addResourceLocations("classpath:/static/"); + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new TokenResolver(jwtTokenProvider)); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + final String[] ALLOWED_URLS = { + "http://localhost:3000", + "https://solitour.ssssksss.xyz", + "https://solitour-admin.ssssksss.xyz" + }; + + registry.addMapping("/**") + .allowedOrigins(ALLOWED_URLS) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/config/AuthInterceptor.java b/src/main/java/solitour_backend/solitour/auth/config/AuthInterceptor.java new file mode 100644 index 0000000..9b30bc5 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/config/AuthInterceptor.java @@ -0,0 +1,59 @@ +package solitour_backend.solitour.auth.config; + + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import solitour_backend.solitour.auth.exception.TokenNotExistsException; +import solitour_backend.solitour.auth.exception.TokenNotValidException; +import solitour_backend.solitour.auth.support.CookieExtractor; +import solitour_backend.solitour.auth.support.JwtTokenProvider; + +@RequiredArgsConstructor +public class AuthInterceptor implements HandlerInterceptor { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws IOException { + if (CorsUtils.isPreFlightRequest(request)) { + return true; + } + + Optional authenticated = parseAnnotation((HandlerMethod) handler, + Authenticated.class); + if (authenticated.isPresent()) { + try { + validateToken(request); + } catch (TokenNotValidException e) { + throw new TokenNotExistsException("토큰이 존재하지 않습니다."); + } + } + return true; + } + + private Optional parseAnnotation(HandlerMethod handler, Class clazz) { + T methodAnnotation = handler.getMethodAnnotation(clazz); + + if (methodAnnotation == null) { + methodAnnotation = handler.getBeanType().getAnnotation(clazz); + } + + return Optional.ofNullable(methodAnnotation); + } + + private void validateToken(HttpServletRequest request) { + String token = CookieExtractor.findToken("access_token", request.getCookies()); + if (jwtTokenProvider.validateTokenNotUsable(token)) { + throw new TokenNotValidException("토큰이 유효하지 않습니다."); + } + } + +} diff --git a/src/main/java/solitour_backend/solitour/auth/config/Authenticated.java b/src/main/java/solitour_backend/solitour/auth/config/Authenticated.java new file mode 100644 index 0000000..35961fc --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/config/Authenticated.java @@ -0,0 +1,12 @@ +package solitour_backend.solitour.auth.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Authenticated { + +} diff --git a/src/main/java/solitour_backend/solitour/auth/config/AuthenticationPrincipal.java b/src/main/java/solitour_backend/solitour/auth/config/AuthenticationPrincipal.java new file mode 100644 index 0000000..1f7ee96 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/config/AuthenticationPrincipal.java @@ -0,0 +1,12 @@ +package solitour_backend.solitour.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 AuthenticationPrincipal { + +} diff --git a/src/main/java/solitour_backend/solitour/auth/config/AuthenticationRefreshPrincipal.java b/src/main/java/solitour_backend/solitour/auth/config/AuthenticationRefreshPrincipal.java new file mode 100644 index 0000000..7b53d4f --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/config/AuthenticationRefreshPrincipal.java @@ -0,0 +1,12 @@ +package solitour_backend.solitour.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 AuthenticationRefreshPrincipal { + +} diff --git a/src/main/java/solitour_backend/solitour/auth/config/RefreshTokenAuthInterceptor.java b/src/main/java/solitour_backend/solitour/auth/config/RefreshTokenAuthInterceptor.java new file mode 100644 index 0000000..c37547e --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/config/RefreshTokenAuthInterceptor.java @@ -0,0 +1,37 @@ +package solitour_backend.solitour.auth.config; + + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.servlet.HandlerInterceptor; +import solitour_backend.solitour.auth.entity.Token; +import solitour_backend.solitour.auth.entity.TokenRepository; +import solitour_backend.solitour.auth.support.CookieExtractor; +import solitour_backend.solitour.auth.support.JwtTokenProvider; + +@RequiredArgsConstructor +public class RefreshTokenAuthInterceptor implements HandlerInterceptor { + + private final JwtTokenProvider jwtTokenProvider; + private final TokenRepository tokenRepository; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String refreshToken = CookieExtractor.findToken("refresh_token", request.getCookies()); + + if (jwtTokenProvider.validateTokenNotUsable(refreshToken)) { + throw new RuntimeException("토큰이 유효하지 않습니다."); + } + + Long userId = jwtTokenProvider.getPayload(refreshToken); + Token token = tokenRepository.findByUserId(userId) + .orElseThrow(() -> new RuntimeException("토큰이 존재하지 않습니다.")); + + if (token.isDifferentRefreshToken(refreshToken)) { + throw new RuntimeException("토큰이 일치하지 않습니다."); + } + return true; + } + +} diff --git a/src/main/java/solitour_backend/solitour/auth/config/TokenResolver.java b/src/main/java/solitour_backend/solitour/auth/config/TokenResolver.java new file mode 100644 index 0000000..440b277 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/config/TokenResolver.java @@ -0,0 +1,42 @@ +package solitour_backend.solitour.auth.config; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import solitour_backend.solitour.auth.support.CookieExtractor; +import solitour_backend.solitour.auth.support.JwtTokenProvider; + +@RequiredArgsConstructor +public class TokenResolver implements HandlerMethodArgumentResolver { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + if (parameter.hasParameterAnnotation(AuthenticationPrincipal.class)) { + return true; + } else { + return parameter.hasParameterAnnotation(AuthenticationRefreshPrincipal.class); + } + } + + @Override + public Long resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + String token = ""; + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + + if (parameter.hasParameterAnnotation(AuthenticationPrincipal.class)) { + token = CookieExtractor.findToken("access_token", request.getCookies()); + } else { + token = CookieExtractor.findToken("refresh_token", request.getCookies()); + } + + return jwtTokenProvider.getPayload(token); + } + +} diff --git a/src/main/java/solitour_backend/solitour/auth/controller/OauthController.java b/src/main/java/solitour_backend/solitour/auth/controller/OauthController.java new file mode 100644 index 0000000..607502a --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/controller/OauthController.java @@ -0,0 +1,141 @@ +package solitour_backend.solitour.auth.controller; + + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import solitour_backend.solitour.auth.config.Authenticated; +import solitour_backend.solitour.auth.config.AuthenticationPrincipal; +import solitour_backend.solitour.auth.config.AuthenticationRefreshPrincipal; +import solitour_backend.solitour.auth.entity.Token; +import solitour_backend.solitour.auth.entity.TokenRepository; +import solitour_backend.solitour.auth.exception.TokenNotExistsException; +import solitour_backend.solitour.auth.exception.UnsupportedLoginTypeException; +import solitour_backend.solitour.auth.exception.UserRevokeErrorException; +import solitour_backend.solitour.auth.service.OauthService; +import solitour_backend.solitour.auth.service.dto.response.AccessTokenResponse; +import solitour_backend.solitour.auth.service.dto.response.LoginResponse; +import solitour_backend.solitour.auth.service.dto.response.OauthLinkResponse; +import solitour_backend.solitour.auth.support.google.GoogleConnector; +import solitour_backend.solitour.auth.support.kakao.KakaoConnector; +import solitour_backend.solitour.auth.support.kakao.dto.request.CreateUserInfoRequest; +import solitour_backend.solitour.auth.support.naver.NaverConnector; +import solitour_backend.solitour.user.user_status.UserStatus; + + +@RequiredArgsConstructor +@RequestMapping("/api/auth/oauth2") +@RestController +public class OauthController { + + private final OauthService oauthService; + private final KakaoConnector kakaoConnector; + private final GoogleConnector googleConnector; + private final NaverConnector naverConnector; + private final TokenRepository tokenRepository; + + @GetMapping(value = "/login", params = {"type", "redirectUrl"}) + public ResponseEntity access(@RequestParam String type, @RequestParam String redirectUrl) { + OauthLinkResponse response = oauthService.generateAuthUrl(type, redirectUrl); + return ResponseEntity.ok(response); + } + + @PostMapping(value = "/login/kakao", params = {"code", "redirectUrl"}) + public ResponseEntity kakaoLogin(HttpServletResponse response, + @RequestParam String code, @RequestParam String redirectUrl, + @RequestBody CreateUserInfoRequest createUserInfoRequest) { + LoginResponse loginResponse = oauthService.requestkakaoAccessToken(code, redirectUrl, createUserInfoRequest); + + String accessCookieHeader = setCookieHeader(loginResponse.getAccessToken()); + String refreshCookieHeader = setCookieHeader(loginResponse.getRefreshToken()); + + response.addHeader("Set-Cookie", accessCookieHeader); + response.addHeader("Set-Cookie", refreshCookieHeader); + + return ResponseEntity.ok(loginResponse.getLoginStatus()); + } + + + @GetMapping(value = "/login", params = {"type", "code", "redirectUrl"}) + public ResponseEntity login(HttpServletResponse response, @RequestParam String type, + @RequestParam String code, @RequestParam String redirectUrl) { + LoginResponse loginResponse = oauthService.requestAccessToken(type, code, redirectUrl); + + String accessCookieHeader = setCookieHeader(loginResponse.getAccessToken()); + String refreshCookieHeader = setCookieHeader(loginResponse.getRefreshToken()); + + response.addHeader("Set-Cookie", accessCookieHeader); + response.addHeader("Set-Cookie", refreshCookieHeader); + + return ResponseEntity.ok(loginResponse.getLoginStatus()); + } + + @PostMapping("/logout") + public ResponseEntity logout(HttpServletResponse response, @AuthenticationPrincipal Long memberId) { + oauthService.logout(response, memberId); + + return ResponseEntity.ok().build(); + } + + @PostMapping("/token/refresh") + public ResponseEntity reissueAccessToken(HttpServletResponse response, + @AuthenticationRefreshPrincipal Long memberId) { + AccessTokenResponse accessToken = oauthService.reissueAccessToken(memberId); + + String accessCookieHeader = setCookieHeader(accessToken.getAccessToken()); + response.addHeader("Set-Cookie", accessCookieHeader); + + return ResponseEntity.ok().build(); + } + + @Authenticated + @DeleteMapping() + public ResponseEntity deleteUser(HttpServletResponse response, @AuthenticationPrincipal Long id, + @RequestParam String type) { + Token token = tokenRepository.findByUserId(id) + .orElseThrow(() -> new TokenNotExistsException("토큰이 존재하지 않습니다")); + String oauthRefreshToken = getOauthAccessToken(type, token.getOauthToken()); + + try { + oauthService.revokeToken(type, oauthRefreshToken); + + oauthService.logout(response, id); + oauthService.deleteUser(id); + } catch (Exception e) { + throw new UserRevokeErrorException("회원 탈퇴 중 오류가 발생했습니다"); + } + + return ResponseEntity.noContent().build(); + } + + private String setCookieHeader(Cookie cookie) { + return String.format("%s=%s; Path=%s; Max-Age=%d;Secure; HttpOnly; SameSite=Lax", + cookie.getName(), cookie.getValue(), cookie.getPath(), cookie.getMaxAge()); + } + + private String getOauthAccessToken(String type, String refreshToken) { + String token = ""; + switch (type) { + case "kakao" -> { + token = kakaoConnector.refreshToken(refreshToken); + } + case "naver" -> { + token = naverConnector.refreshToken(refreshToken); + } +// case "google" -> { +// token = googleConnector.refreshToken(refreshToken); +// } + default -> throw new UnsupportedLoginTypeException("지원하지 않는 로그인 타입입니다"); + } + return token; + } + +} diff --git a/src/main/java/solitour_backend/solitour/auth/entity/Term.java b/src/main/java/solitour_backend/solitour/auth/entity/Term.java new file mode 100644 index 0000000..9397192 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/entity/Term.java @@ -0,0 +1,45 @@ +package solitour_backend.solitour.auth.entity; + +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.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import solitour_backend.solitour.user.entity.User; + +@Getter +@NoArgsConstructor +@Entity +@Builder +@AllArgsConstructor +@Table(name = "term") +public class Term { + + @Id + @Column(name = "term_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column(nullable = false, name = "term_condition_agreement") + private Boolean termCondition; + + @Column(nullable = false, name = "term_privacy_agreement") + private Boolean termPrivacy; + + @Column(name="term_created_at") + private LocalDateTime createdAt; + +} diff --git a/src/main/java/solitour_backend/solitour/auth/entity/TermRepository.java b/src/main/java/solitour_backend/solitour/auth/entity/TermRepository.java new file mode 100644 index 0000000..5dcc6ff --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/entity/TermRepository.java @@ -0,0 +1,15 @@ +package solitour_backend.solitour.auth.entity; + +import java.util.Optional; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; +import solitour_backend.solitour.user.entity.User; + +public interface TermRepository extends Repository { + void save(Term term); + + @Query("SELECT t FROM Term t WHERE t.user = :user") + Optional findByUser(User user); +} diff --git a/src/main/java/solitour_backend/solitour/auth/entity/Token.java b/src/main/java/solitour_backend/solitour/auth/entity/Token.java new file mode 100644 index 0000000..2c8d3c7 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/entity/Token.java @@ -0,0 +1,53 @@ +package solitour_backend.solitour.auth.entity; + +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.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import solitour_backend.solitour.user.entity.User; + +@Getter +@NoArgsConstructor() +@Entity +@Builder +@AllArgsConstructor +@Table(name = "token") +public class Token { + + @Id + @Column(name = "token_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column(nullable = false, name = "refresh_token") + private String refreshToken; + + @Column(nullable = false, name = "oauth_token") + private String oauthToken; + + public Token(User user, String refreshToken) { + this.user = user; + this.refreshToken = refreshToken; + } + + public boolean isDifferentRefreshToken(String refreshToken) { + return !this.refreshToken.equals(refreshToken); + } + + public void updateRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/entity/TokenRepository.java b/src/main/java/solitour_backend/solitour/auth/entity/TokenRepository.java new file mode 100644 index 0000000..d020ddf --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/entity/TokenRepository.java @@ -0,0 +1,18 @@ +package solitour_backend.solitour.auth.entity; + +import java.util.Optional; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; + +public interface TokenRepository extends Repository { + + Token save(Token token); + + Optional findByUserId(Long userId); + + @Modifying + @Query("delete from Token t where t.user.id = :userId") + void deleteByUserId(@Param("userId") Long userId); +} diff --git a/src/main/java/solitour_backend/solitour/auth/exception/RevokeFailException.java b/src/main/java/solitour_backend/solitour/auth/exception/RevokeFailException.java new file mode 100644 index 0000000..6bd23ec --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/exception/RevokeFailException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.auth.exception; + +public class RevokeFailException extends RuntimeException { + public RevokeFailException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/exception/TokenNotExistsException.java b/src/main/java/solitour_backend/solitour/auth/exception/TokenNotExistsException.java new file mode 100644 index 0000000..2b454e1 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/exception/TokenNotExistsException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.auth.exception; + +public class TokenNotExistsException extends RuntimeException { + public TokenNotExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/exception/TokenNotValidException.java b/src/main/java/solitour_backend/solitour/auth/exception/TokenNotValidException.java new file mode 100644 index 0000000..7afc9d5 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/exception/TokenNotValidException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.auth.exception; + +public class TokenNotValidException extends RuntimeException { + public TokenNotValidException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/exception/UnsupportedLoginTypeException.java b/src/main/java/solitour_backend/solitour/auth/exception/UnsupportedLoginTypeException.java new file mode 100644 index 0000000..4b10d35 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/exception/UnsupportedLoginTypeException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.auth.exception; + +public class UnsupportedLoginTypeException extends RuntimeException { + public UnsupportedLoginTypeException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/exception/UserRevokeErrorException.java b/src/main/java/solitour_backend/solitour/auth/exception/UserRevokeErrorException.java new file mode 100644 index 0000000..d69a887 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/exception/UserRevokeErrorException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.auth.exception; + +public class UserRevokeErrorException extends RuntimeException { + public UserRevokeErrorException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/service/OauthService.java b/src/main/java/solitour_backend/solitour/auth/service/OauthService.java new file mode 100644 index 0000000..55d6cb3 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/service/OauthService.java @@ -0,0 +1,398 @@ +package solitour_backend.solitour.auth.service; + + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatusCode; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import solitour_backend.solitour.auth.entity.Token; +import solitour_backend.solitour.auth.entity.TokenRepository; +import solitour_backend.solitour.auth.exception.RevokeFailException; +import solitour_backend.solitour.auth.exception.UnsupportedLoginTypeException; +import solitour_backend.solitour.auth.service.dto.response.AccessTokenResponse; +import solitour_backend.solitour.auth.service.dto.response.LoginResponse; +import solitour_backend.solitour.auth.service.dto.response.OauthLinkResponse; +import solitour_backend.solitour.auth.support.JwtTokenProvider; +import solitour_backend.solitour.auth.support.RandomNickName; +import solitour_backend.solitour.auth.support.google.GoogleConnector; +import solitour_backend.solitour.auth.support.google.GoogleProvider; +import solitour_backend.solitour.auth.support.google.dto.GoogleUserResponse; +import solitour_backend.solitour.auth.support.kakao.KakaoConnector; +import solitour_backend.solitour.auth.support.kakao.KakaoProvider; +import solitour_backend.solitour.auth.support.kakao.dto.KakaoTokenAndUserResponse; +import solitour_backend.solitour.auth.support.kakao.dto.KakaoTokenResponse; +import solitour_backend.solitour.auth.support.kakao.dto.KakaoUserResponse; +import solitour_backend.solitour.auth.support.kakao.dto.request.CreateUserInfoRequest; +import solitour_backend.solitour.auth.support.naver.NaverConnector; +import solitour_backend.solitour.auth.support.naver.NaverProvider; +import solitour_backend.solitour.auth.support.naver.dto.NaverTokenAndUserResponse; +import solitour_backend.solitour.auth.support.naver.dto.NaverTokenResponse; +import solitour_backend.solitour.auth.support.naver.dto.NaverUserResponse; +import solitour_backend.solitour.image.s3.S3Uploader; +import solitour_backend.solitour.user.entity.User; +import solitour_backend.solitour.user.exception.BlockedUserException; +import solitour_backend.solitour.user.exception.DeletedUserException; +import solitour_backend.solitour.user.exception.DormantUserException; +import solitour_backend.solitour.user.repository.UserRepository; +import solitour_backend.solitour.user.user_status.UserStatus; +import solitour_backend.solitour.user_image.entity.UserImage; +import solitour_backend.solitour.user_image.entity.UserImageRepository; +import solitour_backend.solitour.user_image.service.UserImageService; + +@RequiredArgsConstructor +@Service +public class OauthService { + + private final TokenService tokenService; + private final UserRepository userRepository; + private final JwtTokenProvider jwtTokenProvider; + private final KakaoConnector kakaoConnector; + private final KakaoProvider kakaoProvider; + private final GoogleConnector googleConnector; + private final GoogleProvider googleProvider; + private final NaverConnector naverConnector; + private final NaverProvider naverProvider; + private final UserImageService userImageService; + private final TokenRepository tokenRepository; + private final UserImageRepository userImageRepository; + private final S3Uploader s3Uploader; + @Value("${user.profile.url.male}") + private String USER_PROFILE_MALE; + @Value("${user.profile.url.female}") + private String USER_PROFILE_FEMALE; + @Value("${user.profile.url.none}") + private String USER_PROFILE_NONE; + + public OauthLinkResponse generateAuthUrl(String type, String redirectUrl) { + String oauthLink = getAuthLink(type, redirectUrl); + return new OauthLinkResponse(oauthLink); + } + + @Transactional + public LoginResponse requestAccessToken(String type, String code, String redirectUrl) { + User user = checkAndSaveUser(type, code, redirectUrl); + user.updateLoginTime(); + final int ACCESS_COOKIE_AGE = (int) TimeUnit.MINUTES.toSeconds(30); + final int REFRESH_COOKIE_AGE = (int) TimeUnit.DAYS.toSeconds(30); + + String token = jwtTokenProvider.createAccessToken(user.getId()); + String refreshToken = jwtTokenProvider.createRefreshToken(user.getId()); + + tokenService.synchronizeRefreshToken(user, refreshToken); + + Cookie accessCookie = createCookie("access_token", token, ACCESS_COOKIE_AGE); + Cookie refreshCookie = createCookie("refresh_token", refreshToken, REFRESH_COOKIE_AGE); + + return new LoginResponse(accessCookie, refreshCookie, user.getUserStatus()); + } + + @Transactional + public LoginResponse requestkakaoAccessToken(String code, String redirectUrl, + CreateUserInfoRequest createUserInfoRequest) { + User user = checkAndSaveKakaoUser(code, redirectUrl, createUserInfoRequest); + user.updateLoginTime(); + final int ACCESS_COOKIE_AGE = (int) TimeUnit.MINUTES.toSeconds(30); + final int REFRESH_COOKIE_AGE = (int) TimeUnit.DAYS.toSeconds(30); + + String token = jwtTokenProvider.createAccessToken(user.getId()); + String refreshToken = jwtTokenProvider.createRefreshToken(user.getId()); + + tokenService.synchronizeRefreshToken(user, refreshToken); + + Cookie accessCookie = createCookie("access_token", token, ACCESS_COOKIE_AGE); + Cookie refreshCookie = createCookie("refresh_token", refreshToken, REFRESH_COOKIE_AGE); + + return new LoginResponse(accessCookie, refreshCookie, user.getUserStatus()); + } + + private Cookie createCookie(String name, String value, int maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setSecure(true); + cookie.setHttpOnly(true); + cookie.setMaxAge(maxAge); + cookie.setPath("/"); + return cookie; + } + + private User checkAndSaveKakaoUser(String code, String redirectUrl, CreateUserInfoRequest createUserInfoRequest) { + KakaoTokenAndUserResponse response = kakaoConnector.requestKakaoUserInfo(code, redirectUrl); + KakaoTokenResponse tokenResponse = response.getKakaoTokenResponse(); + KakaoUserResponse kakaoUserResponse = response.getKakaoUserResponse(); + + String id = kakaoUserResponse.getId().toString(); + User user = userRepository.findByOauthId(id) + .orElseGet(() -> saveActiveKakaoUser(kakaoUserResponse, createUserInfoRequest)); + + checkUserStatus(user); + + Token token = tokenRepository.findByUserId(user.getId()) + .orElseGet(() -> tokenService.saveToken(tokenResponse.getRefreshToken(), user)); + + return user; + } + + private User checkAndSaveUser(String type, String code, String redirectUrl) { + if (Objects.equals(type, "kakao")) { + KakaoTokenAndUserResponse response = kakaoConnector.requestKakaoUserInfo(code, redirectUrl); + KakaoTokenResponse tokenResponse = response.getKakaoTokenResponse(); + KakaoUserResponse kakaoUserResponse = response.getKakaoUserResponse(); + + String id = kakaoUserResponse.getId().toString(); + User user = userRepository.findByOauthId(id) + .orElseGet(() -> saveKakaoUser(kakaoUserResponse)); + + checkUserStatus(user); + + Token token = tokenRepository.findByUserId(user.getId()) + .orElseGet(() -> tokenService.saveToken(tokenResponse.getRefreshToken(), user)); + + return user; + } + if (Objects.equals(type, "naver")) { + NaverTokenAndUserResponse response = naverConnector.requestNaverUserInfo(code); + NaverTokenResponse tokenResponse = response.getNaverTokenResponse(); + NaverUserResponse naverUserResponse = response.getNaverUserResponse(); + + String id = naverUserResponse.getResponse().getId().toString(); + User user = userRepository.findByOauthId(id) + .orElseGet(() -> saveNaverUser(naverUserResponse)); + + checkUserStatus(user); + + Token token = tokenRepository.findByUserId(user.getId()) + .orElseGet(() -> tokenService.saveToken(tokenResponse.getRefreshToken(), user)); + + return user; + } + if (Objects.equals(type, "google")) { + GoogleUserResponse response = googleConnector.requestGoogleUserInfo(code, redirectUrl) + .getBody(); + String id = response.getResourceName(); + return userRepository.findByOauthId(id) + .orElseGet(() -> saveGoogleUser(response)); + } else { + throw new UnsupportedLoginTypeException("지원하지 않는 oauth 로그인입니다."); + } + } + + private User saveNaverUser(NaverUserResponse naverUserResponse) { + String convertedSex = convertSex(naverUserResponse.getResponse().getGender()); + String imageUrl = getDefaultUserImage(convertedSex); + UserImage savedUserImage = userImageService.saveUserImage(imageUrl); + + User user = User.builder() + .userStatus(UserStatus.ACTIVATE) + .oauthId(String.valueOf(naverUserResponse.getResponse().getId())) + .provider("naver") + .isAdmin(false) + .userImage(savedUserImage) + .nickname(RandomNickName.generateRandomNickname()) + .email(naverUserResponse.getResponse().getEmail()) + .name(naverUserResponse.getResponse().getName()) + .age(Integer.parseInt(naverUserResponse.getResponse().getBirthyear())) + .sex(convertedSex) + .createdAt(LocalDateTime.now()) + .build(); + return userRepository.save(user); + } + + private String convertSex(String gender) { + if (gender.equals("M")) { + return "male"; + } else if (gender.equals("F")) { + return "female"; + } else { + return "none"; + } + } + + private void checkUserStatus(User user) { + UserStatus userStatus = user.getUserStatus(); + switch (userStatus) { + case BLOCK -> throw new BlockedUserException("차단된 계정입니다."); + case DELETE -> throw new DeletedUserException("탈퇴한 계정입니다."); + case DORMANT -> throw new DormantUserException("휴면 계정입니다."); + } + } + + private void saveToken(KakaoTokenResponse tokenResponse, User user) { + Token token = Token.builder() + .user(user) + .oauthToken(tokenResponse.getRefreshToken()) + .build(); + + tokenRepository.save(token); + } + + private User saveGoogleUser(GoogleUserResponse response) { + String imageUrl = getGoogleUserImage(response); + UserImage savedUserImage = userImageService.saveUserImage(imageUrl); + + User user = User.builder() + .userStatus(UserStatus.INACTIVATE) + .oauthId(response.getResourceName()) + .provider("google") + .isAdmin(false) + .userImage(savedUserImage) + .nickname(RandomNickName.generateRandomNickname()) + .name(response.getNames().get(0).getDisplayName()) + .age(response.getBirthdays().get(0).getDate().getYear()) + .sex(response.getGenders().get(0).getValue()) + .email(response.getEmailAddresses().get(0).getValue()) + .createdAt(LocalDateTime.now()) + .build(); + return userRepository.save(user); + } + + private String getGoogleUserImage(GoogleUserResponse response) { + String gender = response.getGenders().get(0).getValue(); + if (Objects.equals(gender, "male")) { + return USER_PROFILE_MALE; + } + if (Objects.equals(gender, "female")) { + return USER_PROFILE_FEMALE; + } + return USER_PROFILE_NONE; + } + + private User saveActiveKakaoUser(KakaoUserResponse kakaoUserResponse, CreateUserInfoRequest createUserInfoRequest) { + String imageUrl = getDefaultUserImage(createUserInfoRequest.getSex()); + UserImage savedUserImage = userImageService.saveUserImage(imageUrl); + + User user = User.builder() + .userStatus(UserStatus.PENDING) + .oauthId(String.valueOf(kakaoUserResponse.getId())) + .provider("kakao") + .isAdmin(false) + .userImage(savedUserImage) + .name(createUserInfoRequest.getName()) + .sex(createUserInfoRequest.getSex()) + .nickname(RandomNickName.generateRandomNickname()) + .email(kakaoUserResponse.getKakaoAccount().getEmail()) + .name(createUserInfoRequest.getName()) + .age(Integer.valueOf(createUserInfoRequest.getAge())) + .createdAt(LocalDateTime.now()) + .build(); + return userRepository.save(user); + } + + private User saveKakaoUser(KakaoUserResponse response) { + String imageUrl = getDefaultUserImage(response.getKakaoAccount().getGender()); + UserImage savedUserImage = userImageService.saveUserImage(imageUrl); + + User user = User.builder() + .userStatus(UserStatus.PENDING) + .oauthId(String.valueOf(response.getId())) + .provider("kakao") + .isAdmin(false) + .userImage(savedUserImage) + .nickname(RandomNickName.generateRandomNickname()) + .email(response.getKakaoAccount().getEmail()) + .createdAt(LocalDateTime.now()) + .build(); + return userRepository.save(user); + } + + + private String getDefaultUserImage(String gender) { + if (Objects.equals(gender, "male")) { + return USER_PROFILE_MALE; + } + if (Objects.equals(gender, "female")) { + return USER_PROFILE_FEMALE; + } + return USER_PROFILE_NONE; + } + + private String getAuthLink(String type, String redirectUrl) { + return switch (type) { + case "kakao" -> kakaoProvider.generateAuthUrl(redirectUrl); + case "google" -> googleProvider.generateAuthUrl(redirectUrl); + case "naver" -> naverProvider.generateAuthUrl(redirectUrl); + default -> throw new UnsupportedLoginTypeException("지원하지 않는 oauth 로그인입니다."); + }; + } + + public AccessTokenResponse reissueAccessToken(Long userId) { + boolean isExistMember = userRepository.existsById(userId); + int ACCESS_COOKIE_AGE = (int) TimeUnit.MINUTES.toSeconds(30); + if (!isExistMember) { + throw new RuntimeException("유효하지 않은 토큰입니다."); + } + String accessToken = jwtTokenProvider.createAccessToken(userId); + Cookie accessCookie = createCookie("access_token", accessToken, ACCESS_COOKIE_AGE); + + return new AccessTokenResponse(accessCookie); + } + + @Transactional + public void logout(HttpServletResponse response, Long userId) { + tokenService.deleteByMemberId(userId); + deleteCookie("access_token", "", response); + deleteCookie("refresh_token", "", response); + } + + private void deleteCookie(String name, String value, HttpServletResponse response) { + Cookie cookie = new Cookie(name, value); + cookie.setMaxAge(0); + cookie.setPath("/"); + response.addCookie(cookie); + } + + public void revokeToken(String type, String token) throws IOException { + HttpStatusCode responseCode; + switch (type) { + case "kakao" -> responseCode = kakaoConnector.requestRevoke(token); + case "google" -> responseCode = googleConnector.requestRevoke(token); + case "naver" -> responseCode = naverConnector.requestRevoke(token); + default -> throw new UnsupportedLoginTypeException("지원하지 않는 oauth 로그인입니다."); + } + + if (!responseCode.is2xxSuccessful()) { + throw new RevokeFailException("회원탈퇴에 실패하였습니다."); + } + } + + @Transactional + public void deleteUser(Long userId) { + User user = userRepository.findByUserId(userId); + UserImage userImage = userImageRepository.findById(user.getUserImage().getId()).orElseThrow(); + changeToDefaultProfile(user, userImage); + user.deleteUser(); + } + + private void changeToDefaultProfile(User user, UserImage userImage) { + String defaultImageUrl = getDefaultProfile(user); + deleteUserProfileFromS3(userImage, defaultImageUrl); + } + + private String getDefaultProfile(User user) { + if (user.getSex() != null) { + if (user.getSex().equals("male")) { + return USER_PROFILE_MALE; + } else { + return USER_PROFILE_FEMALE; + } + } + return USER_PROFILE_NONE; + } + + private void deleteUserProfileFromS3(UserImage userImage, String defaultImageUrl) { + String userImageUrl = userImage.getAddress(); + if (userImageUrl.equals(USER_PROFILE_MALE) || userImageUrl.equals(USER_PROFILE_FEMALE) + || userImageUrl.equals( + USER_PROFILE_NONE)) { + return; + } + s3Uploader.deleteImage(userImageUrl); + userImage.changeToDefaultProfile(defaultImageUrl); + } + +} diff --git a/src/main/java/solitour_backend/solitour/auth/service/TokenService.java b/src/main/java/solitour_backend/solitour/auth/service/TokenService.java new file mode 100644 index 0000000..b4c2758 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/service/TokenService.java @@ -0,0 +1,43 @@ +package solitour_backend.solitour.auth.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import solitour_backend.solitour.auth.entity.Token; +import solitour_backend.solitour.auth.entity.TokenRepository; +import solitour_backend.solitour.auth.exception.TokenNotExistsException; +import solitour_backend.solitour.auth.support.kakao.dto.KakaoTokenResponse; +import solitour_backend.solitour.user.entity.User; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class TokenService { + + private final TokenRepository tokenRepository; + + @Transactional + public void synchronizeRefreshToken(User user, String refreshToken) { + Token token = tokenRepository.findByUserId(user.getId()) + .orElseThrow(() -> new TokenNotExistsException("토큰이 존재하지 않습니다")); + + token.updateRefreshToken(refreshToken); + } + + @Transactional + public void deleteByMemberId(Long memberId) { + tokenRepository.deleteByUserId(memberId); + } + + @Transactional + public Token saveToken(String refreshToken, User user) { + Token token = Token.builder() + .user(user) + .oauthToken(refreshToken) + .build(); + + tokenRepository.save(token); + + return token; + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/service/dto/response/AccessTokenResponse.java b/src/main/java/solitour_backend/solitour/auth/service/dto/response/AccessTokenResponse.java new file mode 100644 index 0000000..109a5aa --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/service/dto/response/AccessTokenResponse.java @@ -0,0 +1,12 @@ +package solitour_backend.solitour.auth.service.dto.response; + +import jakarta.servlet.http.Cookie; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class AccessTokenResponse { + + private final Cookie accessToken; +} diff --git a/src/main/java/solitour_backend/solitour/auth/service/dto/response/LoginResponse.java b/src/main/java/solitour_backend/solitour/auth/service/dto/response/LoginResponse.java new file mode 100644 index 0000000..572a9c2 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/service/dto/response/LoginResponse.java @@ -0,0 +1,18 @@ +package solitour_backend.solitour.auth.service.dto.response; + +import jakarta.servlet.http.Cookie; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import solitour_backend.solitour.user.user_status.UserStatus; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor +public class LoginResponse { + + private Cookie accessToken; + private Cookie refreshToken; + private UserStatus loginStatus; +} diff --git a/src/main/java/solitour_backend/solitour/auth/service/dto/response/OauthLinkResponse.java b/src/main/java/solitour_backend/solitour/auth/service/dto/response/OauthLinkResponse.java new file mode 100644 index 0000000..57aee00 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/service/dto/response/OauthLinkResponse.java @@ -0,0 +1,11 @@ +package solitour_backend.solitour.auth.service.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class OauthLinkResponse { + + private String oauthLink; +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/CookieExtractor.java b/src/main/java/solitour_backend/solitour/auth/support/CookieExtractor.java new file mode 100644 index 0000000..912a4df --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/CookieExtractor.java @@ -0,0 +1,20 @@ +package solitour_backend.solitour.auth.support; + +import jakarta.servlet.http.Cookie; + +public class CookieExtractor { + + public static String findToken(String token, Cookie[] cookies) { + String value = null; + if (cookies == null) { + return null; + } + for (Cookie cookie : cookies) { + if (token.equals(cookie.getName())) { + value = cookie.getValue(); + break; + } + } + return value; + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/JwtTokenProvider.java b/src/main/java/solitour_backend/solitour/auth/support/JwtTokenProvider.java new file mode 100644 index 0000000..cbd7973 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/JwtTokenProvider.java @@ -0,0 +1,72 @@ +package solitour_backend.solitour.auth.support; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import java.util.Date; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class JwtTokenProvider { + + private final SecretKey key; + private final long accessTokenValidityInMilliseconds; + private final long refreshTokenValidityInMilliseconds; + + public JwtTokenProvider(@Value("${security.jwt.token.secret-key}") final String secretKey, + @Value("${security.jwt.token.access-token-expire-length}") final long accessTokenValidityInMilliseconds, + @Value("${security.jwt.token.refresh-token-expire-length}") final long refreshTokenValidityInMilliseconds) { + this.key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey)); + this.accessTokenValidityInMilliseconds = accessTokenValidityInMilliseconds; + this.refreshTokenValidityInMilliseconds = refreshTokenValidityInMilliseconds; + } + + public String createAccessToken(Long payload) { + return createToken(payload, accessTokenValidityInMilliseconds); + } + + public String createRefreshToken(Long payload) { + return createToken(payload, refreshTokenValidityInMilliseconds); + } + + private String createToken(Long payload, long validityInMilliseconds) { + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); + + return Jwts.builder() + .subject(Long.toString(payload)) + .issuedAt(new Date()) + .expiration(validity) + .signWith(key) + .compact(); + } + + public Long getPayload(String token) { + return Long.valueOf( + getClaims(token).getPayload().getSubject()); + } + + public boolean validateTokenNotUsable(String token) { + try { + Jws claims = getClaims(token); + + return claims.getPayload().getExpiration().before(new Date()); + } catch (ExpiredJwtException e) { + throw new RuntimeException("토큰이 만료되었습니다."); + } catch (JwtException | IllegalArgumentException e) { + return true; + } + } + + private Jws getClaims(String token) { + final int CLOCK_SKEW_SECONDS = 3 * 60; + return Jwts.parser().clockSkewSeconds(CLOCK_SKEW_SECONDS).verifyWith(key).build() + .parseSignedClaims(token); + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/RandomNickName.java b/src/main/java/solitour_backend/solitour/auth/support/RandomNickName.java new file mode 100644 index 0000000..af1a7aa --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/RandomNickName.java @@ -0,0 +1,13 @@ +package solitour_backend.solitour.auth.support; + +import java.util.UUID; + +public class RandomNickName { + + public static String generateRandomNickname() { + UUID uuid = UUID.randomUUID(); + String nickname = uuid.toString().substring(0, 4); + return "유저" + nickname; + } + +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/google/GoogleConnector.java b/src/main/java/solitour_backend/solitour/auth/support/google/GoogleConnector.java new file mode 100644 index 0000000..58c0da6 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/google/GoogleConnector.java @@ -0,0 +1,118 @@ +package solitour_backend.solitour.auth.support.google; + + +import java.io.IOException; +import java.util.Collections; +import java.util.Optional; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import solitour_backend.solitour.auth.support.google.dto.GoogleTokenResponse; +import solitour_backend.solitour.auth.support.google.dto.GoogleUserResponse; +import solitour_backend.solitour.auth.support.kakao.dto.KakaoTokenResponse; + +@Getter +@RequiredArgsConstructor +@Component +public class GoogleConnector { + + private static final String BEARER_TYPE = "Bearer"; + private static final RestTemplate REST_TEMPLATE = new RestTemplate(); + + private final GoogleProvider provider; + + + public ResponseEntity requestGoogleUserInfo(String code, String redirectUrl) { + String googleToken = requestAccessToken(code, redirectUrl); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", String.join(" ", BEARER_TYPE, googleToken)); + HttpEntity entity = new HttpEntity<>(headers); + + return REST_TEMPLATE.exchange(provider.getUserInfoUrl(), HttpMethod.GET, entity, + GoogleUserResponse.class); + } + + public String requestAccessToken(String code, String redirectUrl) { + HttpEntity> entity = new HttpEntity<>( + createLoginBody(code, redirectUrl), createLoginHeaders()); + + ResponseEntity response = REST_TEMPLATE.postForEntity( + provider.getAccessTokenUrl(), + entity, GoogleTokenResponse.class); + + return extractAccessToken(response); + } + + public HttpStatusCode requestRevoke(String token) throws IOException { + HttpEntity> entity = new HttpEntity<>( + createRevokeBody(token), createRevokeHeaders()); + + ResponseEntity response = REST_TEMPLATE.postForEntity(provider.getRevokeUrl(), entity, Void.class); + + return response.getStatusCode(); + } + + public String refreshToken(String refreshToken) { + HttpEntity> entity = new HttpEntity<>( + createRefreshBody(refreshToken), createLoginHeaders()); + + return REST_TEMPLATE.postForEntity( + provider.getAccessTokenUrl(), + entity, KakaoTokenResponse.class).getBody().getAccessToken(); + } + + private HttpHeaders createLoginHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + return headers; + } + + private MultiValueMap createLoginBody(String code, String redirectUrl) { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("code", code); + body.add("client_id", provider.getClientId()); + body.add("client_secret", provider.getClientSecret()); + body.add("redirect_uri", redirectUrl); + body.add("grant_type", provider.getGrantType()); + return body; + } + + private MultiValueMap createRefreshBody(String refreshToken) { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("client_id", provider.getClientId()); + body.add("client_secret", provider.getClientSecret()); + body.add("grant_type", provider.getRefreshGrantType()); + body.add("refresh_token", refreshToken); + return body; + } + + private HttpHeaders createRevokeHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + return headers; + } + + private MultiValueMap createRevokeBody(String token) { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("token", token); + return body; + } + + private String extractAccessToken(ResponseEntity responseEntity) { + GoogleTokenResponse response = Optional.ofNullable(responseEntity.getBody()) + .orElseThrow(() -> new RuntimeException("구글 토큰을 가져오는데 실패했습니다.")); + + return response.getAccessToken(); + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/google/GoogleProvider.java b/src/main/java/solitour_backend/solitour/auth/support/google/GoogleProvider.java new file mode 100644 index 0000000..be927c3 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/google/GoogleProvider.java @@ -0,0 +1,59 @@ +package solitour_backend.solitour.auth.support.google; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Getter +@Component +public class GoogleProvider { + + private final String clientId; + private final String clientSecret; + private final String authUrl; + private final String accessTokenUrl; + private final String userInfoUrl; + private final String revokeUrl; + private final String grantType; + private final String refreshGrantType; + private final String scope; + + public GoogleProvider(@Value("${oauth2.google.client.id}") String clientId, + @Value("${oauth2.google.client.secret}") String clientSecret, + @Value("${oauth2.google.url.auth}") String authUrl, + @Value("${oauth2.google.url.token}") String accessTokenUrl, + @Value("${oauth2.google.url.userinfo}") String userInfoUrl, + @Value("${oauth2.google.url.revoke}") String revokeUrl, + @Value("${oauth2.google.grant-type}") String grantType, + @Value("${oauth2.google.refresh-grant-type}") String refreshGrantType, + @Value("${oauth2.google.scope}") String scope) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.authUrl = authUrl; + this.accessTokenUrl = accessTokenUrl; + this.userInfoUrl = userInfoUrl; + this.revokeUrl = revokeUrl; + this.grantType = grantType; + this.refreshGrantType = refreshGrantType; + this.scope = scope; + } + + public String generateAuthUrl(String redirectUrl) { + Map params = new HashMap<>(); + params.put("scope", scope); + params.put("response_type", "code"); + params.put("client_id", clientId); + params.put("redirect_uri", redirectUrl); + return authUrl + "?" + concatParams(params); + } + + private String concatParams(Map params) { + return params.entrySet() + .stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining("&")); + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/google/dto/GoogleTokenResponse.java b/src/main/java/solitour_backend/solitour/auth/support/google/dto/GoogleTokenResponse.java new file mode 100644 index 0000000..b76dcff --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/google/dto/GoogleTokenResponse.java @@ -0,0 +1,16 @@ +package solitour_backend.solitour.auth.support.google.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class GoogleTokenResponse { + + private String accessToken; +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/google/dto/GoogleUserResponse.java b/src/main/java/solitour_backend/solitour/auth/support/google/dto/GoogleUserResponse.java new file mode 100644 index 0000000..6542575 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/google/dto/GoogleUserResponse.java @@ -0,0 +1,104 @@ +package solitour_backend.solitour.auth.support.google.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class GoogleUserResponse { + + @JsonProperty("resourceName") + private String resourceName; + + @Data + public static class Name { + + @JsonProperty("displayName") + private String displayName; + + @JsonProperty("familyName") + private String familyName; + + @JsonProperty("givenName") + private String givenName; + } + + @Data + public static class EmailAddress { + + @JsonProperty("value") + private String value; + + @JsonProperty("type") + private String type; + } + + @Data + public static class PhoneNumber { + + @JsonProperty("value") + private String value; + + @JsonProperty("type") + private String type; + } + + @Data + public static class Gender { + + @JsonProperty("value") + private String value; + } + + @Data + public static class Birthday { + + @JsonProperty("date") + private Date date; + + @Data + public static class Date { + + @JsonProperty("year") + private int year; + + @JsonProperty("month") + private int month; + + @JsonProperty("day") + private int day; + } + } + + @JsonProperty("names") + private List names; + + @JsonProperty("emailAddresses") + private List emailAddresses; + + @JsonProperty("phoneNumbers") + private List phoneNumbers; + + @JsonProperty("genders") + private List genders; + + @JsonProperty("birthdays") + private List birthdays; + + public PhoneNumber getMobilePhoneNumber() { + if (phoneNumbers != null) { + for (PhoneNumber phoneNumber : phoneNumbers) { + if ("mobile".equalsIgnoreCase(phoneNumber.getType())) { + return phoneNumber; + } + } + } + return null; + } + +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/kakao/KakaoConnector.java b/src/main/java/solitour_backend/solitour/auth/support/kakao/KakaoConnector.java new file mode 100644 index 0000000..c5f34dc --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/kakao/KakaoConnector.java @@ -0,0 +1,105 @@ +package solitour_backend.solitour.auth.support.kakao; + + +import java.io.IOException; +import java.util.Collections; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import solitour_backend.solitour.auth.support.kakao.dto.KakaoTokenAndUserResponse; +import solitour_backend.solitour.auth.support.kakao.dto.KakaoTokenResponse; +import solitour_backend.solitour.auth.support.kakao.dto.KakaoUserResponse; + +@Getter +@RequiredArgsConstructor +@Component +public class KakaoConnector { + + private static final String BEARER_TYPE = "Bearer"; + private static final RestTemplate REST_TEMPLATE = new RestTemplate(); + + private final KakaoProvider provider; + + public KakaoTokenAndUserResponse requestKakaoUserInfo(String code, String redirectUrl) { + KakaoTokenResponse kakaoToken = requestAccessToken(code, redirectUrl); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", String.join(" ", BEARER_TYPE, kakaoToken.getAccessToken())); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity responseEntity = REST_TEMPLATE.exchange(provider.getUserInfoUrl(), + HttpMethod.GET, entity, + KakaoUserResponse.class); + + return new KakaoTokenAndUserResponse(kakaoToken, responseEntity.getBody()); + + } + + public KakaoTokenResponse requestAccessToken(String code, String redirectUrl) { + HttpEntity> entity = new HttpEntity<>( + createBody(code, redirectUrl), createHeaders()); + + return REST_TEMPLATE.postForEntity( + provider.getAccessTokenUrl(), + entity, KakaoTokenResponse.class).getBody(); + } + + public String refreshToken(String refreshToken) { + HttpEntity> entity = new HttpEntity<>( + createRefreshBody(refreshToken), createHeaders()); + + return REST_TEMPLATE.postForEntity( + provider.getAccessTokenUrl(), + entity, KakaoTokenResponse.class).getBody().getAccessToken(); + } + + private HttpHeaders createHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + return headers; + } + + private MultiValueMap createBody(String code, String redirectUrl) { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("code", code); + body.add("client_id", provider.getClientId()); + body.add("client_secret", provider.getClientSecret()); + body.add("redirect_uri", redirectUrl); + body.add("grant_type", provider.getGrantType()); + return body; + } + + private MultiValueMap createRefreshBody(String refreshToken) { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("client_id", provider.getClientId()); + body.add("client_secret", provider.getClientSecret()); + body.add("grant_type", provider.getRefreshGrantType()); + body.add("refresh_token", refreshToken); + return body; + } + + public HttpStatusCode requestRevoke(String token) throws IOException { + HttpEntity> entity = new HttpEntity<>(createRevokeHeaders(token)); + + ResponseEntity response = REST_TEMPLATE.postForEntity(provider.getRevokeUrl(), entity, String.class); + + return response.getStatusCode(); + } + + private HttpHeaders createRevokeHeaders(String token) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("Authorization", String.join(" ", BEARER_TYPE, token)); + return headers; + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/kakao/KakaoProvider.java b/src/main/java/solitour_backend/solitour/auth/support/kakao/KakaoProvider.java new file mode 100644 index 0000000..cabf515 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/kakao/KakaoProvider.java @@ -0,0 +1,59 @@ +package solitour_backend.solitour.auth.support.kakao; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Getter +@Component +public class KakaoProvider { + + private final String clientId; + private final String clientSecret; + private final String authUrl; + private final String accessTokenUrl; + private final String userInfoUrl; + private final String grantType; + private final String refreshGrantType; + private final String revokeUrl; + private final String scope; + + + public KakaoProvider(@Value("${oauth2.kakao.client.id}") String clientId, + @Value("${oauth2.kakao.client.secret}") String clientSecret, + @Value("${oauth2.kakao.url.auth}") String authUrl, + @Value("${oauth2.kakao.url.token}") String accessTokenUrl, + @Value("${oauth2.kakao.url.userinfo}") String userInfoUrl, + @Value("${oauth2.kakao.grant-type}") String grantType, + @Value("${oauth2.kakao.refresh-grant-type}") String refreshGrantType, + @Value("${oauth2.kakao.url.revoke}") String revokeUrl, + @Value("${oauth2.kakao.scope}") String scope) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.authUrl = authUrl; + this.accessTokenUrl = accessTokenUrl; + this.userInfoUrl = userInfoUrl; + this.grantType = grantType; + this.refreshGrantType = refreshGrantType; + this.revokeUrl = revokeUrl; + this.scope = scope; + } + + public String generateAuthUrl(String redirectUrl) { + Map params = new HashMap<>(); + params.put("response_type", "code"); + params.put("client_id", clientId); + params.put("redirect_uri", redirectUrl); + return authUrl + "?" + concatParams(params); + } + + private String concatParams(Map params) { + return params.entrySet() + .stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining("&")); + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/kakao/dto/KakaoTokenAndUserResponse.java b/src/main/java/solitour_backend/solitour/auth/support/kakao/dto/KakaoTokenAndUserResponse.java new file mode 100644 index 0000000..0bfa374 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/kakao/dto/KakaoTokenAndUserResponse.java @@ -0,0 +1,17 @@ +package solitour_backend.solitour.auth.support.kakao.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class KakaoTokenAndUserResponse { + + private KakaoTokenResponse kakaoTokenResponse; + private KakaoUserResponse kakaoUserResponse; +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/kakao/dto/KakaoTokenResponse.java b/src/main/java/solitour_backend/solitour/auth/support/kakao/dto/KakaoTokenResponse.java new file mode 100644 index 0000000..bb3895a --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/kakao/dto/KakaoTokenResponse.java @@ -0,0 +1,17 @@ +package solitour_backend.solitour.auth.support.kakao.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class KakaoTokenResponse { + + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/kakao/dto/KakaoUserResponse.java b/src/main/java/solitour_backend/solitour/auth/support/kakao/dto/KakaoUserResponse.java new file mode 100644 index 0000000..af3b0ab --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/kakao/dto/KakaoUserResponse.java @@ -0,0 +1,189 @@ +package solitour_backend.solitour.auth.support.kakao.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Date; +import java.util.HashMap; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class KakaoUserResponse { + + //회원 번호 + @JsonProperty("id") + public Long id; + + //자동 연결 설정을 비활성화한 경우만 존재. + //true : 연결 상태, false : 연결 대기 상태 + @JsonProperty("has_signed_up") + public Boolean hasSignedUp; + + //서비스에 연결 완료된 시각. UTC + @JsonProperty("connected_at") + public Date connectedAt; + + //카카오싱크 간편가입을 통해 로그인한 시각. UTC + @JsonProperty("synched_at") + public Date synchedAt; + + //사용자 프로퍼티 + @JsonProperty("properties") + public HashMap properties; + + //카카오 계정 정보 + @JsonProperty("kakao_account") + public KakaoAccount kakaoAccount; + + //uuid 등 추가 정보 + @JsonProperty("for_partner") + public Partner partner; + + @Getter + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + public class KakaoAccount { + + //프로필 정보 제공 동의 여부 + @JsonProperty("profile_needs_agreement") + public Boolean isProfileAgree; + + //닉네임 제공 동의 여부 + @JsonProperty("profile_nickname_needs_agreement") + public Boolean isNickNameAgree; + + //프로필 사진 제공 동의 여부 + @JsonProperty("profile_image_needs_agreement") + public Boolean isProfileImageAgree; + + //사용자 프로필 정보 + @JsonProperty("profile") + public Profile profile; + + //이름 제공 동의 여부 + @JsonProperty("name_needs_agreement") + public Boolean isNameAgree; + + //카카오계정 이름 + @JsonProperty("name") + public String name; + + //이메일 제공 동의 여부 + @JsonProperty("email_needs_agreement") + public Boolean isEmailAgree; + + //이메일이 유효 여부 + // true : 유효한 이메일, false : 이메일이 다른 카카오 계정에 사용돼 만료 + @JsonProperty("is_email_valid") + public Boolean isEmailValid; + + //이메일이 인증 여부 + //true : 인증된 이메일, false : 인증되지 않은 이메일 + @JsonProperty("is_email_verified") + public Boolean isEmailVerified; + + //카카오계정 대표 이메일 + @JsonProperty("email") + public String email; + + //연령대 제공 동의 여부 + @JsonProperty("age_range_needs_agreement") + public Boolean isAgeAgree; + + //연령대 + //참고 https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info + @JsonProperty("age_range") + public String ageRange; + + //출생 연도 제공 동의 여부 + @JsonProperty("birthyear_needs_agreement") + public Boolean isBirthYearAgree; + + //출생 연도 (YYYY 형식) + @JsonProperty("birthyear") + public String birthYear; + + //생일 제공 동의 여부 + @JsonProperty("birthday_needs_agreement") + public Boolean isBirthDayAgree; + + //생일 (MMDD 형식) + @JsonProperty("birthday") + public String birthDay; + + //생일 타입 + // SOLAR(양력) 혹은 LUNAR(음력) + @JsonProperty("birthday_type") + public String birthDayType; + + //성별 제공 동의 여부 + @JsonProperty("gender_needs_agreement") + public Boolean isGenderAgree; + + //성별 + @JsonProperty("gender") + public String gender; + + //전화번호 제공 동의 여부 + @JsonProperty("phone_number_needs_agreement") + public Boolean isPhoneNumberAgree; + + //전화번호 + //국내 번호인 경우 +82 00-0000-0000 형식 + @JsonProperty("phone_number") + public String phoneNumber; + + //CI 동의 여부 + @JsonProperty("ci_needs_agreement") + public Boolean isCIAgree; + + //CI, 연계 정보 + @JsonProperty("ci") + public String ci; + + //CI 발급 시각, UTC + @JsonProperty("ci_authenticated_at") + public Date ciCreatedAt; + + @Getter + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + public class Profile { + + //닉네임 + @JsonProperty("nickname") + public String nickName; + + //프로필 미리보기 이미지 URL + @JsonProperty("thumbnail_image_url") + public String thumbnailImageUrl; + + //프로필 사진 URL + @JsonProperty("profile_image_url") + public String profileImageUrl; + + //프로필 사진 URL 기본 프로필인지 여부 + //true : 기본 프로필, false : 사용자 등록 + @JsonProperty("is_default_image") + public String isDefaultImage; + + //닉네임이 기본 닉네임인지 여부 + //true : 기본 닉네임, false : 사용자 등록 + @JsonProperty("is_default_nickname") + public Boolean isDefaultNickName; + + } + } + + @Getter + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + public class Partner { + + //고유 ID + @JsonProperty("uuid") + public String uuid; + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/kakao/dto/request/CreateUserInfoRequest.java b/src/main/java/solitour_backend/solitour/auth/support/kakao/dto/request/CreateUserInfoRequest.java new file mode 100644 index 0000000..ddba8c8 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/kakao/dto/request/CreateUserInfoRequest.java @@ -0,0 +1,12 @@ +package solitour_backend.solitour.auth.support.kakao.dto.request; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Getter +public class CreateUserInfoRequest { + private String name; + private String age; + private String sex; +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/naver/NaverConnector.java b/src/main/java/solitour_backend/solitour/auth/support/naver/NaverConnector.java new file mode 100644 index 0000000..5d47e7b --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/naver/NaverConnector.java @@ -0,0 +1,118 @@ +package solitour_backend.solitour.auth.support.naver; + + +import java.io.IOException; +import java.util.Collections; +import java.util.UUID; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import solitour_backend.solitour.auth.support.kakao.dto.KakaoUserResponse; +import solitour_backend.solitour.auth.support.naver.dto.NaverTokenAndUserResponse; +import solitour_backend.solitour.auth.support.naver.dto.NaverTokenResponse; +import solitour_backend.solitour.auth.support.naver.dto.NaverUserResponse; + +@Getter +@RequiredArgsConstructor +@Component +public class NaverConnector { + + private static final String BEARER_TYPE = "Bearer"; + private static final RestTemplate REST_TEMPLATE = new RestTemplate(); + + private final NaverProvider provider; + + public NaverTokenAndUserResponse requestNaverUserInfo(String code) { + NaverTokenResponse naverTokenResponse = requestAccessToken(code); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", String.join(" ", BEARER_TYPE, naverTokenResponse.getAccessToken())); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity responseEntity = REST_TEMPLATE.exchange(provider.getUserInfoUrl(), + HttpMethod.GET, entity, + NaverUserResponse.class); + + return new NaverTokenAndUserResponse(naverTokenResponse, responseEntity.getBody()); + + } + + public NaverTokenResponse requestAccessToken(String code) { + HttpEntity> entity = new HttpEntity<>( + createBody(code), createHeaders()); + + return REST_TEMPLATE.postForEntity( + provider.getAccessTokenUrl(), + entity, + NaverTokenResponse.class).getBody(); + } + + public String refreshToken(String refreshToken) { + HttpEntity> entity = new HttpEntity<>( + createRefreshBody(refreshToken), createHeaders()); + + return REST_TEMPLATE.postForEntity( + provider.getAccessTokenUrl(), + entity, NaverTokenResponse.class).getBody().getAccessToken(); + } + + private HttpHeaders createHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + return headers; + } + + private MultiValueMap createBody(String code) { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("code", code); + body.add("grant_type", provider.getGrantType()); + body.add("client_id", provider.getClientId()); + body.add("client_secret", provider.getClientSecret()); + body.add("state", UUID.randomUUID().toString()); + return body; + } + + private MultiValueMap createRefreshBody(String refreshToken) { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", provider.getRefreshGrantType()); + body.add("client_id", provider.getClientId()); + body.add("client_secret", provider.getClientSecret()); + body.add("refresh_token", refreshToken); + return body; + } + + public HttpStatusCode requestRevoke(String token) throws IOException { + HttpEntity> entity = new HttpEntity<>(createRevokeBody(token),createRevokeHeaders(token)); + + ResponseEntity response = REST_TEMPLATE.postForEntity(provider.getAccessTokenUrl(), entity, String.class); + + return response.getStatusCode(); + } + + private MultiValueMap createRevokeBody(String accessToken) { + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("client_id", provider.getClientId()); + body.add("client_secret", provider.getClientSecret()); + body.add("grant_type", provider.getRevokeGrantType()); + body.add("access_token", accessToken); + body.add("service_provider", provider.getServiceProvider()); + return body; + } + + private HttpHeaders createRevokeHeaders(String token) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("Authorization", String.join(" ", BEARER_TYPE, token)); + return headers; + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/naver/NaverProvider.java b/src/main/java/solitour_backend/solitour/auth/support/naver/NaverProvider.java new file mode 100644 index 0000000..3b2e3f8 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/naver/NaverProvider.java @@ -0,0 +1,79 @@ +package solitour_backend.solitour.auth.support.naver; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import solitour_backend.solitour.auth.support.naver.dto.NaverTokenResponse; + +@Getter +@Component +public class NaverProvider { + + private final String clientId; + private final String clientSecret; + private final String authUrl; + private final String accessTokenUrl; + private final String userInfoUrl; + private final String grantType; + private final String refreshGrantType; + private final String revokeGrantType; + private final String serviceProvider; + private final String state = UUID.randomUUID().toString(); + + + public NaverProvider(@Value("${oauth2.naver.client.id}") String clientId, + @Value("${oauth2.naver.client.secret}") String clientSecret, + @Value("${oauth2.naver.url.auth}") String authUrl, + @Value("${oauth2.naver.url.token}") String accessTokenUrl, + @Value("${oauth2.naver.url.userinfo}") String userInfoUrl, + @Value("${oauth2.naver.service-provider}") String serviceProvider, + @Value("${oauth2.naver.grant-type}") String grantType, + @Value("${oauth2.naver.refresh-grant-type}") String refreshGrantType, + @Value("${oauth2.naver.revoke-grant-type}") String revokeGrantType) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.authUrl = authUrl; + this.accessTokenUrl = accessTokenUrl; + this.userInfoUrl = userInfoUrl; + this.serviceProvider = serviceProvider; + this.grantType = grantType; + this.refreshGrantType = refreshGrantType; + this.revokeGrantType = revokeGrantType; + } + + public String generateTokenUrl(String grantType, String code) { + Map params = new HashMap<>(); + params.put("grant_type", grantType); + params.put("client_id", clientId); + params.put("client_secret", clientSecret); + params.put("code", code); + params.put("state", state); + return authUrl + "?" + concatParams(params); + } + + public String generateAuthUrl(String redirectUrl) { + Map params = new HashMap<>(); + params.put("response_type", "code"); + params.put("client_id", clientId); + params.put("redirect_uri", redirectUrl); + params.put("state", state); + return authUrl + "?" + concatParams(params); + } + + private String concatParams(Map params) { + return params.entrySet() + .stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.joining("&")); + } + + public String generateAccessTokenUrl(String code) { + return generateTokenUrl("authorization_code", code); + } +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverTokenAndUserResponse.java b/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverTokenAndUserResponse.java new file mode 100644 index 0000000..fcdee93 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverTokenAndUserResponse.java @@ -0,0 +1,17 @@ +package solitour_backend.solitour.auth.support.naver.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class NaverTokenAndUserResponse { + + private NaverTokenResponse naverTokenResponse; + private NaverUserResponse naverUserResponse; +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverTokenResponse.java b/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverTokenResponse.java new file mode 100644 index 0000000..ccb38fb --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverTokenResponse.java @@ -0,0 +1,17 @@ +package solitour_backend.solitour.auth.support.naver.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class NaverTokenResponse { + + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverUserResponse.java b/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverUserResponse.java new file mode 100644 index 0000000..6b4329d --- /dev/null +++ b/src/main/java/solitour_backend/solitour/auth/support/naver/dto/NaverUserResponse.java @@ -0,0 +1,36 @@ +package solitour_backend.solitour.auth.support.naver.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Date; +import java.util.HashMap; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import solitour_backend.solitour.auth.support.kakao.dto.KakaoUserResponse.Partner; + +@Getter +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class NaverUserResponse { + private String resultcode; + private String message; + private Response response; + + @Getter + @NoArgsConstructor + public static class Response { + private String id; + private String email; + private String name; + private String nickname; + private String gender; + private String age; + private String birthday; + private String birthyear; + private String mobile; + private String profileImage; + } + +} diff --git a/src/main/java/solitour_backend/solitour/book_mark_gathering/controller/BookMarkGatheringController.java b/src/main/java/solitour_backend/solitour/book_mark_gathering/controller/BookMarkGatheringController.java new file mode 100644 index 0000000..8f6f9e7 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/book_mark_gathering/controller/BookMarkGatheringController.java @@ -0,0 +1,41 @@ +package solitour_backend.solitour.book_mark_gathering.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import solitour_backend.solitour.auth.config.Authenticated; +import solitour_backend.solitour.auth.config.AuthenticationPrincipal; +import solitour_backend.solitour.book_mark_gathering.entity.BookMarkGathering; +import solitour_backend.solitour.book_mark_gathering.service.BookMarkGatheringService; + +@Authenticated +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/bookmark/gathering") +public class BookMarkGatheringController { + + private final BookMarkGatheringService service; + + @Transactional + @PostMapping() + public ResponseEntity createUserBookmark( + @AuthenticationPrincipal Long userId, @RequestParam Long gatheringId) { + BookMarkGathering bookMarkGathering = service.createUserBookmark(userId, gatheringId); + + return ResponseEntity.ok(bookMarkGathering.getId()); + } + + @Transactional + @DeleteMapping() + public ResponseEntity deleteUserBookmark(@AuthenticationPrincipal Long userId, + @RequestParam Long gatheringId) { + service.deleteUserBookmark(userId, gatheringId); + + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/solitour_backend/solitour/book_mark_gathering/dto/response/BookMarkGatheringResponse.java b/src/main/java/solitour_backend/solitour/book_mark_gathering/dto/response/BookMarkGatheringResponse.java new file mode 100644 index 0000000..2548c6e --- /dev/null +++ b/src/main/java/solitour_backend/solitour/book_mark_gathering/dto/response/BookMarkGatheringResponse.java @@ -0,0 +1,29 @@ +package solitour_backend.solitour.book_mark_gathering.dto.response; + +import java.util.List; +import lombok.Getter; +import solitour_backend.solitour.book_mark_gathering.entity.BookMarkGathering; + +@Getter +public class BookMarkGatheringResponse { + + private final BookMarkDto bookMarkGathering; + + public BookMarkGatheringResponse(List bookMarkGathering) { + this.bookMarkGathering = new BookMarkDto(bookMarkGathering); + } + + @Getter + private static class BookMarkDto { + + private final List bookMarkInfoList; + + private BookMarkDto(List bookMarkGathering) { + this.bookMarkInfoList = bookMarkGathering.stream().map(bookMark -> { + Long bookMarkId = bookMark.getGathering().getId(); + return new BookMarkInfo(bookMarkId); + }).toList(); + } + + } +} diff --git a/src/main/java/solitour_backend/solitour/book_mark_gathering/dto/response/BookMarkInfo.java b/src/main/java/solitour_backend/solitour/book_mark_gathering/dto/response/BookMarkInfo.java new file mode 100644 index 0000000..8357c89 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/book_mark_gathering/dto/response/BookMarkInfo.java @@ -0,0 +1,10 @@ +package solitour_backend.solitour.book_mark_gathering.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class BookMarkInfo { + private Long bookMarkId; +} diff --git a/src/main/java/solitour_backend/solitour/book_mark_gathering/entity/BookMarkGathering.java b/src/main/java/solitour_backend/solitour/book_mark_gathering/entity/BookMarkGathering.java new file mode 100644 index 0000000..2263e04 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/book_mark_gathering/entity/BookMarkGathering.java @@ -0,0 +1,42 @@ +package solitour_backend.solitour.book_mark_gathering.entity; + +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.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import solitour_backend.solitour.gathering.entity.Gathering; +import solitour_backend.solitour.user.entity.User; + +@Entity +@Getter +@Table(name = "book_mark_gathering") +@NoArgsConstructor +public class BookMarkGathering { + + @Id + @Column(name = "book_mark_gathering_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "gathering_id") + private Gathering gathering; + + public BookMarkGathering(User user, Gathering gathering) { + this.user = user; + this.gathering = gathering; + } + +} diff --git a/src/main/java/solitour_backend/solitour/book_mark_gathering/exception/GatheringBookMarkNotExistsException.java b/src/main/java/solitour_backend/solitour/book_mark_gathering/exception/GatheringBookMarkNotExistsException.java new file mode 100644 index 0000000..1381ce1 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/book_mark_gathering/exception/GatheringBookMarkNotExistsException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.book_mark_gathering.exception; + +public class GatheringBookMarkNotExistsException extends RuntimeException { + public GatheringBookMarkNotExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/book_mark_gathering/repository/BookMarkGatheringRepository.java b/src/main/java/solitour_backend/solitour/book_mark_gathering/repository/BookMarkGatheringRepository.java new file mode 100644 index 0000000..d3e08e8 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/book_mark_gathering/repository/BookMarkGatheringRepository.java @@ -0,0 +1,15 @@ +package solitour_backend.solitour.book_mark_gathering.repository; + +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import solitour_backend.solitour.book_mark_gathering.entity.BookMarkGathering; + +public interface BookMarkGatheringRepository extends JpaRepository { + + @Query("SELECT b FROM BookMarkGathering b JOIN FETCH b.user u JOIN FETCH b.gathering i WHERE u.id = :userId") + List findByUserId(Long userId); + + Optional findByGatheringIdAndUserId(Long gatheringId, Long userId); +} diff --git a/src/main/java/solitour_backend/solitour/book_mark_gathering/service/BookMarkGatheringService.java b/src/main/java/solitour_backend/solitour/book_mark_gathering/service/BookMarkGatheringService.java new file mode 100644 index 0000000..4b11406 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/book_mark_gathering/service/BookMarkGatheringService.java @@ -0,0 +1,44 @@ +package solitour_backend.solitour.book_mark_gathering.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import solitour_backend.solitour.book_mark_gathering.entity.BookMarkGathering; +import solitour_backend.solitour.book_mark_gathering.exception.GatheringBookMarkNotExistsException; +import solitour_backend.solitour.book_mark_gathering.repository.BookMarkGatheringRepository; +import solitour_backend.solitour.gathering.entity.Gathering; +import solitour_backend.solitour.gathering.repository.GatheringRepository; +import solitour_backend.solitour.information.exception.InformationNotExistsException; +import solitour_backend.solitour.user.entity.User; +import solitour_backend.solitour.user.repository.UserRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BookMarkGatheringService { + + private final BookMarkGatheringRepository bookMarkGatheringRepository; + private final UserRepository userRepository; + private final GatheringRepository gatheringRepository; + + @Transactional + public BookMarkGathering createUserBookmark(Long userId, Long gatheringId) { + User user = userRepository.findByUserId(userId); + Gathering gathering = gatheringRepository.findById(gatheringId) + .orElseThrow(() -> new InformationNotExistsException("해당하는 정보가 없습니다")); + + return bookMarkGatheringRepository.findByGatheringIdAndUserId(gatheringId, userId) + .orElseGet( + () -> bookMarkGatheringRepository.save(new BookMarkGathering(user, gathering))); + } + + @Transactional + public void deleteUserBookmark(Long userId, Long gatheringId) { + BookMarkGathering bookmark = bookMarkGatheringRepository.findByGatheringIdAndUserId(gatheringId, + userId) + .orElseThrow(() -> new GatheringBookMarkNotExistsException("해당하는 북마크가 없습니다")); + + bookMarkGatheringRepository.delete(bookmark); + } + +} diff --git a/src/main/java/solitour_backend/solitour/book_mark_information/controller/BookMarkInformationController.java b/src/main/java/solitour_backend/solitour/book_mark_information/controller/BookMarkInformationController.java new file mode 100644 index 0000000..3cc8c30 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/book_mark_information/controller/BookMarkInformationController.java @@ -0,0 +1,41 @@ +package solitour_backend.solitour.book_mark_information.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import solitour_backend.solitour.auth.config.Authenticated; +import solitour_backend.solitour.auth.config.AuthenticationPrincipal; +import solitour_backend.solitour.book_mark_information.entity.BookMarkInformation; +import solitour_backend.solitour.book_mark_information.service.BookMarkInformationService; + +@Authenticated +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/bookmark/information") +public class BookMarkInformationController { + + private final BookMarkInformationService service; + + @Transactional + @PostMapping() + public ResponseEntity createUserBookmark( + @AuthenticationPrincipal Long userId, @RequestParam Long infoId) { + BookMarkInformation bookMarkInformation = service.createUserBookmark(userId, infoId); + + return ResponseEntity.ok(bookMarkInformation.getId()); + } + + @Transactional + @DeleteMapping() + public ResponseEntity deleteUserBookmark(@AuthenticationPrincipal Long userId, + @RequestParam Long infoId) { + service.deleteUserBookmark(userId, infoId); + + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/solitour_backend/solitour/book_mark_information/entity/BookMarkInformation.java b/src/main/java/solitour_backend/solitour/book_mark_information/entity/BookMarkInformation.java new file mode 100644 index 0000000..47eb51b --- /dev/null +++ b/src/main/java/solitour_backend/solitour/book_mark_information/entity/BookMarkInformation.java @@ -0,0 +1,40 @@ +package solitour_backend.solitour.book_mark_information.entity; + +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.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import solitour_backend.solitour.information.entity.Information; +import solitour_backend.solitour.user.entity.User; + +@Entity +@Getter +@Table(name = "book_mark_information") +@NoArgsConstructor +public class BookMarkInformation { + + @Id + @Column(name = "book_mark_information_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "information_id") + private Information information; + + public BookMarkInformation(User user, Information information) { + this.user = user; + this.information = information; + } +} diff --git a/src/main/java/solitour_backend/solitour/book_mark_information/entity/BookMarkInformationRepository.java b/src/main/java/solitour_backend/solitour/book_mark_information/entity/BookMarkInformationRepository.java new file mode 100644 index 0000000..4e4e461 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/book_mark_information/entity/BookMarkInformationRepository.java @@ -0,0 +1,16 @@ +package solitour_backend.solitour.book_mark_information.entity; + +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface BookMarkInformationRepository extends JpaRepository { + + @Query("SELECT b FROM BookMarkInformation b JOIN FETCH b.user u JOIN FETCH b.information i WHERE u.id = :userId") + List findByUserId(Long userId); + + Optional findByInformationIdAndUserId(Long infoId, Long userId); + + void deleteAllByInformationId(Long informationId); +} diff --git a/src/main/java/solitour_backend/solitour/book_mark_information/exception/InformationBookMarkNotExistsException.java b/src/main/java/solitour_backend/solitour/book_mark_information/exception/InformationBookMarkNotExistsException.java new file mode 100644 index 0000000..408696a --- /dev/null +++ b/src/main/java/solitour_backend/solitour/book_mark_information/exception/InformationBookMarkNotExistsException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.book_mark_information.exception; + +public class InformationBookMarkNotExistsException extends RuntimeException { + public InformationBookMarkNotExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/book_mark_information/service/BookMarkInformationService.java b/src/main/java/solitour_backend/solitour/book_mark_information/service/BookMarkInformationService.java new file mode 100644 index 0000000..e297b48 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/book_mark_information/service/BookMarkInformationService.java @@ -0,0 +1,40 @@ +package solitour_backend.solitour.book_mark_information.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import solitour_backend.solitour.book_mark_information.entity.BookMarkInformation; +import solitour_backend.solitour.book_mark_information.entity.BookMarkInformationRepository; +import solitour_backend.solitour.book_mark_information.exception.InformationBookMarkNotExistsException; +import solitour_backend.solitour.information.entity.Information; +import solitour_backend.solitour.information.repository.InformationRepository; +import solitour_backend.solitour.user.entity.User; +import solitour_backend.solitour.user.repository.UserRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BookMarkInformationService { + + private final BookMarkInformationRepository bookMarkInformationRepository; + private final UserRepository userRepository; + private final InformationRepository informationRepository; + + public BookMarkInformation createUserBookmark(Long userId, Long infoId) { + User user = userRepository.findByUserId(userId); + Information information = informationRepository.findById(infoId) + .orElseThrow(() -> new IllegalArgumentException("해당 정보가 없습니다.")); + + return bookMarkInformationRepository.findByInformationIdAndUserId(infoId, userId) + .orElseGet(() -> bookMarkInformationRepository.save(new BookMarkInformation(user, information))); + } + + public void deleteUserBookmark(Long userId, Long infoId) { + BookMarkInformation bookmark = bookMarkInformationRepository.findByInformationIdAndUserId(infoId, + userId) + .orElseThrow(() -> new InformationBookMarkNotExistsException("해당하는 북마크가 없습니다")); + + bookMarkInformationRepository.delete(bookmark); + } + +} diff --git a/src/main/java/solitour_backend/solitour/book_mark_information/service/dto/response/BookMarkInfo.java b/src/main/java/solitour_backend/solitour/book_mark_information/service/dto/response/BookMarkInfo.java new file mode 100644 index 0000000..7a83251 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/book_mark_information/service/dto/response/BookMarkInfo.java @@ -0,0 +1,11 @@ +package solitour_backend.solitour.book_mark_information.service.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class BookMarkInfo { + + private Long bookMarkId; +} diff --git a/src/main/java/solitour_backend/solitour/book_mark_information/service/dto/response/BookMarkInformationResponse.java b/src/main/java/solitour_backend/solitour/book_mark_information/service/dto/response/BookMarkInformationResponse.java new file mode 100644 index 0000000..613dfe3 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/book_mark_information/service/dto/response/BookMarkInformationResponse.java @@ -0,0 +1,29 @@ +package solitour_backend.solitour.book_mark_information.service.dto.response; + +import java.util.List; +import lombok.Getter; +import solitour_backend.solitour.book_mark_information.entity.BookMarkInformation; + +@Getter +public class BookMarkInformationResponse { + + private final BookMarkDto bookMarkInformation; + + public BookMarkInformationResponse(List bookMarkInformation) { + this.bookMarkInformation = new BookMarkDto(bookMarkInformation); + } + + @Getter + private static class BookMarkDto { + + private final List bookMarkInfoList; + + private BookMarkDto(List bookMarkInformation) { + this.bookMarkInfoList = bookMarkInformation.stream().map(bookMark -> { + Long bookMarkId = bookMark.getInformation().getId(); + return new BookMarkInfo(bookMarkId); + }).toList(); + } + + } +} diff --git a/src/main/java/solitour_backend/solitour/category/controller/CategoryController.java b/src/main/java/solitour_backend/solitour/category/controller/CategoryController.java new file mode 100644 index 0000000..e7216be --- /dev/null +++ b/src/main/java/solitour_backend/solitour/category/controller/CategoryController.java @@ -0,0 +1,80 @@ +package solitour_backend.solitour.category.controller; + +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import solitour_backend.solitour.category.dto.request.CategoryModifyRequest; +import solitour_backend.solitour.category.dto.request.CategoryRegisterRequest; +import solitour_backend.solitour.category.dto.response.CategoryGetResponse; +import solitour_backend.solitour.category.dto.response.CategoryResponse; +import solitour_backend.solitour.category.service.CategoryService; +import solitour_backend.solitour.error.exception.RequestValidationFailedException; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/categories") +public class CategoryController { + + private final CategoryService categoryService; + + @GetMapping + public ResponseEntity> getAllCategories() { + List parentCategories = categoryService.getParentCategories(); + + return ResponseEntity + .status(HttpStatus.OK) + .body(parentCategories); + } + + @GetMapping("/{id}") + public ResponseEntity getCategory(@PathVariable Long id) { + CategoryResponse category = categoryService.getCategory(id); + + return ResponseEntity + .status(HttpStatus.OK) + .body(category); + } + + @PostMapping + public ResponseEntity registerCategory( + @Valid @RequestBody CategoryRegisterRequest categoryRegisterRequest, + BindingResult bindingResult) { + if (bindingResult.hasErrors()) { + throw new RequestValidationFailedException(bindingResult); + } + CategoryResponse categoryResponse = categoryService.registerCategory( + categoryRegisterRequest); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(categoryResponse); + + } + + @PutMapping("/{id}") + public ResponseEntity modifyCategory( + @Valid @RequestBody CategoryModifyRequest categoryModifyRequest, + BindingResult bindingResult, + @PathVariable Long id) { + if (bindingResult.hasErrors()) { + throw new RequestValidationFailedException(bindingResult); + } + CategoryResponse categoryResponse = categoryService.modifyCategory(id, + categoryModifyRequest); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(categoryResponse); + } + + +} diff --git a/src/main/java/solitour_backend/solitour/category/dto/mapper/CategoryMapper.java b/src/main/java/solitour_backend/solitour/category/dto/mapper/CategoryMapper.java new file mode 100644 index 0000000..4f9c294 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/category/dto/mapper/CategoryMapper.java @@ -0,0 +1,15 @@ +package solitour_backend.solitour.category.dto.mapper; + +import java.util.List; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import solitour_backend.solitour.category.dto.response.CategoryResponse; +import solitour_backend.solitour.category.entity.Category; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface CategoryMapper { + + CategoryResponse mapToCategoryResponse(Category category); + + List mapToCategoryResponses(List categories); +} diff --git a/src/main/java/solitour_backend/solitour/category/dto/request/CategoryModifyRequest.java b/src/main/java/solitour_backend/solitour/category/dto/request/CategoryModifyRequest.java new file mode 100644 index 0000000..08fb801 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/category/dto/request/CategoryModifyRequest.java @@ -0,0 +1,19 @@ +package solitour_backend.solitour.category.dto.request; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class CategoryModifyRequest { + + @Nullable + private Long parentCategory; + + @NotBlank + @Size(min = 2, max = 20) + private String name; +} diff --git a/src/main/java/solitour_backend/solitour/category/dto/request/CategoryRegisterRequest.java b/src/main/java/solitour_backend/solitour/category/dto/request/CategoryRegisterRequest.java new file mode 100644 index 0000000..8e9162c --- /dev/null +++ b/src/main/java/solitour_backend/solitour/category/dto/request/CategoryRegisterRequest.java @@ -0,0 +1,19 @@ +package solitour_backend.solitour.category.dto.request; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class CategoryRegisterRequest { + + @Nullable + private Long parentCategory; + + @NotBlank + @Size(min = 2, max = 20) + private String name; +} diff --git a/src/main/java/solitour_backend/solitour/category/dto/response/CategoryGetResponse.java b/src/main/java/solitour_backend/solitour/category/dto/response/CategoryGetResponse.java new file mode 100644 index 0000000..f4fb2c1 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/category/dto/response/CategoryGetResponse.java @@ -0,0 +1,14 @@ +package solitour_backend.solitour.category.dto.response; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class CategoryGetResponse { + + private Long id; + private String name; + private List childrenCategories; +} diff --git a/src/main/java/solitour_backend/solitour/category/dto/response/CategoryResponse.java b/src/main/java/solitour_backend/solitour/category/dto/response/CategoryResponse.java new file mode 100644 index 0000000..9b3d3fb --- /dev/null +++ b/src/main/java/solitour_backend/solitour/category/dto/response/CategoryResponse.java @@ -0,0 +1,15 @@ +package solitour_backend.solitour.category.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import solitour_backend.solitour.category.entity.Category; + +@Getter +@AllArgsConstructor +public class CategoryResponse { + + private Long id; + private Category parentCategory; + private String name; + +} diff --git a/src/main/java/solitour_backend/solitour/category/entity/Category.java b/src/main/java/solitour_backend/solitour/category/entity/Category.java new file mode 100644 index 0000000..0138662 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/category/entity/Category.java @@ -0,0 +1,38 @@ +package solitour_backend.solitour.category.entity; + +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.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table(name = "category") +@NoArgsConstructor +public class Category { + + @Id + @Column(name = "category_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "parent_category_id") + private Category parentCategory; + + @Column(name = "category_name") + private String name; + + public Category(Category parentCategory, String name) { + this.parentCategory = parentCategory; + this.name = name; + } +} diff --git a/src/main/java/solitour_backend/solitour/category/exception/CategoryNotExistsException.java b/src/main/java/solitour_backend/solitour/category/exception/CategoryNotExistsException.java new file mode 100644 index 0000000..974680b --- /dev/null +++ b/src/main/java/solitour_backend/solitour/category/exception/CategoryNotExistsException.java @@ -0,0 +1,8 @@ +package solitour_backend.solitour.category.exception; + +public class CategoryNotExistsException extends RuntimeException { + + public CategoryNotExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/category/repository/CategoryRepository.java b/src/main/java/solitour_backend/solitour/category/repository/CategoryRepository.java new file mode 100644 index 0000000..dd525a7 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/category/repository/CategoryRepository.java @@ -0,0 +1,14 @@ +package solitour_backend.solitour.category.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import solitour_backend.solitour.category.entity.Category; + +public interface CategoryRepository extends JpaRepository { + + List findAllByParentCategoryId(Long parentCategoryId); + + boolean existsByIdAndParentCategoryId(Long id, Long parentCategoryId); +} diff --git a/src/main/java/solitour_backend/solitour/category/service/CategoryService.java b/src/main/java/solitour_backend/solitour/category/service/CategoryService.java new file mode 100644 index 0000000..6866724 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/category/service/CategoryService.java @@ -0,0 +1,93 @@ +package solitour_backend.solitour.category.service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import solitour_backend.solitour.category.dto.mapper.CategoryMapper; +import solitour_backend.solitour.category.dto.request.CategoryModifyRequest; +import solitour_backend.solitour.category.dto.request.CategoryRegisterRequest; +import solitour_backend.solitour.category.dto.response.CategoryGetResponse; +import solitour_backend.solitour.category.dto.response.CategoryResponse; +import solitour_backend.solitour.category.entity.Category; +import solitour_backend.solitour.category.exception.CategoryNotExistsException; +import solitour_backend.solitour.category.repository.CategoryRepository; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class CategoryService { + + private final CategoryRepository categoryRepository; + private final CategoryMapper categoryMapper; + + @Transactional + public CategoryResponse registerCategory(CategoryRegisterRequest categoryRegisterRequest) { + Category parentCategoryEntity; + if (Objects.isNull(categoryRegisterRequest.getParentCategory())) { + parentCategoryEntity = null; + } else { + parentCategoryEntity = categoryRepository.findById( + categoryRegisterRequest.getParentCategory()) + .orElseThrow( + () -> new CategoryNotExistsException("Parent category not found")); + } + + Category category = new Category(parentCategoryEntity, categoryRegisterRequest.getName()); + Category saveCategory = categoryRepository.save(category); + + return categoryMapper.mapToCategoryResponse(saveCategory); + } + + + public CategoryResponse getCategory(Long id) { + Category category = categoryRepository.findById(id) + .orElseThrow( + () -> new CategoryNotExistsException("category not found")); + + return categoryMapper.mapToCategoryResponse(category); + } + + + public List getChildrenCategories(Long id) { + List childrenCategories = categoryRepository.findAllByParentCategoryId(id); + + return categoryMapper.mapToCategoryResponses(childrenCategories); + } + + public List getParentCategories() { + List parentCategories = categoryRepository.findAllByParentCategoryId(null); + List categoryGetResponses = new ArrayList<>(); + + for (Category category : parentCategories) { + List childrenCategories = getChildrenCategories(category.getId()); + categoryGetResponses.add( + new CategoryGetResponse(category.getId(), category.getName(), childrenCategories)); + } + + return categoryGetResponses; + } + + @Transactional + public CategoryResponse modifyCategory(Long id, CategoryModifyRequest categoryModifyRequest) { + Category parentCategoryEntity; + if (Objects.isNull(categoryModifyRequest.getParentCategory())) { + parentCategoryEntity = null; + } else { + parentCategoryEntity = categoryRepository.findById( + categoryModifyRequest.getParentCategory()) + .orElseThrow( + () -> new CategoryNotExistsException("Parent category not found")); + } + + Category category = categoryRepository.findById(id).orElseThrow(); + + category.setName(categoryModifyRequest.getName()); + category.setParentCategory(parentCategoryEntity); + + return categoryMapper.mapToCategoryResponse(category); + } + +} diff --git a/src/main/java/solitour_backend/solitour/diary/controller/DiaryController.java b/src/main/java/solitour_backend/solitour/diary/controller/DiaryController.java new file mode 100644 index 0000000..5d44b4f --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/controller/DiaryController.java @@ -0,0 +1,74 @@ +package solitour_backend.solitour.diary.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import solitour_backend.solitour.auth.config.Authenticated; +import solitour_backend.solitour.auth.config.AuthenticationPrincipal; +import solitour_backend.solitour.diary.dto.request.DiaryCreateRequest; +import solitour_backend.solitour.diary.dto.request.DiaryUpdateRequest; +import solitour_backend.solitour.diary.dto.response.DiaryContent; +import solitour_backend.solitour.diary.dto.response.DiaryResponse; +import solitour_backend.solitour.diary.service.DiaryService; + +@Authenticated +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/diary") +public class DiaryController { + + private final DiaryService diaryService; + public static final int PAGE_SIZE = 6; + + @GetMapping() + public ResponseEntity> getAllDiary(@RequestParam(defaultValue = "0") int page, + @AuthenticationPrincipal Long userId) { + Pageable pageable = PageRequest.of(page, PAGE_SIZE); + + Page response = diaryService.getAllDiary(pageable, userId); + + return ResponseEntity.ok(response); + } + + @GetMapping("/{diaryId}") + public ResponseEntity getDiary(@AuthenticationPrincipal Long userId, @PathVariable Long diaryId) { + DiaryResponse response = diaryService.getDiary(userId, diaryId); + + return ResponseEntity.ok(response); + } + + @PostMapping() + public ResponseEntity createDiary(@AuthenticationPrincipal Long userId, + @RequestBody DiaryCreateRequest request) { + Long diaryId = diaryService.createDiary(userId, request); + + return ResponseEntity.status(HttpStatus.CREATED).body(diaryId); + } + + @PutMapping("/{diaryId}") + public ResponseEntity updateDiary(@AuthenticationPrincipal Long userId, @PathVariable Long diaryId, + @RequestBody DiaryUpdateRequest request) { + diaryService.updateDiary(userId, diaryId, request); + + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/{diaryId}") + public ResponseEntity deleteDiary(@AuthenticationPrincipal Long userId, @PathVariable Long diaryId) { + diaryService.deleteDiary(userId, diaryId); + + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/solitour_backend/solitour/diary/diary_day_content/DiaryDayContent.java b/src/main/java/solitour_backend/solitour/diary/diary_day_content/DiaryDayContent.java new file mode 100644 index 0000000..3e32727 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/diary_day_content/DiaryDayContent.java @@ -0,0 +1,67 @@ +package solitour_backend.solitour.diary.diary_day_content; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +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 jakarta.persistence.Table; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import solitour_backend.solitour.diary.entity.Diary; +import solitour_backend.solitour.diary.feeling_status.FeelingStatus; +import solitour_backend.solitour.diary.feeling_status.FeelingStatusConverter; + +@Entity +@Getter +@Table(name = "diary_day_content") +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class DiaryDayContent { + + @Id + @Column(name = "diary_day_content_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "diary_id") + private Diary diary; + + @Column(name = "diary_day_content_place") + private String place; + + @Column(name = "diary_day_content_image") + private String contentImage; + + @Column(columnDefinition = "LONGTEXT", name = "diary_day_content_content") + private String content; + + @Column(name = "diary_day_content_feeling_status") + @Convert(converter = FeelingStatusConverter.class) + private FeelingStatus feelingStatus; + + public List getDiaryDayContentImagesList() { + if (contentImage == null || contentImage.isEmpty()) { + return new ArrayList<>(); + } + return Arrays.asList(contentImage.split(",")); + } + + public void setDiaryDayContentImagesList(List imageUrls) { + this.contentImage = String.join(",", imageUrls); + } +} diff --git a/src/main/java/solitour_backend/solitour/diary/dto/request/DiaryCreateRequest.java b/src/main/java/solitour_backend/solitour/diary/dto/request/DiaryCreateRequest.java new file mode 100644 index 0000000..1debd1d --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/dto/request/DiaryCreateRequest.java @@ -0,0 +1,25 @@ +package solitour_backend.solitour.diary.dto.request; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class DiaryCreateRequest { + private String title; + private String titleImage; + private LocalDateTime startDatetime; + private LocalDateTime endDatetime; + private List diaryDayRequests; + + @Getter + @AllArgsConstructor + public static class DiaryDayRequest { + private String content; + private String feelingStatus; + private String diaryDayContentImages; + private String place; + } +} diff --git a/src/main/java/solitour_backend/solitour/diary/dto/request/DiaryUpdateRequest.java b/src/main/java/solitour_backend/solitour/diary/dto/request/DiaryUpdateRequest.java new file mode 100644 index 0000000..d7abd56 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/dto/request/DiaryUpdateRequest.java @@ -0,0 +1,38 @@ +package solitour_backend.solitour.diary.dto.request; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class DiaryUpdateRequest { + private String title; + private String deleteTitleImage; + private String saveTitleImage; + private LocalDateTime startDatetime; + private LocalDateTime endDatetime; + private List diaryDayRequests; + + @Getter + @AllArgsConstructor + public static class DiaryUpdateDayRequest { + private String content; + private String feelingStatus; + private String deleteImagesUrl; + private String saveImagesUrl; + private String place; + + public List getSplitImageUrl(String urlList) { + if (urlList == null || urlList.isEmpty()) { + return new ArrayList<>(); + } + return Arrays.asList(urlList.split(",")); + } + } + + +} diff --git a/src/main/java/solitour_backend/solitour/diary/dto/response/DiaryContent.java b/src/main/java/solitour_backend/solitour/diary/dto/response/DiaryContent.java new file mode 100644 index 0000000..ca3aa55 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/dto/response/DiaryContent.java @@ -0,0 +1,50 @@ +package solitour_backend.solitour.diary.dto.response; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import solitour_backend.solitour.diary.diary_day_content.DiaryDayContent; +import solitour_backend.solitour.diary.entity.Diary; + +@Getter +@Builder +@AllArgsConstructor +public class DiaryContent { + private Long diaryId; + private String title; + private String titleImage; + private LocalDateTime startDatetime; + private LocalDateTime endDatetime; + private DiaryDayContentResponse diaryDayContentResponses; + + public static DiaryContent from(Diary diary) { + return DiaryContent.builder() + .diaryId(diary.getId()) + .title(diary.getTitle()) + .titleImage(diary.getTitleImage()) + .startDatetime(diary.getStartDatetime()) + .endDatetime(diary.getEndDatetime()) + .diaryDayContentResponses(new DiaryDayContentResponse(diary.getDiaryDayContent())) + .build(); + } + + @Getter + public static class DiaryDayContentResponse { + private final List diaryDayContentDetail; + + public DiaryDayContentResponse(List diaryDayContent) { + this.diaryDayContentDetail = diaryDayContent.stream() + .map(diaryDayContentDetail -> + new DiaryDayContentDetail( + diaryDayContentDetail.getContent(), + diaryDayContentDetail.getFeelingStatus().name(), + diaryDayContentDetail.getPlace(), + diaryDayContentDetail.getContentImage() + ) + ).collect(Collectors.toList()); + } + } +} diff --git a/src/main/java/solitour_backend/solitour/diary/dto/response/DiaryDayContentDetail.java b/src/main/java/solitour_backend/solitour/diary/dto/response/DiaryDayContentDetail.java new file mode 100644 index 0000000..9dc25c9 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/dto/response/DiaryDayContentDetail.java @@ -0,0 +1,15 @@ +package solitour_backend.solitour.diary.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class DiaryDayContentDetail { + private String content; + private String feelingStatus; + private String place; + private String contentImage; +} diff --git a/src/main/java/solitour_backend/solitour/diary/dto/response/DiaryListResponse.java b/src/main/java/solitour_backend/solitour/diary/dto/response/DiaryListResponse.java new file mode 100644 index 0000000..b3fe9ef --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/dto/response/DiaryListResponse.java @@ -0,0 +1,66 @@ +package solitour_backend.solitour.diary.dto.response; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; +import lombok.Builder; +import lombok.Getter; +import solitour_backend.solitour.diary.diary_day_content.DiaryDayContent; +import solitour_backend.solitour.diary.entity.Diary; + +@Getter +public class DiaryListResponse { + private final List diaryContentResponse; + + public DiaryListResponse(List> diaries) { + + diaryContentResponse = diaries.stream().flatMap(List::stream).map( + diary -> DiaryContent.builder() + .diaryId(diary.getId()) + .title(diary.getTitle()) + .titleImage(diary.getTitleImage()) + .startDatetime(diary.getStartDatetime()) + .endDatetime(diary.getEndDatetime()) + .diaryDayContentResponses(new DiaryDayContentResponse(diary.getDiaryDayContent())).build() + ).collect(Collectors.toList()); + + } + + @Getter + @Builder + private static class DiaryContent { + private Long diaryId; + private String title; + private String titleImage; + private LocalDateTime startDatetime; + private LocalDateTime endDatetime; + private DiaryDayContentResponse diaryDayContentResponses; + } + + @Getter + private static class DiaryDayContentResponse { + + private final List diaryDayContentDetail; + + private DiaryDayContentResponse(List diaryDayContent) { + this.diaryDayContentDetail = diaryDayContent.stream() + .map(diaryDayContentDetail -> + DiaryDayContentDetail.builder() + .content(diaryDayContentDetail.getContent()) + .feelingStatus(diaryDayContentDetail.getFeelingStatus().name()) + .place(diaryDayContentDetail.getPlace()) + .build() + ).collect(Collectors.toList()); + } + + } + + @Getter + @Builder + private static class DiaryDayContentDetail { + private String content; + private String feelingStatus; + private String place; + } + +} diff --git a/src/main/java/solitour_backend/solitour/diary/dto/response/DiaryResponse.java b/src/main/java/solitour_backend/solitour/diary/dto/response/DiaryResponse.java new file mode 100644 index 0000000..2620e7d --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/dto/response/DiaryResponse.java @@ -0,0 +1,66 @@ +package solitour_backend.solitour.diary.dto.response; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; +import lombok.Builder; +import lombok.Getter; +import solitour_backend.solitour.diary.diary_day_content.DiaryDayContent; +import solitour_backend.solitour.diary.entity.Diary; + +@Getter +public class DiaryResponse { + private final DiaryContent diaryContentResponse; + + public DiaryResponse(Diary diary) { + + diaryContentResponse = DiaryContent.builder() + .diaryId(diary.getId()) + .title(diary.getTitle()) + .titleImage(diary.getTitleImage()) + .startDatetime(diary.getStartDatetime()) + .endDatetime(diary.getEndDatetime()) + .diaryDayContentResponses(new DiaryDayContentResponse(diary.getDiaryDayContent())) + .build(); + } + + @Getter + @Builder + private static class DiaryContent { + private Long diaryId; + private String title; + private String titleImage; + private LocalDateTime startDatetime; + private LocalDateTime endDatetime; + private DiaryDayContentResponse diaryDayContentResponses; + } + + @Getter + private static class DiaryDayContentResponse { + + private final List diaryDayContentDetail; + + private DiaryDayContentResponse(List diaryDayContent) { + this.diaryDayContentDetail = diaryDayContent.stream() + .map(diaryDayContentDetail -> + DiaryDayContentDetail.builder() + .content(diaryDayContentDetail.getContent()) + .feelingStatus(diaryDayContentDetail.getFeelingStatus().name()) + .place(diaryDayContentDetail.getPlace()) + .diaryDayContentImages(diaryDayContentDetail.getContentImage()) + .build() + ).collect(Collectors.toList()); + } + + } + + @Getter + @Builder + private static class DiaryDayContentDetail { + private String content; + private String feelingStatus; + private String place; + private String diaryDayContentImages; + } + +} diff --git a/src/main/java/solitour_backend/solitour/diary/entity/Diary.java b/src/main/java/solitour_backend/solitour/diary/entity/Diary.java new file mode 100644 index 0000000..c33909c --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/entity/Diary.java @@ -0,0 +1,76 @@ +package solitour_backend.solitour.diary.entity; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +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 jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import solitour_backend.solitour.diary.diary_day_content.DiaryDayContent; +import solitour_backend.solitour.diary.dto.request.DiaryUpdateRequest; +import solitour_backend.solitour.user.entity.User; + + +@Entity +@Getter +@Table(name = "diary") +@NoArgsConstructor +@Builder +@AllArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class Diary { + + @Id + @Column(name = "diary_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column(name = "diary_title") + private String title; + + @Column(name = "diary_title_image") + private String titleImage; + + @Column(name = "diary_start_date") + private LocalDateTime startDatetime; + + @Column(name = "diary_end_date") + private LocalDateTime endDatetime; + + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "diary") + private List diaryDayContent; + + @CreatedDate + @Column(name = "diary_created_date") + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "diary_edited_date") + private LocalDateTime editedAt; + + public void updateDiary(DiaryUpdateRequest request) { + this.title = request.getTitle(); + this.titleImage = request.getSaveTitleImage(); + this.startDatetime = request.getStartDatetime(); + this.endDatetime = request.getEndDatetime(); + } +} diff --git a/src/main/java/solitour_backend/solitour/diary/exception/DiaryNotExistsException.java b/src/main/java/solitour_backend/solitour/diary/exception/DiaryNotExistsException.java new file mode 100644 index 0000000..09c94ef --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/exception/DiaryNotExistsException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.diary.exception; + +public class DiaryNotExistsException extends RuntimeException { + public DiaryNotExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/diary/feeling_status/FeelingStatus.java b/src/main/java/solitour_backend/solitour/diary/feeling_status/FeelingStatus.java new file mode 100644 index 0000000..a830738 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/feeling_status/FeelingStatus.java @@ -0,0 +1,26 @@ +package solitour_backend.solitour.diary.feeling_status; + +import java.util.Arrays; +import lombok.Getter; + +@Getter +public enum FeelingStatus { + EXCITED("최고"), + NICE("좋아"), + SOSO("무난"), + SAD("슬퍼"), + ANGRY("화나"); + + private final String status; + + FeelingStatus(String status) { + this.status = status; + } + + public static FeelingStatus fromName(String status) { + return Arrays.stream(FeelingStatus.values()) + .filter(e -> e.getStatus().equals(status)) + .findAny() + .orElse(null); + } +} diff --git a/src/main/java/solitour_backend/solitour/diary/feeling_status/FeelingStatusConverter.java b/src/main/java/solitour_backend/solitour/diary/feeling_status/FeelingStatusConverter.java new file mode 100644 index 0000000..195d5cc --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/feeling_status/FeelingStatusConverter.java @@ -0,0 +1,18 @@ +package solitour_backend.solitour.diary.feeling_status; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class FeelingStatusConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(FeelingStatus userStatus) { + return userStatus.getStatus(); + } + + @Override + public FeelingStatus convertToEntityAttribute(String dbData) { + return FeelingStatus.fromName(dbData); + } +} diff --git a/src/main/java/solitour_backend/solitour/diary/repository/DiaryDayContentRepository.java b/src/main/java/solitour_backend/solitour/diary/repository/DiaryDayContentRepository.java new file mode 100644 index 0000000..d006c10 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/repository/DiaryDayContentRepository.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.diary.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import solitour_backend.solitour.diary.diary_day_content.DiaryDayContent; + +public interface DiaryDayContentRepository extends JpaRepository { +} diff --git a/src/main/java/solitour_backend/solitour/diary/repository/DiaryRepository.java b/src/main/java/solitour_backend/solitour/diary/repository/DiaryRepository.java new file mode 100644 index 0000000..3e0fc38 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/repository/DiaryRepository.java @@ -0,0 +1,14 @@ +package solitour_backend.solitour.diary.repository; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import solitour_backend.solitour.diary.entity.Diary; + +public interface DiaryRepository extends JpaRepository, DiaryRepositoryCustom { + @Query("SELECT d FROM Diary d WHERE d.user.id = :userId") + List findByUserId(Long userId); + + @Query("DELETE FROM Diary d WHERE d.user.id = :userId") + void deleteByUserId(Long userId); +} diff --git a/src/main/java/solitour_backend/solitour/diary/repository/DiaryRepositoryCustom.java b/src/main/java/solitour_backend/solitour/diary/repository/DiaryRepositoryCustom.java new file mode 100644 index 0000000..f6fefa9 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/repository/DiaryRepositoryCustom.java @@ -0,0 +1,11 @@ +package solitour_backend.solitour.diary.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.NoRepositoryBean; +import solitour_backend.solitour.diary.dto.response.DiaryContent; + +@NoRepositoryBean +public interface DiaryRepositoryCustom { + Page getAllDiaryPageFilterAndOrder(Pageable pageable, Long userId); +} diff --git a/src/main/java/solitour_backend/solitour/diary/repository/DiaryRepositoryImpl.java b/src/main/java/solitour_backend/solitour/diary/repository/DiaryRepositoryImpl.java new file mode 100644 index 0000000..600608f --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/repository/DiaryRepositoryImpl.java @@ -0,0 +1,81 @@ +package solitour_backend.solitour.diary.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.annotation.PostConstruct; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; +import solitour_backend.solitour.diary.diary_day_content.QDiaryDayContent; +import solitour_backend.solitour.diary.dto.response.DiaryContent; +import solitour_backend.solitour.diary.dto.response.DiaryContent.DiaryDayContentResponse; +import solitour_backend.solitour.diary.entity.Diary; +import solitour_backend.solitour.diary.entity.QDiary; +import solitour_backend.solitour.diary.exception.DiaryNotExistsException; +import solitour_backend.solitour.user.entity.QUser; + +public class DiaryRepositoryImpl extends QuerydslRepositorySupport implements DiaryRepositoryCustom { + + @PersistenceContext + private EntityManager entityManager; + + private JPAQueryFactory queryFactory; + + public DiaryRepositoryImpl() { + super(Diary.class); + } + + @PostConstruct + private void init() { + this.queryFactory = new JPAQueryFactory(entityManager); + } + + + QDiary diary = QDiary.diary; + QDiaryDayContent diaryDayContent = QDiaryDayContent.diaryDayContent; + QUser user = QUser.user; + + @Override + public Page getAllDiaryPageFilterAndOrder(Pageable pageable, Long userId) { + + List diaries = queryFactory + .selectFrom(diary) + .distinct() + .join(diary.diaryDayContent, diaryDayContent) + .join(diary.user, user) + .where(diary.user.id.eq(userId)) + .orderBy(diary.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + if (diaries == null) { + throw new DiaryNotExistsException("해당 일기가 존재하지 않습니다."); + } + + List diaryContents = diaries.stream() + .map(diary -> DiaryContent.builder() + .diaryId(diary.getId()) + .title(diary.getTitle()) + .titleImage(diary.getTitleImage()) + .startDatetime(diary.getStartDatetime()) + .endDatetime(diary.getEndDatetime()) + .diaryDayContentResponses(new DiaryDayContentResponse( + diary.getDiaryDayContent() + )) + .build()).collect(Collectors.toList()); + + long totalCount = queryFactory.selectFrom(diary) + .distinct() + .join(diary.diaryDayContent, diaryDayContent) + .join(diary.user, user) + .where(diary.user.id.eq(userId)) + .fetchCount(); + + return new PageImpl<>(diaryContents, pageable, totalCount); + } +} diff --git a/src/main/java/solitour_backend/solitour/diary/service/DiaryService.java b/src/main/java/solitour_backend/solitour/diary/service/DiaryService.java new file mode 100644 index 0000000..7c90983 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/diary/service/DiaryService.java @@ -0,0 +1,161 @@ +package solitour_backend.solitour.diary.service; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import solitour_backend.solitour.diary.diary_day_content.DiaryDayContent; +import solitour_backend.solitour.diary.dto.request.DiaryCreateRequest; +import solitour_backend.solitour.diary.dto.request.DiaryCreateRequest.DiaryDayRequest; +import solitour_backend.solitour.diary.dto.request.DiaryUpdateRequest; +import solitour_backend.solitour.diary.dto.request.DiaryUpdateRequest.DiaryUpdateDayRequest; +import solitour_backend.solitour.diary.dto.response.DiaryContent; +import solitour_backend.solitour.diary.dto.response.DiaryResponse; +import solitour_backend.solitour.diary.entity.Diary; +import solitour_backend.solitour.diary.exception.DiaryNotExistsException; +import solitour_backend.solitour.diary.feeling_status.FeelingStatus; +import solitour_backend.solitour.diary.repository.DiaryDayContentRepository; +import solitour_backend.solitour.diary.repository.DiaryRepository; +import solitour_backend.solitour.error.exception.ForbiddenAccessException; +import solitour_backend.solitour.image.s3.S3Uploader; +import solitour_backend.solitour.user.entity.User; +import solitour_backend.solitour.user.repository.UserRepository; + + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class DiaryService { + private final DiaryRepository diaryRepository; + private final DiaryDayContentRepository diaryDayContentRepository; + private final UserRepository userRepository; + private final S3Uploader s3Uploader; + + @Transactional + public Long createDiary(Long userId, DiaryCreateRequest request) { + User user = userRepository.findByUserId(userId); + Diary diary = Diary.builder() + .user(user) + .title(request.getTitle()) + .titleImage(request.getTitleImage()) + .startDatetime(request.getStartDatetime()) + .endDatetime(request.getEndDatetime()) + .createdAt(LocalDateTime.now()) + .build(); + + Diary savedDiary = diaryRepository.save(diary); + + saveDiaryDayContent(savedDiary, request); + + return savedDiary.getId(); + } + + public Page getAllDiary(Pageable pageable, Long userId) { + return diaryRepository.getAllDiaryPageFilterAndOrder(pageable, userId); + } + + public DiaryResponse getDiary(Long userId, Long diaryId) { + Diary diary = diaryRepository.findById(diaryId) + .orElseThrow(() -> new DiaryNotExistsException("해당하는 일기가 존재하지 않습니다.")); + + if (!diary.getUser().getId().equals(userId)) { + throw new ForbiddenAccessException("해당 일기에 대한 권한이 없습니다."); + } + + return new DiaryResponse(diary); + } + + @Transactional + public void deleteDiary(Long userId, Long diaryId) { + Diary diary = diaryRepository.findById(diaryId) + .orElseThrow(() -> new DiaryNotExistsException("해당 일기가 존재하지 않습니다.")); + + if (!diary.getUser().getId().equals(userId)) { + throw new ForbiddenAccessException("해당 일기에 대한 권한이 없습니다."); + } + + deleteAllDiaryImage(diary.getDiaryDayContent()); + diaryRepository.deleteById(diaryId); + } + + @Transactional + public void updateDiary(Long userId, Long diaryId, DiaryUpdateRequest request) { + Diary diary = diaryRepository.findById(diaryId) + .orElseThrow(() -> new DiaryNotExistsException("해당 일기가 존재하지 않습니다.")); + + if (!diary.getUser().getId().equals(userId)) { + throw new ForbiddenAccessException("해당 일기에 대한 권한이 없습니다."); + } + + updateDiary(diaryId, request); + } + + private void updateDiary(Long diaryId, DiaryUpdateRequest request) { + Diary diary = diaryRepository.findById(diaryId) + .orElseThrow(() -> new DiaryNotExistsException("해당 일기가 존재하지 않습니다.")); + deleteDiaryImage(request); + diary.getDiaryDayContent().clear(); + diary.updateDiary(request); + updateDiaryDayContent(diary, request); + } + + private void saveDiaryDayContent(Diary savedDiary, DiaryCreateRequest request) { + for (DiaryDayRequest dayRequest : request.getDiaryDayRequests()) { + makeDiaryImagePermanent(dayRequest.getDiaryDayContentImages()); + DiaryDayContent diaryDayContent = DiaryDayContent.builder() + .diary(savedDiary) + .content(dayRequest.getContent()) + .contentImage(dayRequest.getDiaryDayContentImages()) + .feelingStatus(FeelingStatus.valueOf(dayRequest.getFeelingStatus())) + .place(dayRequest.getPlace()) + .build(); + diaryDayContentRepository.save(diaryDayContent); + } + } + + private void makeDiaryImagePermanent(String diaryDayContentImages) { + if (!diaryDayContentImages.isEmpty()) { + String[] contentImages = diaryDayContentImages.split(","); + for (String contentImage : contentImages) { + s3Uploader.markImagePermanent(contentImage); + } + } + } + + private void updateDiaryDayContent(Diary savedDiary, DiaryUpdateRequest request) { + diaryDayContentRepository.deleteById(savedDiary.getId()); + for (DiaryUpdateDayRequest dayRequest : request.getDiaryDayRequests()) { + DiaryDayContent diaryDayContent = DiaryDayContent.builder() + .diary(savedDiary) + .content(dayRequest.getContent()) + .contentImage(dayRequest.getSaveImagesUrl()) + .feelingStatus(FeelingStatus.valueOf(dayRequest.getFeelingStatus())) + .place(dayRequest.getPlace()) + .build(); + diaryDayContentRepository.save(diaryDayContent); + } + } + + private void deleteDiaryImage(DiaryUpdateRequest request) { + if (request.getDeleteTitleImage() != "") { + s3Uploader.deleteImage(request.getDeleteTitleImage()); + } + + for (DiaryUpdateDayRequest dayRequest : request.getDiaryDayRequests()) { + for (String imageUrl : dayRequest.getSplitImageUrl(dayRequest.getDeleteImagesUrl())) { + s3Uploader.deleteImage(imageUrl); + } + } + } + + private void deleteAllDiaryImage(List diaryDayContent) { + for (DiaryDayContent content : diaryDayContent) { + for (String imageUrl : content.getDiaryDayContentImagesList()) { + s3Uploader.deleteImage(imageUrl); + } + } + } +} diff --git a/src/main/java/solitour_backend/solitour/error/GlobalControllerAdvice.java b/src/main/java/solitour_backend/solitour/error/GlobalControllerAdvice.java new file mode 100644 index 0000000..a877ef3 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/error/GlobalControllerAdvice.java @@ -0,0 +1,126 @@ +package solitour_backend.solitour.error; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import solitour_backend.solitour.auth.exception.TokenNotExistsException; +import solitour_backend.solitour.auth.exception.UnsupportedLoginTypeException; +import solitour_backend.solitour.auth.exception.UserRevokeErrorException; +import solitour_backend.solitour.book_mark_gathering.exception.GatheringBookMarkNotExistsException; +import solitour_backend.solitour.book_mark_information.exception.InformationBookMarkNotExistsException; +import solitour_backend.solitour.category.exception.CategoryNotExistsException; +import solitour_backend.solitour.diary.exception.DiaryNotExistsException; +import solitour_backend.solitour.error.exception.ForbiddenAccessException; +import solitour_backend.solitour.error.exception.RequestValidationFailedException; +import solitour_backend.solitour.gathering.exception.GatheringCategoryNotExistsException; +import solitour_backend.solitour.gathering.exception.GatheringDeleteException; +import solitour_backend.solitour.gathering.exception.GatheringNotExistsException; +import solitour_backend.solitour.gathering_applicants.exception.GatheringApplicantsAlreadyExistsException; +import solitour_backend.solitour.gathering_applicants.exception.GatheringApplicantsAlreadyFullPeopleException; +import solitour_backend.solitour.gathering_applicants.exception.GatheringApplicantsManagerException; +import solitour_backend.solitour.gathering_applicants.exception.GatheringApplicantsNotExistsException; +import solitour_backend.solitour.gathering_applicants.exception.GatheringNotManagerException; +import solitour_backend.solitour.great_gathering.exception.GatheringGreatNotExistsException; +import solitour_backend.solitour.great_information.exception.InformationGreatNotExistsException; +import solitour_backend.solitour.image.exception.ImageAlreadyExistsException; +import solitour_backend.solitour.image.exception.ImageNotExistsException; +import solitour_backend.solitour.image.exception.ImageRequestValidationFailedException; +import solitour_backend.solitour.information.exception.InformationNotExistsException; +import solitour_backend.solitour.information.exception.InformationNotManageException; +import solitour_backend.solitour.user.exception.BlockedUserException; +import solitour_backend.solitour.user.exception.DeletedUserException; +import solitour_backend.solitour.user.exception.DormantUserException; +import solitour_backend.solitour.user.exception.UserNotExistsException; +import solitour_backend.solitour.zone_category.exception.ZoneCategoryAlreadyExistsException; +import solitour_backend.solitour.zone_category.exception.ZoneCategoryNotExistsException; + +@RestControllerAdvice +public class GlobalControllerAdvice { + + @ExceptionHandler({ + RequestValidationFailedException.class, + ImageRequestValidationFailedException.class, + GatheringApplicantsManagerException.class, + InformationNotManageException.class, + UnsupportedLoginTypeException.class + }) + public ResponseEntity validationException(Exception exception) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(exception.getMessage()); + } + + @ExceptionHandler({ + ZoneCategoryAlreadyExistsException.class, + ImageAlreadyExistsException.class, + GatheringApplicantsAlreadyExistsException.class, + GatheringApplicantsAlreadyFullPeopleException.class + }) + public ResponseEntity conflictException(Exception exception) { + return ResponseEntity + .status(HttpStatus.CONFLICT) + .body(exception.getMessage()); + } + + @ExceptionHandler({ + ZoneCategoryNotExistsException.class, + ImageNotExistsException.class, + CategoryNotExistsException.class, + InformationNotExistsException.class, + UserNotExistsException.class, + GatheringCategoryNotExistsException.class, + GatheringNotExistsException.class, + GatheringApplicantsNotExistsException.class, + GatheringDeleteException.class, + InformationGreatNotExistsException.class, + InformationGreatNotExistsException.class, + GatheringGreatNotExistsException.class, + GatheringBookMarkNotExistsException.class, + InformationBookMarkNotExistsException.class, + DiaryNotExistsException.class, + }) + public ResponseEntity notFoundException(Exception exception) { + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(exception.getMessage()); + } + + @ExceptionHandler({GatheringNotManagerException.class, + ForbiddenAccessException.class, + BlockedUserException.class, + DeletedUserException.class + }) + public ResponseEntity forbiddenException(Exception exception) { + return ResponseEntity + .status(HttpStatus.FORBIDDEN) + .body(exception.getMessage()); + } + + @ExceptionHandler({ + DormantUserException.class + }) + public ResponseEntity dormantException(Exception exception) { + return ResponseEntity + .status(HttpStatus.LOCKED) + .body(exception.getMessage()); + } + + @ExceptionHandler({ + TokenNotExistsException.class + }) + public ResponseEntity unauthorizedException(Exception exception) { + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body(exception.getMessage()); + } + @ExceptionHandler({ + UserRevokeErrorException.class + }) + public ResponseEntity serverErrorException(Exception exception) { + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(exception.getMessage()); + } + +} diff --git a/src/main/java/solitour_backend/solitour/error/Utils.java b/src/main/java/solitour_backend/solitour/error/Utils.java new file mode 100644 index 0000000..3947c6c --- /dev/null +++ b/src/main/java/solitour_backend/solitour/error/Utils.java @@ -0,0 +1,16 @@ +package solitour_backend.solitour.error; + +import org.springframework.validation.BindingResult; +import solitour_backend.solitour.error.exception.RequestValidationFailedException; + +public class Utils { + + private Utils() { + } + + public static void validationRequest(BindingResult bindingResult) { + if (bindingResult.hasErrors()) { + throw new RequestValidationFailedException(bindingResult); + } + } +} diff --git a/src/main/java/solitour_backend/solitour/error/exception/ForbiddenAccessException.java b/src/main/java/solitour_backend/solitour/error/exception/ForbiddenAccessException.java new file mode 100644 index 0000000..195b8c3 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/error/exception/ForbiddenAccessException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.error.exception; + +public class ForbiddenAccessException extends RuntimeException { + public ForbiddenAccessException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/error/exception/RequestValidationFailedException.java b/src/main/java/solitour_backend/solitour/error/exception/RequestValidationFailedException.java new file mode 100644 index 0000000..cb5d1fd --- /dev/null +++ b/src/main/java/solitour_backend/solitour/error/exception/RequestValidationFailedException.java @@ -0,0 +1,28 @@ +package solitour_backend.solitour.error.exception; + +import jakarta.validation.ValidationException; +import java.util.stream.Collectors; +import org.springframework.validation.BindingResult; + +public class RequestValidationFailedException extends ValidationException { + + public RequestValidationFailedException(BindingResult bindingResult) { + super(bindingResult.getAllErrors() + .stream() + .map(objectError -> new StringBuilder() + .append("Object: ") + .append(objectError.getObjectName()) + .append("\nMessage: ") + .append(objectError.getDefaultMessage()) + .append("\nError Code: ") + .append(objectError.getCode()) + .append("\n") + .toString()) + .collect(Collectors.joining("\n-----------------------------\n"))); + } + + + public RequestValidationFailedException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering/controller/GatheringController.java b/src/main/java/solitour_backend/solitour/gathering/controller/GatheringController.java new file mode 100644 index 0000000..d6472d9 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/controller/GatheringController.java @@ -0,0 +1,223 @@ +package solitour_backend.solitour.gathering.controller; + +import static solitour_backend.solitour.information.controller.InformationController.PAGE_SIZE; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; + +import java.io.UnsupportedEncodingException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import solitour_backend.solitour.auth.config.AuthenticationPrincipal; +import solitour_backend.solitour.auth.exception.TokenNotExistsException; +import solitour_backend.solitour.auth.exception.TokenNotValidException; +import solitour_backend.solitour.auth.support.CookieExtractor; +import solitour_backend.solitour.auth.support.JwtTokenProvider; +import solitour_backend.solitour.error.Utils; +import solitour_backend.solitour.error.exception.RequestValidationFailedException; +import solitour_backend.solitour.gathering.dto.request.GatheringModifyRequest; +import solitour_backend.solitour.gathering.dto.request.GatheringPageRequest; +import solitour_backend.solitour.gathering.dto.request.GatheringRegisterRequest; +import solitour_backend.solitour.gathering.dto.response.GatheringBriefResponse; +import solitour_backend.solitour.gathering.dto.response.GatheringDetailResponse; +import solitour_backend.solitour.gathering.dto.response.GatheringRankResponse; +import solitour_backend.solitour.gathering.dto.response.GatheringResponse; +import solitour_backend.solitour.gathering.service.GatheringService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/gatherings") +public class GatheringController { + private final GatheringService gatheringService; + private final JwtTokenProvider jwtTokenProvider; + + @PostMapping + public ResponseEntity createGathering(@AuthenticationPrincipal Long userId, + @Valid @RequestBody GatheringRegisterRequest gatheringRegisterRequest, + BindingResult bindingResult) { + Utils.validationRequest(bindingResult); + + if (gatheringRegisterRequest.getEndAge() > gatheringRegisterRequest.getStartAge()) { + throw new RequestValidationFailedException("시작 나이 연도가 끝 나이 연도 보다 앞에 있네요"); + } + if (gatheringRegisterRequest.getScheduleStartDate().isAfter(gatheringRegisterRequest.getScheduleEndDate())) { + throw new RequestValidationFailedException("시작 날짜는 종료 날짜보다 앞에 있어야 합니다."); + } + + if (gatheringRegisterRequest.getDeadline().isBefore(LocalDateTime.now())) { + throw new RequestValidationFailedException("마감일은 현재 시간보다 이후여야 합니다."); + } + + GatheringResponse gatheringResponse = gatheringService.registerGathering(userId, gatheringRegisterRequest); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(gatheringResponse); + } + + @GetMapping("/{id}") + public ResponseEntity getGatheringDetail(@PathVariable Long id, + HttpServletRequest request, + HttpServletResponse response) { + Long userId = findUser(request); + GatheringDetailResponse gatheringDetail = gatheringService.getGatheringDetail(userId, id, request, response); + + return ResponseEntity + .status(HttpStatus.OK) + .body(gatheringDetail); + } + + @PutMapping("/{gatheringId}") + public ResponseEntity updateGathering(@AuthenticationPrincipal Long userId, + @PathVariable Long gatheringId, + @Valid @RequestBody GatheringModifyRequest gatheringModifyRequest) { + + if (gatheringModifyRequest.getEndAge() > gatheringModifyRequest.getStartAge()) { + throw new RequestValidationFailedException("시작 나이 연도가 끝 나이 연도 보다 앞에 있네요"); + } + + if (gatheringModifyRequest.getScheduleStartDate().isAfter(gatheringModifyRequest.getScheduleEndDate())) { + throw new RequestValidationFailedException("시작 날짜는 종료 날짜보다 앞에 있어야 합니다."); + } + + if (gatheringModifyRequest.getDeadline().isBefore(LocalDateTime.now())) { + throw new RequestValidationFailedException("마감일은 현재 시간보다 이후여야 합니다."); + } + + GatheringResponse gatheringResponse = gatheringService.modifyGathering(userId, gatheringId, + gatheringModifyRequest); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(gatheringResponse); + } + + @DeleteMapping("/{gatheringId}") + public ResponseEntity deleteGathering(@PathVariable Long gatheringId, HttpServletRequest request) { + Long userId = findUser(request); + + gatheringService.deleteGathering(gatheringId, userId); + + return ResponseEntity + .status(HttpStatus.NO_CONTENT) + .build(); + } + + + @GetMapping + public ResponseEntity> pageGatheringSortAndFilter(@RequestParam(defaultValue = "0") int page, + @Valid @ModelAttribute GatheringPageRequest gatheringPageRequest, + BindingResult bindingResult, + HttpServletRequest request) { + Utils.validationRequest(bindingResult); + Long userId = findUser(request); + + Pageable pageable = PageRequest.of(page, PAGE_SIZE); + Page pageGathering = gatheringService.getPageGathering(pageable, gatheringPageRequest, + userId); + + return ResponseEntity + .status(HttpStatus.OK) + .body(pageGathering); + } + + @GetMapping("/tag/search") + public ResponseEntity> getPageGatheringByTag(@RequestParam(defaultValue = "0") int page, + @Valid @ModelAttribute GatheringPageRequest gatheringPageRequest, + @RequestParam(required = false, name = "tagName") String tag, + BindingResult bindingResult, + HttpServletRequest request) + throws UnsupportedEncodingException { + String decodedValue = java.net.URLDecoder.decode(tag, "UTF-8"); + String filteredTag = decodedValue.replaceAll("[^a-zA-Z0-9가-힣]", ""); + + Utils.validationRequest(bindingResult); + Long userId = findUser(request); + Pageable pageable = PageRequest.of(page, PAGE_SIZE); + Page briefGatheringPage = gatheringService.getPageGatheringByTag(pageable, userId, gatheringPageRequest, filteredTag); + return ResponseEntity + .status(HttpStatus.OK) + .body(briefGatheringPage); + } + + @GetMapping("/ranks") + public ResponseEntity> getGatheringRankOrderByLikes() { + List gatheringRankOrderByLikes = gatheringService.getGatheringRankOrderByLikes(); + + return ResponseEntity + .status(HttpStatus.OK) + .body(gatheringRankOrderByLikes); + } + + @PutMapping("/finish/{gatheringId}") + public ResponseEntity gatheringFinish(@AuthenticationPrincipal Long userId, + @PathVariable Long gatheringId) { + gatheringService.setGatheringFinish(userId, gatheringId); + return ResponseEntity + .status(HttpStatus.NO_CONTENT) + .build(); + } + + @PutMapping("/not-finish/{gatheringId}") + public ResponseEntity gatheringNotFinish(@AuthenticationPrincipal Long userId, + @PathVariable Long gatheringId) { + + gatheringService.setGatheringNotFinish(userId, gatheringId); + + return ResponseEntity + .status(HttpStatus.NO_CONTENT) + .build(); + } + + + @GetMapping("/home") + public ResponseEntity> getHomeGathering(HttpServletRequest request) { + Long userId = findUser(request); + + List gatheringOrderByLikesFilterByCreate3After = gatheringService.getGatheringOrderByLikesFilterByCreate3After( + userId); + + return ResponseEntity + .status(HttpStatus.OK) + .body(gatheringOrderByLikesFilterByCreate3After); + } + + private Long findUser(HttpServletRequest request) { + String token = CookieExtractor.findToken("access_token", request.getCookies()); + + if (Objects.isNull(token)) { + token = CookieExtractor.findToken("refresh_token", request.getCookies()); + } + if (Objects.isNull(token)) { + return (long) 0; + } + + if (jwtTokenProvider.validateTokenNotUsable(token)) { + throw new TokenNotExistsException("토큰이 존재하지 않습니다."); + } + + return jwtTokenProvider.getPayload(token); + } + + +} diff --git a/src/main/java/solitour_backend/solitour/gathering/dto/mapper/GatheringMapper.java b/src/main/java/solitour_backend/solitour/gathering/dto/mapper/GatheringMapper.java new file mode 100644 index 0000000..5737bf8 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/dto/mapper/GatheringMapper.java @@ -0,0 +1,12 @@ +package solitour_backend.solitour.gathering.dto.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import solitour_backend.solitour.gathering.dto.response.GatheringResponse; +import solitour_backend.solitour.gathering.entity.Gathering; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface GatheringMapper { + GatheringResponse mapToGatheringResponse(Gathering gathering); +} + diff --git a/src/main/java/solitour_backend/solitour/gathering/dto/request/GatheringModifyRequest.java b/src/main/java/solitour_backend/solitour/gathering/dto/request/GatheringModifyRequest.java new file mode 100644 index 0000000..ee6c250 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/dto/request/GatheringModifyRequest.java @@ -0,0 +1,76 @@ +package solitour_backend.solitour.gathering.dto.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +import java.time.LocalDateTime; +import java.util.List; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import solitour_backend.solitour.gathering.entity.AllowedSex; +import solitour_backend.solitour.place.dto.request.PlaceModifyRequest; +import solitour_backend.solitour.tag.dto.request.TagRegisterRequest; + +@Getter +@NoArgsConstructor +public class GatheringModifyRequest { + @NotBlank + @Size(min = 1, max = 50) + private String title; + private String content; + + @NotNull + @Min(2) + @Max(10) + private Integer personCount; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime scheduleStartDate; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime scheduleEndDate; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime deadline; + + @NotNull + private AllowedSex allowedSex; + + @NotNull + @Min(20) + private Integer startAge; + + @NotNull + @Min(20) + private Integer endAge; + + @NotNull + private PlaceModifyRequest placeModifyRequest; + + @NotNull + @Min(1) + private Long gatheringCategoryId; + + @NotBlank + @Size(min = 1, max = 20) + private String zoneCategoryNameParent; + + @NotBlank + @Size(min = 1, max = 20) + private String zoneCategoryNameChild; + + private List tagRegisterRequests; + + @NotBlank + @Size(min = 1, max = 255) + private String openChattingUrl; +} diff --git a/src/main/java/solitour_backend/solitour/gathering/dto/request/GatheringNotFinishRequest.java b/src/main/java/solitour_backend/solitour/gathering/dto/request/GatheringNotFinishRequest.java new file mode 100644 index 0000000..b69abb0 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/dto/request/GatheringNotFinishRequest.java @@ -0,0 +1,17 @@ +package solitour_backend.solitour.gathering.dto.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor +public class GatheringNotFinishRequest { + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime deadline; +} diff --git a/src/main/java/solitour_backend/solitour/gathering/dto/request/GatheringPageRequest.java b/src/main/java/solitour_backend/solitour/gathering/dto/request/GatheringPageRequest.java new file mode 100644 index 0000000..bd6deb0 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/dto/request/GatheringPageRequest.java @@ -0,0 +1,45 @@ +package solitour_backend.solitour.gathering.dto.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Min; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.format.annotation.DateTimeFormat; +import solitour_backend.solitour.gathering.entity.AllowedSex; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class GatheringPageRequest { + private Boolean isExclude; + + @Min(1) + private Long category; + + @Min(1) + private Long location; + + private AllowedSex allowedSex; + + @Min(20) + private Integer startAge; + + @Min(20) + private Integer endAge; + + private String sort; + + private String search; + + @JsonFormat(pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate startDate; + + @JsonFormat(pattern = "yyyy-MM-dd") + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate endDate; +} diff --git a/src/main/java/solitour_backend/solitour/gathering/dto/request/GatheringRegisterRequest.java b/src/main/java/solitour_backend/solitour/gathering/dto/request/GatheringRegisterRequest.java new file mode 100644 index 0000000..7e21ba2 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/dto/request/GatheringRegisterRequest.java @@ -0,0 +1,76 @@ +package solitour_backend.solitour.gathering.dto.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +import java.time.LocalDateTime; +import java.util.List; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import solitour_backend.solitour.gathering.entity.AllowedSex; +import solitour_backend.solitour.place.dto.request.PlaceRegisterRequest; +import solitour_backend.solitour.tag.dto.request.TagRegisterRequest; + +@Getter +@NoArgsConstructor +public class GatheringRegisterRequest { + @NotBlank + @Size(min = 1, max = 50) + private String title; + private String content; + + @NotNull + @Min(2) + @Max(10) + private Integer personCount; //몇명으로 제한 할 것인지 + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime scheduleStartDate; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime scheduleEndDate; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime deadline; + + @NotNull + private AllowedSex allowedSex; + + @NotNull + @Min(20) + private Integer startAge; + + @NotNull + @Min(20) + private Integer endAge; + + @NotNull + private PlaceRegisterRequest placeRegisterRequest; + + @NotNull + @Min(1) + private Long gatheringCategoryId; + + @NotBlank + @Size(min = 1, max = 20) + private String zoneCategoryNameParent; + + @NotBlank + @Size(min = 1, max = 20) + private String zoneCategoryNameChild; + + private List tagRegisterRequests; + + @NotBlank + @Size(min = 1, max = 255) + private String openChattingUrl; +} diff --git a/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringApplicantResponse.java b/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringApplicantResponse.java new file mode 100644 index 0000000..2dc2399 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringApplicantResponse.java @@ -0,0 +1,36 @@ +package solitour_backend.solitour.gathering.dto.response; + +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Getter; +import solitour_backend.solitour.gathering.entity.AllowedSex; + +@Getter +@AllArgsConstructor +public class GatheringApplicantResponse { + private Long gatheringId; + private String title; + private String zoneCategoryParentName; + private String zoneCategoryChildName; + private Integer viewCount; + private Boolean isBookMark; + private Integer likeCount; + + private String gatheringCategoryName; + private String userName; + + private LocalDateTime scheduleStartDate; + private LocalDateTime scheduleEndDate; + + private LocalDateTime deadline; + + private AllowedSex allowedSex; + + private Integer startAge; + private Integer endAge; + private Integer personCount; + private Integer nowPersonCount; + private Boolean isLike; + private String gatheringStatus; + private Boolean isFinish; +} diff --git a/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringBriefResponse.java b/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringBriefResponse.java new file mode 100644 index 0000000..0451bbd --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringBriefResponse.java @@ -0,0 +1,35 @@ +package solitour_backend.solitour.gathering.dto.response; + +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Getter; +import solitour_backend.solitour.gathering.entity.AllowedSex; + +@Getter +@AllArgsConstructor +public class GatheringBriefResponse { + private Long gatheringId; + private String title; + private String zoneCategoryParentName; + private String zoneCategoryChildName; + private Integer viewCount; + private Boolean isBookMark; + private Integer likeCount; + + private String gatheringCategoryName; + private String nickname; + + private LocalDateTime scheduleStartDate; + private LocalDateTime scheduleEndDate; + + private LocalDateTime deadline; + + private AllowedSex allowedSex; + + private Integer startAge; + private Integer endAge; + private Integer personCount; + private Integer nowPersonCount; + private Boolean isLike; + +} diff --git a/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringDetailResponse.java b/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringDetailResponse.java new file mode 100644 index 0000000..6aaeb5f --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringDetailResponse.java @@ -0,0 +1,56 @@ +package solitour_backend.solitour.gathering.dto.response; + +import java.time.LocalDateTime; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import solitour_backend.solitour.gathering.entity.AllowedSex; +import solitour_backend.solitour.gathering_applicants.dto.response.GatheringApplicantsResponse; +import solitour_backend.solitour.gathering_applicants.entity.GatheringStatus; +import solitour_backend.solitour.gathering_category.dto.response.GatheringCategoryResponse; +import solitour_backend.solitour.place.dto.response.PlaceResponse; +import solitour_backend.solitour.tag.dto.response.TagResponse; +import solitour_backend.solitour.user.dto.UserPostingResponse; +import solitour_backend.solitour.zone_category.dto.response.ZoneCategoryResponse; + +@Getter +@AllArgsConstructor +public class GatheringDetailResponse { + private String title; + private String content; + private Integer personCount; + private Integer viewCount; + private LocalDateTime createdAt; + + private LocalDateTime scheduleStartDate; + private LocalDateTime scheduleEndDate; + private LocalDateTime deadline; + private Boolean isFinish; + + private AllowedSex allowedSex; + private Integer startAge; + private Integer endAge; + + private List tagResponses; + + private UserPostingResponse userPostingResponse; + private PlaceResponse placeResponse; + private ZoneCategoryResponse zoneCategoryResponse; + private GatheringCategoryResponse gatheringCategoryResponse; + + private Integer likeCount; + private Integer nowPersonCount; + + private Boolean isLike; + + private String openChattingUrl; + + private String userImage; + + private List gatheringApplicantsResponses; + + private List gatheringRecommend; + + private GatheringStatus gatheringStatus; +} diff --git a/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringMypageResponse.java b/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringMypageResponse.java new file mode 100644 index 0000000..ae28a0e --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringMypageResponse.java @@ -0,0 +1,35 @@ +package solitour_backend.solitour.gathering.dto.response; + +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Getter; +import solitour_backend.solitour.gathering.entity.AllowedSex; + +@Getter +@AllArgsConstructor +public class GatheringMypageResponse { + private Long gatheringId; + private String title; + private String zoneCategoryParentName; + private String zoneCategoryChildName; + private Integer viewCount; + private Boolean isBookMark; + private Integer likeCount; + + private String gatheringCategoryName; + private String userName; + + private LocalDateTime scheduleStartDate; + private LocalDateTime scheduleEndDate; + + private LocalDateTime deadline; + + private AllowedSex allowedSex; + + private Integer startAge; + private Integer endAge; + private Integer personCount; + private Integer nowPersonCount; + private Boolean isLike; + private Boolean isFinish; +} diff --git a/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringRankResponse.java b/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringRankResponse.java new file mode 100644 index 0000000..00a54ba --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringRankResponse.java @@ -0,0 +1,11 @@ +package solitour_backend.solitour.gathering.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GatheringRankResponse { + private Long id; + private String title; +} diff --git a/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringResponse.java b/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringResponse.java new file mode 100644 index 0000000..30a501e --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/dto/response/GatheringResponse.java @@ -0,0 +1,10 @@ +package solitour_backend.solitour.gathering.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GatheringResponse { + private Long id; +} diff --git a/src/main/java/solitour_backend/solitour/gathering/entity/AllowedSex.java b/src/main/java/solitour_backend/solitour/gathering/entity/AllowedSex.java new file mode 100644 index 0000000..146ab72 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/entity/AllowedSex.java @@ -0,0 +1,25 @@ +package solitour_backend.solitour.gathering.entity; + +import java.util.Arrays; + +import lombok.Getter; + +@Getter +public enum AllowedSex { + MALE("male"), + FEMALE("female"), + ALL("all"); + + private final String name; + + AllowedSex(String name) { + this.name = name; + } + + public static AllowedSex fromName(String name) { + return Arrays.stream(AllowedSex.values()) + .filter(e -> e.name.equals(name)) + .findAny() + .orElseThrow(RuntimeException::new); + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering/entity/AllowedSexConverter.java b/src/main/java/solitour_backend/solitour/gathering/entity/AllowedSexConverter.java new file mode 100644 index 0000000..bb3f17d --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/entity/AllowedSexConverter.java @@ -0,0 +1,17 @@ +package solitour_backend.solitour.gathering.entity; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class AllowedSexConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(AllowedSex allowedSex) { + return allowedSex.getName(); + } + + @Override + public AllowedSex convertToEntityAttribute(String dbData) { + return AllowedSex.fromName(dbData); + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering/entity/Gathering.java b/src/main/java/solitour_backend/solitour/gathering/entity/Gathering.java new file mode 100644 index 0000000..63983b3 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/entity/Gathering.java @@ -0,0 +1,131 @@ +package solitour_backend.solitour.gathering.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +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 jakarta.persistence.Table; + +import java.time.LocalDateTime; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import solitour_backend.solitour.gathering_category.entity.GatheringCategory; +import solitour_backend.solitour.place.entity.Place; +import solitour_backend.solitour.user.entity.User; +import solitour_backend.solitour.zone_category.entity.ZoneCategory; + +@Entity +@Getter +@Setter +@Table(name = "gathering") +@NoArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class Gathering { + + @Id + @Column(name = "gathering_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "zone_category_id") + private ZoneCategory zoneCategory; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "gathering_category_id") + private GatheringCategory gatheringCategory; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "place_id") + private Place place; + + @Column(name = "gathering_title") + private String title; + + @Column(name = "gathering_content") + private String content; + + @Column(name = "gathering_person_count") + private Integer personCount; + + @Column(name = "gathering_view_count") + private Integer viewCount; + + @CreatedDate + @Column(name = "gathering_created_at") + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "gathering_edited_at") + private LocalDateTime editedAt; + + @Column(name = "gathering_schedule_start_date") + private LocalDateTime scheduleStartDate; + + @Column(name = "gathering_schedule_end_date") + private LocalDateTime scheduleEndDate; + + @Column(name = "gathering_is_finish") + private Boolean isFinish; + + @Column(name = "gathering_deadline") + private LocalDateTime deadline; + + @Column(name = "gathering_allowed_sex") + @Convert(converter = AllowedSexConverter.class) + private AllowedSex allowedSex; + + @Column(name = "gathering_start_age") + private Integer startAge; + + @Column(name = "gathering_end_age") + private Integer endAge; + + @Column(name = "gathering_is_deleted") + private Boolean isDeleted; + + @Column(name = "gathering_open_chatting_url") + private String openChattingUrl; + + public Gathering(User user, ZoneCategory zoneCategory, GatheringCategory gatheringCategory, Place place, + String title, String content, Integer personCount, Integer viewCount, + LocalDateTime scheduleStartDate, LocalDateTime scheduleEndDate, Boolean isFinish, + LocalDateTime deadline, AllowedSex allowedSex, Integer startAge, Integer endAge, String openChattingUrl) { + this.user = user; + this.zoneCategory = zoneCategory; + this.gatheringCategory = gatheringCategory; + this.place = place; + this.title = title; + this.content = content; + this.personCount = personCount; + this.viewCount = viewCount; + this.scheduleStartDate = scheduleStartDate; + this.scheduleEndDate = scheduleEndDate; + this.isFinish = isFinish; + this.deadline = deadline; + this.allowedSex = allowedSex; + this.startAge = startAge; + this.endAge = endAge; + this.isDeleted = false; + this.openChattingUrl = openChattingUrl; + } + + public void upViewCount() { + this.viewCount++; + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering/exception/GatheringCategoryNotExistsException.java b/src/main/java/solitour_backend/solitour/gathering/exception/GatheringCategoryNotExistsException.java new file mode 100644 index 0000000..2fd7a39 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/exception/GatheringCategoryNotExistsException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.gathering.exception; + +public class GatheringCategoryNotExistsException extends RuntimeException { + public GatheringCategoryNotExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering/exception/GatheringDeleteException.java b/src/main/java/solitour_backend/solitour/gathering/exception/GatheringDeleteException.java new file mode 100644 index 0000000..4f8dc54 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/exception/GatheringDeleteException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.gathering.exception; + +public class GatheringDeleteException extends RuntimeException { + public GatheringDeleteException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering/exception/GatheringFinishConflictException.java b/src/main/java/solitour_backend/solitour/gathering/exception/GatheringFinishConflictException.java new file mode 100644 index 0000000..17aed37 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/exception/GatheringFinishConflictException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.gathering.exception; + +public class GatheringFinishConflictException extends RuntimeException { + public GatheringFinishConflictException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering/exception/GatheringNotExistsException.java b/src/main/java/solitour_backend/solitour/gathering/exception/GatheringNotExistsException.java new file mode 100644 index 0000000..5d2aa1c --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/exception/GatheringNotExistsException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.gathering.exception; + +public class GatheringNotExistsException extends RuntimeException { + public GatheringNotExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering/repository/GatheringRepository.java b/src/main/java/solitour_backend/solitour/gathering/repository/GatheringRepository.java new file mode 100644 index 0000000..93c7c22 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/repository/GatheringRepository.java @@ -0,0 +1,9 @@ +package solitour_backend.solitour.gathering.repository; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import solitour_backend.solitour.gathering.entity.Gathering; + +public interface GatheringRepository extends JpaRepository, GatheringRepositoryCustom { + +} diff --git a/src/main/java/solitour_backend/solitour/gathering/repository/GatheringRepositoryCustom.java b/src/main/java/solitour_backend/solitour/gathering/repository/GatheringRepositoryCustom.java new file mode 100644 index 0000000..ee76daa --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/repository/GatheringRepositoryCustom.java @@ -0,0 +1,27 @@ +package solitour_backend.solitour.gathering.repository; + +import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.NoRepositoryBean; +import solitour_backend.solitour.gathering.dto.request.GatheringPageRequest; +import solitour_backend.solitour.gathering.dto.response.GatheringBriefResponse; +import solitour_backend.solitour.gathering.dto.response.GatheringRankResponse; + +@NoRepositoryBean +public interface GatheringRepositoryCustom { + String LIKE_COUNT_SORT = "likes"; + String VIEW_COUNT_SORT = "views"; + + List getGatheringRecommend(Long informationId, Long gatheringCategoryId, Long userId); + + Page getGatheringPageFilterAndOrder(Pageable pageable, + GatheringPageRequest gatheringPageRequest, Long userId); + + List getGatheringRankList(); + + List getGatheringLikeCountFromCreatedIn3(Long userId); + + Page getPageGatheringByTag(Pageable pageable, GatheringPageRequest gatheringPageRequest, + Long userId, String decodedTag); +} diff --git a/src/main/java/solitour_backend/solitour/gathering/repository/GatheringRepositoryImpl.java b/src/main/java/solitour_backend/solitour/gathering/repository/GatheringRepositoryImpl.java new file mode 100644 index 0000000..8d9c710 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/repository/GatheringRepositoryImpl.java @@ -0,0 +1,333 @@ +package solitour_backend.solitour.gathering.repository; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.*; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import java.util.Objects; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; +import solitour_backend.solitour.book_mark_gathering.entity.QBookMarkGathering; +import solitour_backend.solitour.gathering.dto.request.GatheringPageRequest; +import solitour_backend.solitour.gathering.dto.response.GatheringBriefResponse; +import solitour_backend.solitour.gathering.dto.response.GatheringRankResponse; +import solitour_backend.solitour.gathering.entity.Gathering; +import solitour_backend.solitour.gathering.entity.QGathering; +import solitour_backend.solitour.gathering_applicants.entity.GatheringStatus; +import solitour_backend.solitour.gathering_applicants.entity.QGatheringApplicants; +import solitour_backend.solitour.gathering_category.entity.QGatheringCategory; +import solitour_backend.solitour.gathering_tag.entity.QGatheringTag; +import solitour_backend.solitour.great_gathering.entity.QGreatGathering; +import solitour_backend.solitour.zone_category.entity.QZoneCategory; + +public class GatheringRepositoryImpl extends QuerydslRepositorySupport implements GatheringRepositoryCustom { + + public GatheringRepositoryImpl() { + super(Gathering.class); + } + + QGathering gathering = QGathering.gathering; + QZoneCategory zoneCategoryChild = QZoneCategory.zoneCategory; + QZoneCategory zoneCategoryParent = new QZoneCategory("zoneCategoryParent"); + QBookMarkGathering bookMarkGathering = QBookMarkGathering.bookMarkGathering; + QGreatGathering greatGathering = QGreatGathering.greatGathering; + QGatheringCategory category = QGatheringCategory.gatheringCategory; + QGatheringApplicants gatheringApplicants = QGatheringApplicants.gatheringApplicants; + QGatheringTag gatheringTag = QGatheringTag.gatheringTag; + + @Override + public List getGatheringRecommend(Long gatheringId, Long gatheringCategoryId, Long userId) { + return from(gathering) + .join(zoneCategoryChild).on(zoneCategoryChild.id.eq(gathering.zoneCategory.id)) + .leftJoin(zoneCategoryParent).on(zoneCategoryParent.id.eq(zoneCategoryChild.parentZoneCategory.id)) + .where(gathering.isFinish.eq(Boolean.FALSE) + .and(gathering.gatheringCategory.id.eq(gatheringCategoryId)) + .and(gathering.id.ne(gatheringId)) + .and(gathering.isDeleted.eq(Boolean.FALSE)) + .and(gathering.deadline.after(LocalDateTime.now())) + ) + .groupBy(gathering.id, zoneCategoryChild.id, zoneCategoryParent.id) + .orderBy(gathering.createdAt.desc()) + .select(Projections.constructor( + GatheringBriefResponse.class, + gathering.id, + gathering.title, + zoneCategoryParent.name, + zoneCategoryChild.name, + gathering.viewCount, + isGatheringBookmark(userId, gathering.id), + countGreatGatheringByGatheringById(gathering.id), + gathering.gatheringCategory.name, + gathering.user.nickname, + gathering.scheduleStartDate, + gathering.scheduleEndDate, + gathering.deadline, + gathering.allowedSex, + gathering.startAge, + gathering.endAge, + gathering.personCount, + applicantsCountNowConsent(gathering.id), + isUserGreatGathering(userId) + )).limit(3L).fetch(); + } + + + @Override + public Page getGatheringPageFilterAndOrder(Pageable pageable, + GatheringPageRequest gatheringPageRequest, + Long userId) { + BooleanBuilder booleanBuilder = makeWhereSQL(gatheringPageRequest); + + long total = from(gathering) + .where(booleanBuilder) + .select(gathering.id).fetchCount(); + + List content = from(gathering) + .join(zoneCategoryChild).on(zoneCategoryChild.id.eq(gathering.zoneCategory.id)) + .leftJoin(zoneCategoryParent).on(zoneCategoryParent.id.eq(zoneCategoryChild.parentZoneCategory.id)) + .where(booleanBuilder) + .groupBy(gathering.id, zoneCategoryChild.id, zoneCategoryParent.id) + .orderBy(getOrderSpecifier(gatheringPageRequest.getSort(), gathering.id)) + .select(Projections.constructor( + GatheringBriefResponse.class, + gathering.id, + gathering.title, + zoneCategoryParent.name, + zoneCategoryChild.name, + gathering.viewCount, + isGatheringBookmark(userId, gathering.id), + countGreatGatheringByGatheringById(gathering.id), + gathering.gatheringCategory.name, + gathering.user.nickname, + gathering.scheduleStartDate, + gathering.scheduleEndDate, + gathering.deadline, + gathering.allowedSex, + gathering.startAge, + gathering.endAge, + gathering.personCount, + applicantsCountNowConsent(gathering.id), + isUserGreatGathering(userId) + )) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + return new PageImpl<>(content, pageable, total); + } + + @Override + public Page getPageGatheringByTag(Pageable pageable, + GatheringPageRequest gatheringPageRequest, + Long userId, + String decodedTag) { + BooleanBuilder booleanBuilder = makeWhereSQL(gatheringPageRequest); + booleanBuilder.and(gatheringTag.tag.name.eq(decodedTag)); + + long total = from(gathering) + .leftJoin(gatheringTag).on(gatheringTag.gathering.id.eq(gathering.id)) + .where(booleanBuilder) + .select(gathering.id.count()) + .fetchCount(); + + List content = from(gathering) + .join(zoneCategoryChild).on(zoneCategoryChild.id.eq(gathering.zoneCategory.id)) + .leftJoin(zoneCategoryParent).on(zoneCategoryParent.id.eq(zoneCategoryChild.parentZoneCategory.id)) + .leftJoin(gatheringTag).on(gatheringTag.gathering.id.eq(gathering.id)) + .where(booleanBuilder) + .groupBy(gathering.id, zoneCategoryChild.id, zoneCategoryParent.id) + .orderBy(getOrderSpecifier(gatheringPageRequest.getSort(), gathering.id)) + .select(Projections.constructor( + GatheringBriefResponse.class, + gathering.id, + gathering.title, + zoneCategoryParent.name, + zoneCategoryChild.name, + gathering.viewCount, + isGatheringBookmark(userId, gathering.id), + countGreatGatheringByGatheringById(gathering.id), + gathering.gatheringCategory.name, + gathering.user.nickname, + gathering.scheduleStartDate, + gathering.scheduleEndDate, + gathering.deadline, + gathering.allowedSex, + gathering.startAge, + gathering.endAge, + gathering.personCount, + applicantsCountNowConsent(gathering.id), + isUserGreatGathering(userId) + )) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + return new PageImpl<>(content, pageable, total); + } + + @Override + public List getGatheringRankList() { + return from(gathering) + .orderBy(countGreatGatheringByGatheringById(gathering.id).desc()) + .groupBy(gathering.id, gathering.title) + .where(gathering.isFinish.eq(Boolean.FALSE) + .and(gathering.isDeleted.eq(Boolean.FALSE)) + .and(gathering.deadline.after(LocalDateTime.now())) + ) + .limit(5) + .select(Projections.constructor( + GatheringRankResponse.class, + gathering.id, + gathering.title)) + .fetch(); + } + + @Override + public List getGatheringLikeCountFromCreatedIn3(Long userId) { + return from(gathering) + .join(zoneCategoryChild).on(zoneCategoryChild.id.eq(gathering.zoneCategory.id)) + .leftJoin(zoneCategoryParent).on(zoneCategoryParent.id.eq(zoneCategoryChild.parentZoneCategory.id)) + .where(gathering.isFinish.eq(Boolean.FALSE) + .and(gathering.isDeleted.eq(Boolean.FALSE)) + .and(gathering.createdAt.after(LocalDateTime.now().minusMonths(3))) + .and(gathering.deadline.after(LocalDateTime.now()))) + .groupBy(gathering.id, zoneCategoryChild.id, zoneCategoryParent.id) + .orderBy(countGreatGatheringByGatheringById(gathering.id).desc()) + .select(Projections.constructor( + GatheringBriefResponse.class, + gathering.id, + gathering.title, + zoneCategoryParent.name, + zoneCategoryChild.name, + gathering.viewCount, + isGatheringBookmark(userId, gathering.id), + countGreatGatheringByGatheringById(gathering.id), + gathering.gatheringCategory.name, + gathering.user.nickname, + gathering.scheduleStartDate, + gathering.scheduleEndDate, + gathering.deadline, + gathering.allowedSex, + gathering.startAge, + gathering.endAge, + gathering.personCount, + applicantsCountNowConsent(gathering.id), + isUserGreatGathering(userId) + )).limit(6L).fetch(); + } + + //where 절 + private BooleanBuilder makeWhereSQL(GatheringPageRequest gatheringPageRequest) { + BooleanBuilder whereClause = new BooleanBuilder(); + + whereClause.and(gathering.isDeleted.eq(Boolean.FALSE)); + if (Objects.nonNull(gatheringPageRequest.getCategory())) { + whereClause.and(gathering.gatheringCategory.id.eq(gatheringPageRequest.getCategory())); + } + + if (Objects.nonNull(gatheringPageRequest.getLocation())) { + whereClause.and(gathering.zoneCategory.parentZoneCategory.id.eq(gatheringPageRequest.getLocation())); + } + + if (Objects.nonNull(gatheringPageRequest.getAllowedSex())) { + whereClause.and(gathering.allowedSex.eq(gatheringPageRequest.getAllowedSex())); + } + int currentYear = LocalDate.now().getYear(); + + if (Objects.nonNull(gatheringPageRequest.getStartAge()) && Objects.nonNull(gatheringPageRequest.getEndAge())) { + int userMinBirthYear = currentYear - gatheringPageRequest.getEndAge() + 1; + int userMaxBirthYear = currentYear - gatheringPageRequest.getStartAge() + 1; + + whereClause.and(gathering.startAge.goe(userMaxBirthYear)).or(gathering.endAge.loe(userMinBirthYear)); + } + + if (Objects.nonNull(gatheringPageRequest.getStartDate()) && Objects.nonNull(gatheringPageRequest.getEndDate())) { + whereClause.and(gathering.scheduleStartDate.goe(gatheringPageRequest.getStartDate().atStartOfDay())) + .and(gathering.scheduleEndDate.loe(gatheringPageRequest.getEndDate().atTime(LocalTime.MAX))); + } + + if (Objects.isNull(gatheringPageRequest.getIsExclude())) { + whereClause.and(gathering.isFinish.eq(Boolean.FALSE)); + whereClause.and(gathering.deadline.after(LocalDateTime.now())); + } + + if (Objects.nonNull(gatheringPageRequest.getSearch())) { + String searchKeyword = gatheringPageRequest.getSearch().trim(); + whereClause.and(gathering.title.trim().containsIgnoreCase(searchKeyword)); + } + + return whereClause; + } + + // 정렬 방식 + private OrderSpecifier getOrderSpecifier(String sort, NumberPath gatheringId) { + PathBuilder entityPath = new PathBuilder<>(Gathering.class, "gathering"); + + if (Objects.nonNull(sort)) { + if (LIKE_COUNT_SORT.equalsIgnoreCase(sort)) { + return countGreatGatheringByGatheringById(gatheringId).desc(); + } else if (VIEW_COUNT_SORT.equalsIgnoreCase(sort)) { + return entityPath.getNumber("viewCount", Integer.class).desc(); + } + } + + return entityPath.getDateTime("createdAt", LocalDateTime.class).desc(); + } + + // 좋아요 수 가져오는 식 + private NumberExpression countGreatGatheringByGatheringById(NumberPath gatheringId) { + QGreatGathering greatGatheringSub = QGreatGathering.greatGathering; + JPQLQuery likeCountSubQuery = JPAExpressions + .select(greatGatheringSub.count()) + .from(greatGatheringSub) + .where(greatGatheringSub.gathering.id.eq(gatheringId)); + + return Expressions.numberTemplate(Long.class, "{0}", likeCountSubQuery) + .coalesce(0L) + .intValue(); + } + + private BooleanExpression isUserGreatGathering(Long userId) { + return new CaseBuilder() + .when(JPAExpressions.selectOne() + .from(greatGathering) + .where(greatGathering.gathering.id.eq(gathering.id) + .and(greatGathering.user.id.eq(userId))) + .exists()) + .then(true) + .otherwise(false); + } + + private BooleanExpression isGatheringBookmark(Long userId, NumberPath gatheringId) { + return new CaseBuilder() + .when(JPAExpressions.selectOne() + .from(bookMarkGathering) + .where(bookMarkGathering.gathering.id.eq(gatheringId) + .and(bookMarkGathering.user.id.eq(userId))) + .exists()) + .then(true) + .otherwise(false); + } + + private NumberExpression applicantsCountNowConsent(NumberPath gatheringId) { + JPQLQuery applicantsCountSubQuery = JPAExpressions + .select(gatheringApplicants.count().intValue()) + .from(gatheringApplicants) + .where(gatheringApplicants.gathering.id.eq(gatheringId) + .and(gatheringApplicants.gatheringStatus.eq(GatheringStatus.CONSENT))); + + return Expressions.numberTemplate(Integer.class, "{0}", applicantsCountSubQuery) + .coalesce(0); + } + +} diff --git a/src/main/java/solitour_backend/solitour/gathering/service/GatheringService.java b/src/main/java/solitour_backend/solitour/gathering/service/GatheringService.java new file mode 100644 index 0000000..9821334 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering/service/GatheringService.java @@ -0,0 +1,551 @@ +package solitour_backend.solitour.gathering.service; + +import static solitour_backend.solitour.gathering.repository.GatheringRepositoryCustom.LIKE_COUNT_SORT; +import static solitour_backend.solitour.gathering.repository.GatheringRepositoryCustom.VIEW_COUNT_SORT; + +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import solitour_backend.solitour.error.exception.RequestValidationFailedException; +import solitour_backend.solitour.gathering.dto.mapper.GatheringMapper; +import solitour_backend.solitour.gathering.dto.request.GatheringModifyRequest; +import solitour_backend.solitour.gathering.dto.request.GatheringPageRequest; +import solitour_backend.solitour.gathering.dto.request.GatheringRegisterRequest; +import solitour_backend.solitour.gathering.dto.response.GatheringBriefResponse; +import solitour_backend.solitour.gathering.dto.response.GatheringDetailResponse; +import solitour_backend.solitour.gathering.dto.response.GatheringRankResponse; +import solitour_backend.solitour.gathering.dto.response.GatheringResponse; +import solitour_backend.solitour.gathering.entity.Gathering; +import solitour_backend.solitour.gathering.exception.GatheringCategoryNotExistsException; +import solitour_backend.solitour.gathering.exception.GatheringDeleteException; +import solitour_backend.solitour.gathering.exception.GatheringFinishConflictException; +import solitour_backend.solitour.gathering.exception.GatheringNotExistsException; +import solitour_backend.solitour.gathering.repository.GatheringRepository; +import solitour_backend.solitour.gathering_applicants.dto.mapper.GatheringApplicantsMapper; +import solitour_backend.solitour.gathering_applicants.dto.response.GatheringApplicantsResponse; +import solitour_backend.solitour.gathering_applicants.entity.GatheringApplicants; +import solitour_backend.solitour.gathering_applicants.entity.GatheringStatus; +import solitour_backend.solitour.gathering_applicants.exception.GatheringNotManagerException; +import solitour_backend.solitour.gathering_applicants.repository.GatheringApplicantsRepository; +import solitour_backend.solitour.gathering_category.dto.mapper.GatheringCategoryMapper; +import solitour_backend.solitour.gathering_category.dto.response.GatheringCategoryResponse; +import solitour_backend.solitour.gathering_category.entity.GatheringCategory; +import solitour_backend.solitour.gathering_category.repository.GatheringCategoryRepository; +import solitour_backend.solitour.gathering_tag.entity.GatheringTag; +import solitour_backend.solitour.gathering_tag.repository.GatheringTagRepository; +import solitour_backend.solitour.great_gathering.repository.GreatGatheringRepository; +import solitour_backend.solitour.place.dto.mapper.PlaceMapper; +import solitour_backend.solitour.place.dto.request.PlaceModifyRequest; +import solitour_backend.solitour.place.dto.request.PlaceRegisterRequest; +import solitour_backend.solitour.place.dto.response.PlaceResponse; +import solitour_backend.solitour.place.entity.Place; +import solitour_backend.solitour.place.repository.PlaceRepository; +import solitour_backend.solitour.tag.dto.mapper.TagMapper; +import solitour_backend.solitour.tag.dto.response.TagResponse; +import solitour_backend.solitour.tag.entity.Tag; +import solitour_backend.solitour.tag.repository.TagRepository; +import solitour_backend.solitour.user.dto.UserPostingResponse; +import solitour_backend.solitour.user.dto.mapper.UserMapper; +import solitour_backend.solitour.user.entity.User; +import solitour_backend.solitour.user.exception.UserNotExistsException; +import solitour_backend.solitour.user.repository.UserRepository; +import solitour_backend.solitour.user_image.entity.UserImage; +import solitour_backend.solitour.user_image.entity.UserImageRepository; +import solitour_backend.solitour.util.HmacUtils; +import solitour_backend.solitour.zone_category.dto.mapper.ZoneCategoryMapper; +import solitour_backend.solitour.zone_category.dto.response.ZoneCategoryResponse; +import solitour_backend.solitour.zone_category.entity.ZoneCategory; +import solitour_backend.solitour.zone_category.exception.ZoneCategoryNotExistsException; +import solitour_backend.solitour.zone_category.repository.ZoneCategoryRepository; + + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class GatheringService { + private final GatheringRepository gatheringRepository; + private final UserRepository userRepository; + private final ZoneCategoryRepository zoneCategoryRepository; + private final PlaceRepository placeRepository; + private final GatheringCategoryRepository gatheringCategoryRepository; + private final TagMapper tagMapper; + private final TagRepository tagRepository; + private final GatheringTagRepository gatheringTagRepository; + private final GatheringMapper gatheringMapper; + private final UserMapper userMapper; + private final PlaceMapper placeMapper; + private final ZoneCategoryMapper zoneCategoryMapper; + private final GreatGatheringRepository greatGatheringRepository; + private final GatheringApplicantsRepository gatheringApplicantsRepository; + private final GatheringApplicantsMapper gatheringApplicantsMapper; + private final GatheringCategoryMapper gatheringCategoryMapper; + private final UserImageRepository userImageRepository; + + + @Transactional + public GatheringDetailResponse getGatheringDetail(Long userId, Long gatheringId, HttpServletRequest request, HttpServletResponse response) { + Gathering gathering = gatheringRepository.findById(gatheringId) + .orElseThrow( + () -> new GatheringNotExistsException("해당하는 id의 gathering 이 존재 하지 않습니다")); + + if (Boolean.TRUE.equals(gathering.getIsDeleted())) { + throw new GatheringDeleteException("해당하는 모임은 삭제가 되었습니다"); + } + + UserPostingResponse userPostingResponse = userMapper.mapToUserPostingResponse(gathering.getUser()); + + List gatheringTags = gatheringTagRepository.findAllByGathering_Id(gathering.getId()); + + List tagResponses = new ArrayList<>(); + + if (!gatheringTags.isEmpty()) { + tagResponses = gatheringTags.stream() + .map(data -> + tagMapper.mapToTagResponse(data.getTag())) + .toList(); + } + GatheringCategory gatheringCategory = gathering.getGatheringCategory(); + + GatheringCategoryResponse gatheringCategoryResponse = gatheringCategoryMapper.mapToCategoryResponse( + gatheringCategory); + + PlaceResponse placeResponse = placeMapper.mapToPlaceResponse(gathering.getPlace()); + + ZoneCategoryResponse zoneCategoryResponse = zoneCategoryMapper.mapToZoneCategoryResponse( + gathering.getZoneCategory()); + + int likeCount = greatGatheringRepository.countByGatheringId(gathering.getId()); + + List gatheringApplicantsResponses = null; + + User user = gathering.getUser(); + + String userImageUrl = userImageRepository.findById(user.getUserImage().getId()) + .map(UserImage::getAddress) + .orElseGet( + () -> userRepository.getProfileUrl(user.getSex()) + ); + + GatheringStatus gatheringStatus = null; + GatheringApplicants gatheringApplicants = gatheringApplicantsRepository.findByGatheringIdAndUserId(gathering.getId(), userId).orElse(null); + + if (Objects.nonNull(gatheringApplicants)) { + gatheringStatus = gatheringApplicants.getGatheringStatus(); + } + + + if (gathering.getUser().getId().equals(userId)) { + gatheringApplicantsResponses = gatheringApplicantsMapper.mapToGatheringApplicantsResponses( + gatheringApplicantsRepository.findAllByGathering_IdAndUserIdNot(gathering.getId(), gathering.getUser().getId())); + } + + int nowPersonCount = gatheringApplicantsRepository.countAllByGathering_IdAndGatheringStatus(gathering.getId(), GatheringStatus.CONSENT); + + boolean isLike = greatGatheringRepository.existsByGatheringIdAndUserId(gathering.getId(), userId); + + List gatheringRecommend = gatheringRepository.getGatheringRecommend(gathering.getId(), + gathering.getGatheringCategory().getId(), userId); + + try { + updateViewCount(gathering, request, response, userId); + } catch (Exception e) { + throw new IllegalArgumentException(); + } + + + return new GatheringDetailResponse( + gathering.getTitle(), + gathering.getContent(), + gathering.getPersonCount(), + gathering.getViewCount(), + gathering.getCreatedAt(), + gathering.getScheduleStartDate(), + gathering.getScheduleEndDate(), + gathering.getDeadline(), + gathering.getIsFinish(), + gathering.getAllowedSex(), + gathering.getStartAge(), + gathering.getEndAge(), + tagResponses, + userPostingResponse, + placeResponse, + zoneCategoryResponse, + gatheringCategoryResponse, + likeCount, + nowPersonCount, + isLike, + gathering.getOpenChattingUrl(), + userImageUrl, + gatheringApplicantsResponses, + gatheringRecommend, + gatheringStatus + ); + } + + + @Transactional + public GatheringResponse registerGathering(Long userId, GatheringRegisterRequest gatheringRegisterRequest) { + PlaceRegisterRequest placeRegisterRequest = gatheringRegisterRequest.getPlaceRegisterRequest(); + Place place = new Place( + placeRegisterRequest.getSearchId(), + placeRegisterRequest.getName(), + placeRegisterRequest.getXAxis(), + placeRegisterRequest.getYAxis(), + placeRegisterRequest.getAddress()); + + Place savePlace = placeRepository.save(place); + User user = userRepository.findById(userId) + .orElseThrow( + () -> new UserNotExistsException("해당하는 id 의 User 가 없습니다")); + GatheringCategory gatheringCategory = gatheringCategoryRepository.findById( + gatheringRegisterRequest.getGatheringCategoryId()) + .orElseThrow( + () -> new GatheringCategoryNotExistsException("해당하는 id의 category 가 없습니다")); + + ZoneCategory parentZoneCategory = zoneCategoryRepository.findByParentZoneCategoryIdAndName(null, + gatheringRegisterRequest.getZoneCategoryNameParent()) + .orElseThrow( + () -> + new ZoneCategoryNotExistsException("해당하는 name 의 ZoneCategory 없습니다")); + + ZoneCategory childZoneCategory = zoneCategoryRepository.findByParentZoneCategoryIdAndName( + parentZoneCategory.getId(), gatheringRegisterRequest.getZoneCategoryNameChild()) + .orElseThrow( + () -> new ZoneCategoryNotExistsException("해당하는 name 의 ZoneCategory 없습니다")); + + Gathering gathering = + new Gathering( + user, + childZoneCategory, + gatheringCategory, + savePlace, + gatheringRegisterRequest.getTitle(), + gatheringRegisterRequest.getContent(), + gatheringRegisterRequest.getPersonCount(), + 0, + gatheringRegisterRequest.getScheduleStartDate(), + gatheringRegisterRequest.getScheduleEndDate(), + false, + gatheringRegisterRequest.getDeadline(), + gatheringRegisterRequest.getAllowedSex(), + gatheringRegisterRequest.getStartAge(), + gatheringRegisterRequest.getEndAge(), + gatheringRegisterRequest.getOpenChattingUrl() + ); + Gathering saveGathering = gatheringRepository.save(gathering); + + List tags = tagMapper.mapToTags(gatheringRegisterRequest.getTagRegisterRequests()); + List saveTags = tagRepository.saveAll(tags); + + GatheringApplicants gatheringApplicants = new GatheringApplicants(gathering, user, GatheringStatus.CONSENT); + gatheringApplicantsRepository.save(gatheringApplicants); + + for (Tag tag : saveTags) { + gatheringTagRepository.save(new GatheringTag(tag, saveGathering)); + } + return gatheringMapper.mapToGatheringResponse(saveGathering); + } + + @Transactional + public GatheringResponse modifyGathering(Long userId, Long gatheringId, + GatheringModifyRequest gatheringModifyRequest) { + User user = userRepository.findById(userId) + .orElseThrow( + () -> new UserNotExistsException("해당하는 id의 User 가 없습니다")); + + Gathering gathering = gatheringRepository.findById(gatheringId) + .orElseThrow( + () -> new GatheringNotExistsException("해당하는 id의 gathering 이 존재 하지 않습니다")); + + if (Boolean.TRUE.equals(gathering.getIsDeleted())) { + throw new GatheringDeleteException("해당하는 모임은 삭제가 되었습니다"); + } + + if (!Objects.equals(user, gathering.getUser())) { + throw new GatheringNotManagerException("해당 유저는 권한이 없습니다"); + } + + GatheringCategory gatheringCategory = gatheringCategoryRepository.findById( + gatheringModifyRequest.getGatheringCategoryId()) + .orElseThrow( + () -> new GatheringCategoryNotExistsException("모임 카테고리가 존재 하지 않습니다") + ); + ZoneCategory parentZoneCategory = zoneCategoryRepository.findByParentZoneCategoryIdAndName(null, + gatheringModifyRequest.getZoneCategoryNameParent()) + .orElseThrow( + () -> new ZoneCategoryNotExistsException("부모 지역 카테고리가 존재 하지 않습니다") + ); + ZoneCategory childZoneCategory = zoneCategoryRepository.findByParentZoneCategoryIdAndName( + parentZoneCategory.getId(), gatheringModifyRequest.getZoneCategoryNameChild()) + .orElseThrow( + () -> new ZoneCategoryNotExistsException("자식 지역 카테고리가 존재 하지 않습니다") + ); + + Place place = gathering.getPlace(); + PlaceModifyRequest placeModifyRequest = gatheringModifyRequest.getPlaceModifyRequest(); + place.setSearchId(placeModifyRequest.getSearchId()); + place.setName(placeModifyRequest.getName()); + place.setXaxis(placeModifyRequest.getXAxis()); + place.setYaxis(placeModifyRequest.getYAxis()); + place.setAddress(placeModifyRequest.getAddress()); + + gathering.setTitle(gatheringModifyRequest.getTitle()); + gathering.setContent(gatheringModifyRequest.getContent()); + + gathering.setPersonCount(gatheringModifyRequest.getPersonCount()); + gathering.setScheduleStartDate(gatheringModifyRequest.getScheduleStartDate()); + gathering.setScheduleEndDate(gatheringModifyRequest.getScheduleEndDate()); + gathering.setDeadline(gatheringModifyRequest.getDeadline()); + gathering.setAllowedSex(gatheringModifyRequest.getAllowedSex()); + gathering.setStartAge(gatheringModifyRequest.getStartAge()); + gathering.setEndAge(gatheringModifyRequest.getEndAge()); + gathering.setGatheringCategory(gatheringCategory); + gathering.setZoneCategory(childZoneCategory); + gathering.setOpenChattingUrl(gatheringModifyRequest.getOpenChattingUrl()); + + List gatheringTags = gatheringTagRepository.findAllByGathering_Id(gathering.getId()); + + gatheringTagRepository.deleteAllByGathering_Id(gathering.getId()); + + for (GatheringTag gatheringTag : gatheringTags) { + tagRepository.deleteById(gatheringTag.getTag().getTagId()); + } + + List saveTags = tagRepository.saveAll( + tagMapper.mapToTags(gatheringModifyRequest.getTagRegisterRequests())); + + for (Tag tag : saveTags) { + gatheringTagRepository.save(new GatheringTag(tag, gathering)); + } + + return gatheringMapper.mapToGatheringResponse(gathering); + } + + @Transactional + public void deleteGathering(Long gatheringId, Long userId) { + Gathering gathering = gatheringRepository.findById(gatheringId) + .orElseThrow( + () -> new GatheringNotExistsException("해당하는 id의 gathering 이 존재 하지 않습니다")); + + User user = userRepository.findById(userId) + .orElseThrow( + () -> new UserNotExistsException("해당하는 id의 User 가 없습니다")); + + if (!Objects.equals(user, gathering.getUser())) { + throw new GatheringNotManagerException("해당 유저는 권한이 없습니다"); + } + + if (Boolean.TRUE.equals(gathering.getIsDeleted())) { + return; + } + + gathering.setIsDeleted(true); + + } + + public Page getPageGathering(Pageable pageable, GatheringPageRequest gatheringPageRequest, + Long userId) { + validateGatheringPageRequest(gatheringPageRequest); + + return gatheringRepository.getGatheringPageFilterAndOrder(pageable, gatheringPageRequest, userId); + } + + public Page getPageGatheringByTag(Pageable pageable, Long userId, + GatheringPageRequest gatheringPageRequest, + String decodedTag) { + validateGatheringPageRequest(gatheringPageRequest); + + return gatheringRepository.getPageGatheringByTag(pageable, gatheringPageRequest, userId, decodedTag); + } + + public List getGatheringRankOrderByLikes() { + return gatheringRepository.getGatheringRankList(); + } + + public List getGatheringOrderByLikesFilterByCreate3After(Long userId) { + return gatheringRepository.getGatheringLikeCountFromCreatedIn3(userId); + } + + @Transactional + public void setGatheringFinish(Long userId, Long gatheringId) { + Gathering gathering = gatheringRepository.findById(gatheringId) + .orElseThrow( + () -> new GatheringNotExistsException("해당하는 id의 gathering 이 존재 하지 않습니다")); + + User user = userRepository.findById(userId) + .orElseThrow( + () -> new UserNotExistsException("해당하는 id의 User 가 없습니다")); + + if (!Objects.equals(user, gathering.getUser())) { + throw new GatheringNotManagerException("해당 유저는 권한이 없습니다"); + } + + if (Boolean.TRUE.equals(gathering.getIsDeleted())) { + throw new GatheringDeleteException("해당 하는 모임은 삭제된 모임입니다"); + } + + if (Boolean.TRUE.equals(gathering.getIsFinish())) { + throw new GatheringFinishConflictException("이미 모임이 finish 상태입니다"); + } + + gathering.setIsFinish(true); + } + + @Transactional + public void setGatheringNotFinish(Long userId, Long gatheringId) { + Gathering gathering = gatheringRepository.findById(gatheringId) + .orElseThrow( + () -> new GatheringNotExistsException("해당하는 id의 gathering 이 존재 하지 않습니다")); + + User user = userRepository.findById(userId) + .orElseThrow( + () -> new UserNotExistsException("해당하는 id의 User 가 없습니다")); + + if (!Objects.equals(user, gathering.getUser())) { + throw new GatheringNotManagerException("해당 유저는 권한이 없습니다"); + } + + if (Boolean.TRUE.equals(gathering.getIsDeleted())) { + throw new GatheringDeleteException("해당 하는 모임은 삭제된 모임입니다"); + } + + if (Boolean.FALSE.equals(gathering.getIsFinish())) { + throw new GatheringFinishConflictException("이미 모임이 not finish 상태입니다"); + } + + gathering.setIsFinish(false); + } + + + private void validateGatheringPageRequest(GatheringPageRequest gatheringPageRequest) { + // Category 검증 + if (Objects.nonNull(gatheringPageRequest.getCategory())) { + if (!gatheringCategoryRepository.existsById(gatheringPageRequest.getCategory())) { + throw new GatheringCategoryNotExistsException("해당하는 모임 카테고리가 없습니다."); + } + } + + // Location 검증 + if (Objects.nonNull(gatheringPageRequest.getLocation())) { + if (!zoneCategoryRepository.existsById(gatheringPageRequest.getLocation())) { + throw new ZoneCategoryNotExistsException("해당하는 지역 카테고리가 없습니다."); + } + } + + // 나이 범위 검증 + if (Objects.nonNull(gatheringPageRequest.getStartAge()) && Objects.nonNull(gatheringPageRequest.getEndAge())) { + if (gatheringPageRequest.getStartAge() > gatheringPageRequest.getEndAge()) { + throw new RequestValidationFailedException("시작 나이가 끝 나이보다 클 수 없습니다."); + } + } else if (Objects.nonNull(gatheringPageRequest.getStartAge()) || Objects.nonNull( + gatheringPageRequest.getEndAge())) { + throw new RequestValidationFailedException("시작 나이와 끝 나이는 둘 다 입력되거나 둘 다 비어 있어야 합니다."); + } + + // 정렬 방식 검증 + if (Objects.nonNull(gatheringPageRequest.getSort())) { + if (!LIKE_COUNT_SORT.equals(gatheringPageRequest.getSort()) && !VIEW_COUNT_SORT.equals(gatheringPageRequest.getSort())) { + throw new RequestValidationFailedException("잘못된 정렬 코드입니다."); + } + } + + // 날짜 검증 + if (Objects.nonNull(gatheringPageRequest.getStartDate()) && Objects.nonNull( + gatheringPageRequest.getEndDate())) { + + if (gatheringPageRequest.getStartDate().isAfter(gatheringPageRequest.getEndDate())) { + throw new RequestValidationFailedException("시작 날짜가 종료 날짜보다 나중일 수 없습니다."); + } + } else if (Objects.nonNull(gatheringPageRequest.getStartDate()) || Objects.nonNull( + gatheringPageRequest.getEndDate())) { + throw new RequestValidationFailedException("시작 날짜와 종료 날짜는 둘 다 입력되거나 둘 다 비어 있어야 합니다."); + } + } + + + public void updateViewCount(Gathering gathering, HttpServletRequest request, HttpServletResponse response, Long userId) throws Exception { + String cookieName = "viewed_gatherings"; + Cookie[] cookies = request.getCookies(); + Cookie postCookie = null; + + if (Objects.nonNull(cookies)) { + postCookie = Arrays.stream(cookies) + .filter(cookie -> cookieName.equals(cookie.getName())) + .findFirst() + .orElse(null); + } + + LocalDate now = LocalDate.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String cookieData = userId + "_" + gathering.getId() + "_" + now.format(formatter); + + if (Objects.nonNull(postCookie) && postCookie.getValue() != null) { + String[] gatheringDataArray = URLDecoder.decode(postCookie.getValue(), StandardCharsets.UTF_8).split(","); + boolean isUpdated = false; + boolean hasExistingData = false; + + for (int i = 0; i < gatheringDataArray.length; i++) { + String[] parts = gatheringDataArray[i].split("\\|"); + + if (parts.length == 2) { + if (HmacUtils.verifyHmac(parts[0], parts[1])) { + String[] cookieInfo = parts[0].split("_"); + Long cookieUserId = Long.parseLong(cookieInfo[0]); + Long cookieGatheringId = Long.parseLong(cookieInfo[1]); + LocalDate lastViewedAt = LocalDate.parse(cookieInfo[2], formatter); + + if (cookieUserId.equals(userId) && cookieGatheringId.equals(gathering.getId())) { + hasExistingData = true; + if (lastViewedAt.isBefore(now.minusDays(1))) { + incrementGatheringViewCount(gathering); + String newHmac = HmacUtils.generateHmac(cookieData); + gatheringDataArray[i] = cookieData + "|" + newHmac; + isUpdated = true; + } + break; + } + } + + } + } + + if (isUpdated || !hasExistingData) { + if (!hasExistingData) { + incrementGatheringViewCount(gathering); + } + String hmac = HmacUtils.generateHmac(cookieData); + String updatedValue = String.join(",", gatheringDataArray) + "," + cookieData + "|" + hmac; + postCookie.setValue(URLEncoder.encode(updatedValue, StandardCharsets.UTF_8)); + postCookie.setPath("/"); + response.addCookie(postCookie); + } + + + } else { + incrementGatheringViewCount(gathering); + String hmac = HmacUtils.generateHmac(cookieData); + Cookie newCookie = new Cookie(cookieName, URLEncoder.encode(cookieData + "|" + hmac, StandardCharsets.UTF_8)); + newCookie.setMaxAge(60 * 60 * 24); + newCookie.setPath("/"); + response.addCookie(newCookie); + } + } + + private void incrementGatheringViewCount(Gathering gathering) { + gathering.upViewCount(); + } + + +} diff --git a/src/main/java/solitour_backend/solitour/gathering_applicants/controller/GatheringApplicantsController.java b/src/main/java/solitour_backend/solitour/gathering_applicants/controller/GatheringApplicantsController.java new file mode 100644 index 0000000..d41ef2d --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_applicants/controller/GatheringApplicantsController.java @@ -0,0 +1,66 @@ +package solitour_backend.solitour.gathering_applicants.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import solitour_backend.solitour.auth.config.AuthenticationPrincipal; +import solitour_backend.solitour.error.Utils; +import solitour_backend.solitour.gathering_applicants.dto.request.GatheringApplicantsModifyRequest; +import solitour_backend.solitour.gathering_applicants.service.GatheringApplicantsService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/gatherings/applicants") +public class GatheringApplicantsController { + private final GatheringApplicantsService gatheringApplicantsService; + + @PostMapping("/{gatheringId}") + public ResponseEntity participateGathering(@AuthenticationPrincipal Long userId, + @PathVariable Long gatheringId) { + gatheringApplicantsService.participateGatheringFromAnotherUser(userId, gatheringId); + + return ResponseEntity + .status(HttpStatus.CREATED) + .build(); + } + + @DeleteMapping("/{gatheringId}") + public ResponseEntity deleteParticipateGathering(@AuthenticationPrincipal Long userId, + @PathVariable Long gatheringId) { + gatheringApplicantsService.deleteGatheringApplicantsFromAnotherUser(userId, gatheringId); + + return ResponseEntity + .status(HttpStatus.NO_CONTENT) + .build(); + } + + @PutMapping("/{gatheringId}") + public ResponseEntity updateParticipateGatheringStatus(@AuthenticationPrincipal Long userId, + @PathVariable Long gatheringId, + @Valid @RequestBody GatheringApplicantsModifyRequest gatheringApplicantsModifyRequest, + BindingResult bindingResult) { + Utils.validationRequest(bindingResult); + + boolean result = gatheringApplicantsService.updateGatheringApplicantsManagement(userId, gatheringId, + gatheringApplicantsModifyRequest); + + if (result) { + return ResponseEntity + .status(HttpStatus.CREATED) + .build(); + } + + return ResponseEntity + .status(HttpStatus.NO_CONTENT) + .build(); + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering_applicants/dto/mapper/GatheringApplicantsMapper.java b/src/main/java/solitour_backend/solitour/gathering_applicants/dto/mapper/GatheringApplicantsMapper.java new file mode 100644 index 0000000..c34a923 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_applicants/dto/mapper/GatheringApplicantsMapper.java @@ -0,0 +1,18 @@ +package solitour_backend.solitour.gathering_applicants.dto.mapper; + +import java.util.List; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import solitour_backend.solitour.gathering_applicants.dto.response.GatheringApplicantsResponse; +import solitour_backend.solitour.gathering_applicants.entity.GatheringApplicants; +import solitour_backend.solitour.user.dto.mapper.UserMapper; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR, uses = UserMapper.class) +public interface GatheringApplicantsMapper { + + @Mapping(source = "user", target = "userGatheringResponse") + GatheringApplicantsResponse mapToGatheringApplicantsResponse(GatheringApplicants gatheringApplicants); + + List mapToGatheringApplicantsResponses(List gatheringApplicants); +} diff --git a/src/main/java/solitour_backend/solitour/gathering_applicants/dto/request/GatheringApplicantsModifyRequest.java b/src/main/java/solitour_backend/solitour/gathering_applicants/dto/request/GatheringApplicantsModifyRequest.java new file mode 100644 index 0000000..0164e33 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_applicants/dto/request/GatheringApplicantsModifyRequest.java @@ -0,0 +1,19 @@ +package solitour_backend.solitour.gathering_applicants.dto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; +import solitour_backend.solitour.gathering_applicants.entity.GatheringStatus; + +@Getter +@NoArgsConstructor +public class GatheringApplicantsModifyRequest { + + @Min(1) + @NotNull + private Long userId; + + @NotNull + private GatheringStatus gatheringStatus; +} diff --git a/src/main/java/solitour_backend/solitour/gathering_applicants/dto/response/GatheringApplicantsResponse.java b/src/main/java/solitour_backend/solitour/gathering_applicants/dto/response/GatheringApplicantsResponse.java new file mode 100644 index 0000000..f7d0fd7 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_applicants/dto/response/GatheringApplicantsResponse.java @@ -0,0 +1,13 @@ +package solitour_backend.solitour.gathering_applicants.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import solitour_backend.solitour.gathering_applicants.entity.GatheringStatus; +import solitour_backend.solitour.user.dto.response.UserGatheringResponse; + +@Getter +@AllArgsConstructor +public class GatheringApplicantsResponse { + private UserGatheringResponse userGatheringResponse; + private GatheringStatus gatheringStatus; +} diff --git a/src/main/java/solitour_backend/solitour/gathering_applicants/entity/GatheringApplicants.java b/src/main/java/solitour_backend/solitour/gathering_applicants/entity/GatheringApplicants.java new file mode 100644 index 0000000..b2dddc2 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_applicants/entity/GatheringApplicants.java @@ -0,0 +1,48 @@ +package solitour_backend.solitour.gathering_applicants.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +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 jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import solitour_backend.solitour.gathering.entity.Gathering; +import solitour_backend.solitour.user.entity.User; + +@Entity +@Getter +@Setter +@Table(name = "gathering_applicants") +@NoArgsConstructor +public class GatheringApplicants { + + @Id + @Column(name = "gathering_applicants_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "gathering_id") + private Gathering gathering; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column(name = "gathering_applicants_state") + @Convert(converter = GatheringStatusConverter.class) + private GatheringStatus gatheringStatus; + + public GatheringApplicants(Gathering gathering, User user, GatheringStatus gatheringStatus) { + this.gathering = gathering; + this.user = user; + this.gatheringStatus = gatheringStatus; + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering_applicants/entity/GatheringStatus.java b/src/main/java/solitour_backend/solitour/gathering_applicants/entity/GatheringStatus.java new file mode 100644 index 0000000..288f6ae --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_applicants/entity/GatheringStatus.java @@ -0,0 +1,24 @@ +package solitour_backend.solitour.gathering_applicants.entity; + +import java.util.Arrays; +import lombok.Getter; + +@Getter +public enum GatheringStatus { + WAIT("wait"), + CONSENT("consent"), + REFUSE("refuse"); + + private final String name; + + GatheringStatus(String name) { + this.name = name; + } + + public static GatheringStatus fromName(String name) { + return Arrays.stream(GatheringStatus.values()) + .filter(e -> e.name.equals(name)) + .findAny() + .orElse(null); + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering_applicants/entity/GatheringStatusConverter.java b/src/main/java/solitour_backend/solitour/gathering_applicants/entity/GatheringStatusConverter.java new file mode 100644 index 0000000..cd0d7eb --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_applicants/entity/GatheringStatusConverter.java @@ -0,0 +1,17 @@ +package solitour_backend.solitour.gathering_applicants.entity; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class GatheringStatusConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(GatheringStatus gatheringStatus) { + return gatheringStatus.getName(); + } + + @Override + public GatheringStatus convertToEntityAttribute(String dbData) { + return GatheringStatus.fromName(dbData); + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering_applicants/exception/GatheringApplicantsAlreadyExistsException.java b/src/main/java/solitour_backend/solitour/gathering_applicants/exception/GatheringApplicantsAlreadyExistsException.java new file mode 100644 index 0000000..84d7758 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_applicants/exception/GatheringApplicantsAlreadyExistsException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.gathering_applicants.exception; + +public class GatheringApplicantsAlreadyExistsException extends RuntimeException { + public GatheringApplicantsAlreadyExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering_applicants/exception/GatheringApplicantsAlreadyFullPeopleException.java b/src/main/java/solitour_backend/solitour/gathering_applicants/exception/GatheringApplicantsAlreadyFullPeopleException.java new file mode 100644 index 0000000..27fe8fc --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_applicants/exception/GatheringApplicantsAlreadyFullPeopleException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.gathering_applicants.exception; + +public class GatheringApplicantsAlreadyFullPeopleException extends RuntimeException { + public GatheringApplicantsAlreadyFullPeopleException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering_applicants/exception/GatheringApplicantsManagerException.java b/src/main/java/solitour_backend/solitour/gathering_applicants/exception/GatheringApplicantsManagerException.java new file mode 100644 index 0000000..cc82b5b --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_applicants/exception/GatheringApplicantsManagerException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.gathering_applicants.exception; + +public class GatheringApplicantsManagerException extends RuntimeException { + public GatheringApplicantsManagerException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering_applicants/exception/GatheringApplicantsNotExistsException.java b/src/main/java/solitour_backend/solitour/gathering_applicants/exception/GatheringApplicantsNotExistsException.java new file mode 100644 index 0000000..a6595fd --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_applicants/exception/GatheringApplicantsNotExistsException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.gathering_applicants.exception; + +public class GatheringApplicantsNotExistsException extends RuntimeException { + public GatheringApplicantsNotExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering_applicants/exception/GatheringNotManagerException.java b/src/main/java/solitour_backend/solitour/gathering_applicants/exception/GatheringNotManagerException.java new file mode 100644 index 0000000..2d83e2c --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_applicants/exception/GatheringNotManagerException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.gathering_applicants.exception; + +public class GatheringNotManagerException extends RuntimeException { + public GatheringNotManagerException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering_applicants/repository/GatheringApplicantsRepository.java b/src/main/java/solitour_backend/solitour/gathering_applicants/repository/GatheringApplicantsRepository.java new file mode 100644 index 0000000..7eda687 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_applicants/repository/GatheringApplicantsRepository.java @@ -0,0 +1,17 @@ +package solitour_backend.solitour.gathering_applicants.repository; + +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import solitour_backend.solitour.gathering_applicants.entity.GatheringApplicants; +import solitour_backend.solitour.gathering_applicants.entity.GatheringStatus; + +public interface GatheringApplicantsRepository extends JpaRepository { + List findAllByGathering_IdAndUserIdNot(Long id, Long userId); + + int countAllByGathering_IdAndGatheringStatus(Long id, GatheringStatus gatheringStatus); + + boolean existsByGatheringIdAndUserId(Long gatheringId, Long userId); + + Optional findByGatheringIdAndUserId(Long gatheringId, Long userId); +} diff --git a/src/main/java/solitour_backend/solitour/gathering_applicants/service/GatheringApplicantsService.java b/src/main/java/solitour_backend/solitour/gathering_applicants/service/GatheringApplicantsService.java new file mode 100644 index 0000000..ec68b97 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_applicants/service/GatheringApplicantsService.java @@ -0,0 +1,123 @@ +package solitour_backend.solitour.gathering_applicants.service; + +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import solitour_backend.solitour.gathering.entity.Gathering; +import solitour_backend.solitour.gathering.exception.GatheringNotExistsException; +import solitour_backend.solitour.gathering.repository.GatheringRepository; +import solitour_backend.solitour.gathering_applicants.dto.request.GatheringApplicantsModifyRequest; +import solitour_backend.solitour.gathering_applicants.entity.GatheringApplicants; +import solitour_backend.solitour.gathering_applicants.entity.GatheringStatus; +import solitour_backend.solitour.gathering_applicants.exception.GatheringApplicantsAlreadyExistsException; +import solitour_backend.solitour.gathering_applicants.exception.GatheringApplicantsAlreadyFullPeopleException; +import solitour_backend.solitour.gathering_applicants.exception.GatheringApplicantsManagerException; +import solitour_backend.solitour.gathering_applicants.exception.GatheringApplicantsNotExistsException; +import solitour_backend.solitour.gathering_applicants.exception.GatheringNotManagerException; +import solitour_backend.solitour.gathering_applicants.repository.GatheringApplicantsRepository; +import solitour_backend.solitour.user.entity.User; +import solitour_backend.solitour.user.exception.UserNotExistsException; +import solitour_backend.solitour.user.repository.UserRepository; + +@Service +@Transactional +@RequiredArgsConstructor +public class GatheringApplicantsService { + private final GatheringRepository gatheringRepository; + private final GatheringApplicantsRepository gatheringApplicantsRepository; + private final UserRepository userRepository; + + public void participateGatheringFromAnotherUser(Long userId, Long gatheringId) { + Gathering gathering = gatheringRepository.findById(gatheringId) + .orElseThrow( + () -> + new GatheringNotExistsException("해당하는 id의 gathering 이 존재 하지 않습니다")); + + User user = userRepository.findById(userId) + .orElseThrow( + () -> + new UserNotExistsException("해당하는 id의 user가 없습니다")); + + if (Objects.equals(gathering.getUser(), user)) { + throw new GatheringApplicantsManagerException("모임을 만든 사람은 해당 모임에 무조건 참여 하여 이미 있습니다"); + } + + if (gatheringApplicantsRepository.existsByGatheringIdAndUserId(gathering.getId(), user.getId())) { + throw new GatheringApplicantsAlreadyExistsException("해당 유저는 이미 참여 해 있습니다."); + } + + Integer personCount = gathering.getPersonCount(); + int nowPersonCount = gatheringApplicantsRepository.countAllByGathering_IdAndGatheringStatus(gatheringId, + GatheringStatus.CONSENT); + + if (personCount <= nowPersonCount) { + throw new GatheringApplicantsAlreadyFullPeopleException("이미 인원이 가득 찼습니다."); + } + + GatheringApplicants gatheringApplicants = new GatheringApplicants(gathering, user, GatheringStatus.WAIT); + + gatheringApplicantsRepository.save(gatheringApplicants); + } + + public void deleteGatheringApplicantsFromAnotherUser(Long userId, Long gatheringId) { + Gathering gathering = gatheringRepository.findById(gatheringId) + .orElseThrow( + () -> + new GatheringNotExistsException("해당하는 id의 gathering 이 존재 하지 않습니다")); + + User user = userRepository.findById(userId) + .orElseThrow( + () -> + new UserNotExistsException("해당하는 id의 user 가 없습니다")); + + if (Objects.equals(gathering.getUser(), user)) { + throw new GatheringApplicantsManagerException("모임을 만든 사람은 해당 모임에 빠질 수 없습니다."); + } + + GatheringApplicants gatheringApplicants = gatheringApplicantsRepository.findByGatheringIdAndUserId( + gathering.getId(), user.getId()) + .orElseThrow( + () -> + new GatheringApplicantsNotExistsException( + "해당하는 모임과 user 의 gathering applicants 가 없습니다.")); + + gatheringApplicantsRepository.delete(gatheringApplicants); + } + + public boolean updateGatheringApplicantsManagement(Long userId, Long gatheringId, + GatheringApplicantsModifyRequest gatheringApplicantsModifyRequest) { + Gathering gathering = gatheringRepository.findById(gatheringId) + .orElseThrow( + () -> + new GatheringNotExistsException("해당하는 id의 gathering 이 존재 하지 않습니다")); + + User user = userRepository.findById(userId) + .orElseThrow( + () -> + new UserNotExistsException("해당하는 id의 user 가 없습니다")); + + if (!Objects.equals(gathering.getUser(), user)) { + throw new GatheringNotManagerException("해당하는 user 가 해당 gathering 의 manage 가 아닙니다"); + } + + GatheringApplicants gatheringApplicants = gatheringApplicantsRepository.findByGatheringIdAndUserId( + gathering.getId(), gatheringApplicantsModifyRequest.getUserId()) + .orElseThrow( + () -> + new GatheringApplicantsNotExistsException("해당하는 모임, user 의 applicants 가 없습니다") + ); + + if (Objects.equals(gathering.getUser(), gatheringApplicants.getUser())) { + throw new GatheringApplicantsManagerException("모임을 만든 사람은 해당 모임 참여에 대한 수정이 불가 합니다."); + } + + if (Objects.equals(gatheringApplicants.getGatheringStatus(), + gatheringApplicantsModifyRequest.getGatheringStatus())) { + return false; + } + + gatheringApplicants.setGatheringStatus(gatheringApplicantsModifyRequest.getGatheringStatus()); + return true; + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering_category/controller/GatheringCategoryController.java b/src/main/java/solitour_backend/solitour/gathering_category/controller/GatheringCategoryController.java new file mode 100644 index 0000000..4ccae99 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_category/controller/GatheringCategoryController.java @@ -0,0 +1,79 @@ +package solitour_backend.solitour.gathering_category.controller; + +import jakarta.validation.Valid; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import solitour_backend.solitour.category.dto.request.CategoryRegisterRequest; +import solitour_backend.solitour.error.Utils; +import solitour_backend.solitour.error.exception.RequestValidationFailedException; +import solitour_backend.solitour.gathering_category.dto.request.GatheringCategoryModifyRequest; +import solitour_backend.solitour.gathering_category.dto.response.GatheringCategoryResponse; +import solitour_backend.solitour.gathering_category.service.GatheringCategoryService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/categories/gathering") +public class GatheringCategoryController { + + private final GatheringCategoryService gatheringCategoryService; + + @GetMapping + public ResponseEntity> getAllCategories() { + List parentCategories = gatheringCategoryService.getCategories(); + + return ResponseEntity + .status(HttpStatus.OK) + .body(parentCategories); + } + + @GetMapping("/{id}") + public ResponseEntity getCategory(@PathVariable Long id) { + GatheringCategoryResponse category = gatheringCategoryService.getCategory(id); + + return ResponseEntity + .status(HttpStatus.OK) + .body(category); + } + + @PostMapping + public ResponseEntity registerCategory( + @Valid @RequestBody CategoryRegisterRequest categoryRegisterRequest, + BindingResult bindingResult) { + Utils.validationRequest(bindingResult); + + GatheringCategoryResponse categoryResponse = gatheringCategoryService.registerCategory( + categoryRegisterRequest); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(categoryResponse); + + } + + @PutMapping("/{id}") + public ResponseEntity modifyCategory( + @Valid @RequestBody GatheringCategoryModifyRequest gatheringCategoryModifyRequest, + BindingResult bindingResult, + @PathVariable Long id) { + if (bindingResult.hasErrors()) { + throw new RequestValidationFailedException(bindingResult); + } + GatheringCategoryResponse categoryResponse = gatheringCategoryService.modifyCategory(id, + gatheringCategoryModifyRequest); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(categoryResponse); + } + + +} diff --git a/src/main/java/solitour_backend/solitour/gathering_category/dto/mapper/GatheringCategoryMapper.java b/src/main/java/solitour_backend/solitour/gathering_category/dto/mapper/GatheringCategoryMapper.java new file mode 100644 index 0000000..52be5ae --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_category/dto/mapper/GatheringCategoryMapper.java @@ -0,0 +1,15 @@ +package solitour_backend.solitour.gathering_category.dto.mapper; + +import java.util.List; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import solitour_backend.solitour.gathering_category.dto.response.GatheringCategoryResponse; +import solitour_backend.solitour.gathering_category.entity.GatheringCategory; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface GatheringCategoryMapper { + + GatheringCategoryResponse mapToCategoryResponse(GatheringCategory category); + + List mapToCategoryResponses(List categories); +} diff --git a/src/main/java/solitour_backend/solitour/gathering_category/dto/request/GatheringCategoryModifyRequest.java b/src/main/java/solitour_backend/solitour/gathering_category/dto/request/GatheringCategoryModifyRequest.java new file mode 100644 index 0000000..703826f --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_category/dto/request/GatheringCategoryModifyRequest.java @@ -0,0 +1,14 @@ +package solitour_backend.solitour.gathering_category.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class GatheringCategoryModifyRequest { + @NotBlank + @Size(min = 2, max = 20) + private String name; +} diff --git a/src/main/java/solitour_backend/solitour/gathering_category/dto/response/GatheringCategoryResponse.java b/src/main/java/solitour_backend/solitour/gathering_category/dto/response/GatheringCategoryResponse.java new file mode 100644 index 0000000..9f882cb --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_category/dto/response/GatheringCategoryResponse.java @@ -0,0 +1,11 @@ +package solitour_backend.solitour.gathering_category.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GatheringCategoryResponse { + private Long id; + private String name; +} diff --git a/src/main/java/solitour_backend/solitour/gathering_category/entity/GatheringCategory.java b/src/main/java/solitour_backend/solitour/gathering_category/entity/GatheringCategory.java new file mode 100644 index 0000000..0330787 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_category/entity/GatheringCategory.java @@ -0,0 +1,32 @@ +package solitour_backend.solitour.gathering_category.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table(name = "gathering_category") +@NoArgsConstructor +public class GatheringCategory { + + @Id + @Column(name = "gathering_category_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + + @Column(name = "gathering_category_name") + private String name; + + public GatheringCategory(String name) { + this.name = name; + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering_category/repository/GatheringCategoryRepository.java b/src/main/java/solitour_backend/solitour/gathering_category/repository/GatheringCategoryRepository.java new file mode 100644 index 0000000..c014b27 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_category/repository/GatheringCategoryRepository.java @@ -0,0 +1,8 @@ +package solitour_backend.solitour.gathering_category.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import solitour_backend.solitour.gathering_category.entity.GatheringCategory; + +public interface GatheringCategoryRepository extends JpaRepository { + +} diff --git a/src/main/java/solitour_backend/solitour/gathering_category/service/GatheringCategoryService.java b/src/main/java/solitour_backend/solitour/gathering_category/service/GatheringCategoryService.java new file mode 100644 index 0000000..eae4af0 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_category/service/GatheringCategoryService.java @@ -0,0 +1,58 @@ +package solitour_backend.solitour.gathering_category.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import solitour_backend.solitour.category.dto.request.CategoryRegisterRequest; +import solitour_backend.solitour.category.exception.CategoryNotExistsException; +import solitour_backend.solitour.gathering_category.dto.mapper.GatheringCategoryMapper; +import solitour_backend.solitour.gathering_category.dto.request.GatheringCategoryModifyRequest; +import solitour_backend.solitour.gathering_category.dto.response.GatheringCategoryResponse; +import solitour_backend.solitour.gathering_category.entity.GatheringCategory; +import solitour_backend.solitour.gathering_category.repository.GatheringCategoryRepository; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class GatheringCategoryService { + + private final GatheringCategoryRepository gatheringCategoryRepository; + private final GatheringCategoryMapper gatheringCategoryMapper; + + @Transactional + public GatheringCategoryResponse registerCategory(CategoryRegisterRequest categoryRegisterRequest) { + + GatheringCategory category = new GatheringCategory(categoryRegisterRequest.getName()); + GatheringCategory saveCategory = gatheringCategoryRepository.save(category); + + return gatheringCategoryMapper.mapToCategoryResponse(saveCategory); + } + + + public GatheringCategoryResponse getCategory(Long id) { + GatheringCategory category = gatheringCategoryRepository.findById(id) + .orElseThrow( + () -> new CategoryNotExistsException("category not found")); + + return gatheringCategoryMapper.mapToCategoryResponse(category); + } + + + public List getCategories() { + List allGatheringCategory = gatheringCategoryRepository.findAll(); + + return gatheringCategoryMapper.mapToCategoryResponses(allGatheringCategory); + } + + @Transactional + public GatheringCategoryResponse modifyCategory(Long id, + GatheringCategoryModifyRequest gatheringCategoryModifyRequest) { + GatheringCategory category = gatheringCategoryRepository.findById(id).orElseThrow(); + + category.setName(gatheringCategoryModifyRequest.getName()); + + return gatheringCategoryMapper.mapToCategoryResponse(category); + } + +} diff --git a/src/main/java/solitour_backend/solitour/gathering_tag/entity/GatheringTag.java b/src/main/java/solitour_backend/solitour/gathering_tag/entity/GatheringTag.java new file mode 100644 index 0000000..a4762be --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_tag/entity/GatheringTag.java @@ -0,0 +1,40 @@ +package solitour_backend.solitour.gathering_tag.entity; + +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.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import solitour_backend.solitour.gathering.entity.Gathering; +import solitour_backend.solitour.tag.entity.Tag; + +@Entity +@Getter +@Table(name = "gathering_tag") +@NoArgsConstructor +public class GatheringTag { + + @Id + @Column(name = "gathering_tag_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tag_id") + private Tag tag; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "gathering_id") + private Gathering gathering; + + public GatheringTag(Tag tag, Gathering gathering) { + this.tag = tag; + this.gathering = gathering; + } +} diff --git a/src/main/java/solitour_backend/solitour/gathering_tag/repository/GatheringTagRepository.java b/src/main/java/solitour_backend/solitour/gathering_tag/repository/GatheringTagRepository.java new file mode 100644 index 0000000..ed2aa05 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/gathering_tag/repository/GatheringTagRepository.java @@ -0,0 +1,11 @@ +package solitour_backend.solitour.gathering_tag.repository; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import solitour_backend.solitour.gathering_tag.entity.GatheringTag; + +public interface GatheringTagRepository extends JpaRepository { + List findAllByGathering_Id(Long gatheringId); + + void deleteAllByGathering_Id(Long gatheringId); +} diff --git a/src/main/java/solitour_backend/solitour/great_gathering/controller/GreatGatheringController.java b/src/main/java/solitour_backend/solitour/great_gathering/controller/GreatGatheringController.java new file mode 100644 index 0000000..e51f4ab --- /dev/null +++ b/src/main/java/solitour_backend/solitour/great_gathering/controller/GreatGatheringController.java @@ -0,0 +1,38 @@ +package solitour_backend.solitour.great_gathering.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import solitour_backend.solitour.auth.config.Authenticated; +import solitour_backend.solitour.auth.config.AuthenticationPrincipal; +import solitour_backend.solitour.great_gathering.entity.GreatGathering; +import solitour_backend.solitour.great_gathering.service.GreatGatheringService; + +@Authenticated +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/gathering/great") +public class GreatGatheringController { + + private final GreatGatheringService service; + + @PostMapping() + public ResponseEntity createGatheringGreat(@AuthenticationPrincipal Long userId, + @RequestParam Long gatheringId) { + GreatGathering greatGathering = service.createGatheringGreat(userId, gatheringId); + + return ResponseEntity.ok(greatGathering.getId()); + } + + @DeleteMapping() + public ResponseEntity deleteGatheringGreat(@AuthenticationPrincipal Long userId, + @RequestParam Long gatheringId) { + service.deleteGatheringGreat(userId, gatheringId); + + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/solitour_backend/solitour/great_gathering/entity/GreatGathering.java b/src/main/java/solitour_backend/solitour/great_gathering/entity/GreatGathering.java new file mode 100644 index 0000000..a3712f4 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/great_gathering/entity/GreatGathering.java @@ -0,0 +1,42 @@ +package solitour_backend.solitour.great_gathering.entity; + + +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.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import solitour_backend.solitour.gathering.entity.Gathering; +import solitour_backend.solitour.user.entity.User; + +@Entity +@Getter +@Table(name = "great_gathering") +@NoArgsConstructor +public class GreatGathering { + + @Id + @Column(name = "great_gathering_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "gathering_id") + private Gathering gathering; + + public GreatGathering(User user, Gathering gathering) { + this.user = user; + this.gathering = gathering; + } +} diff --git a/src/main/java/solitour_backend/solitour/great_gathering/exception/GatheringGreatNotExistsException.java b/src/main/java/solitour_backend/solitour/great_gathering/exception/GatheringGreatNotExistsException.java new file mode 100644 index 0000000..d59adad --- /dev/null +++ b/src/main/java/solitour_backend/solitour/great_gathering/exception/GatheringGreatNotExistsException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.great_gathering.exception; + +public class GatheringGreatNotExistsException extends RuntimeException { + public GatheringGreatNotExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/great_gathering/repository/GreatGatheringRepository.java b/src/main/java/solitour_backend/solitour/great_gathering/repository/GreatGatheringRepository.java new file mode 100644 index 0000000..1b9ace4 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/great_gathering/repository/GreatGatheringRepository.java @@ -0,0 +1,16 @@ +package solitour_backend.solitour.great_gathering.repository; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import solitour_backend.solitour.great_gathering.entity.GreatGathering; + +public interface GreatGatheringRepository extends JpaRepository { + + @Query("SELECT COUNT(g) FROM GreatGathering g WHERE g.gathering.id = :gatheringId") + int countByGatheringId(Long gatheringId); + + boolean existsByGatheringIdAndUserId(Long gatheringId, Long userId); + + Optional findByGatheringIdAndUserId(Long gatheringId, Long userId); +} diff --git a/src/main/java/solitour_backend/solitour/great_gathering/service/GreatGatheringService.java b/src/main/java/solitour_backend/solitour/great_gathering/service/GreatGatheringService.java new file mode 100644 index 0000000..24b8cd6 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/great_gathering/service/GreatGatheringService.java @@ -0,0 +1,42 @@ +package solitour_backend.solitour.great_gathering.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import solitour_backend.solitour.gathering.entity.Gathering; +import solitour_backend.solitour.gathering.exception.GatheringNotExistsException; +import solitour_backend.solitour.gathering.repository.GatheringRepository; +import solitour_backend.solitour.great_gathering.entity.GreatGathering; +import solitour_backend.solitour.great_gathering.exception.GatheringGreatNotExistsException; +import solitour_backend.solitour.great_gathering.repository.GreatGatheringRepository; +import solitour_backend.solitour.user.entity.User; +import solitour_backend.solitour.user.repository.UserRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class GreatGatheringService { + + private final GreatGatheringRepository greatGatheringRepository; + private final UserRepository userRepository; + private final GatheringRepository gatheringRepository; + + @Transactional + public GreatGathering createGatheringGreat(Long userId, Long gatheringId) { + User user = userRepository.findByUserId(userId); + Gathering gathering = gatheringRepository.findById(gatheringId) + .orElseThrow(() -> new GatheringNotExistsException("해당 모임이 없습니다.")); + + return greatGatheringRepository.findByGatheringIdAndUserId(gatheringId, userId) + .orElseGet(() -> greatGatheringRepository.save(new GreatGathering(user, gathering))); + } + + @Transactional + public void deleteGatheringGreat(Long userId, Long gatheringId) { + GreatGathering greatGathering = greatGatheringRepository.findByGatheringIdAndUserId(gatheringId, + userId) + .orElseThrow(() -> new GatheringGreatNotExistsException("해당 모임에는 좋아요를 하지 않았습니다")); + + greatGatheringRepository.delete(greatGathering); + } +} diff --git a/src/main/java/solitour_backend/solitour/great_information/controller/GreatInformationController.java b/src/main/java/solitour_backend/solitour/great_information/controller/GreatInformationController.java new file mode 100644 index 0000000..71703aa --- /dev/null +++ b/src/main/java/solitour_backend/solitour/great_information/controller/GreatInformationController.java @@ -0,0 +1,38 @@ +package solitour_backend.solitour.great_information.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import solitour_backend.solitour.auth.config.Authenticated; +import solitour_backend.solitour.auth.config.AuthenticationPrincipal; +import solitour_backend.solitour.great_information.entity.GreatInformation; +import solitour_backend.solitour.great_information.service.GreatInformationService; + +@Authenticated +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/information/great") +public class GreatInformationController { + + private final GreatInformationService service; + + @PostMapping() + public ResponseEntity createInformationGreat(@AuthenticationPrincipal Long userId, + @RequestParam Long infoId) { + GreatInformation greatInformation = service.createInformationGreat(userId, infoId); + + return ResponseEntity.ok(greatInformation.getId()); + } + + @DeleteMapping() + public ResponseEntity deleteInformationGreat(@AuthenticationPrincipal Long userId, + @RequestParam Long infoId) { + service.deleteInformationGreat(userId, infoId); + + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/solitour_backend/solitour/great_information/dto/response/GreatInformationResponse.java b/src/main/java/solitour_backend/solitour/great_information/dto/response/GreatInformationResponse.java new file mode 100644 index 0000000..1d112ff --- /dev/null +++ b/src/main/java/solitour_backend/solitour/great_information/dto/response/GreatInformationResponse.java @@ -0,0 +1,13 @@ +package solitour_backend.solitour.great_information.dto.response; + +import lombok.Getter; + +@Getter +public class GreatInformationResponse { + + private final int greatInformationCount; + + public GreatInformationResponse(int greatInformationCount) { + this.greatInformationCount = greatInformationCount; + } +} diff --git a/src/main/java/solitour_backend/solitour/great_information/entity/GreatInformation.java b/src/main/java/solitour_backend/solitour/great_information/entity/GreatInformation.java new file mode 100644 index 0000000..4aa9407 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/great_information/entity/GreatInformation.java @@ -0,0 +1,40 @@ +package solitour_backend.solitour.great_information.entity; + +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.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import solitour_backend.solitour.information.entity.Information; +import solitour_backend.solitour.user.entity.User; + +@Entity +@Getter +@Table(name = "great_information") +@NoArgsConstructor +public class GreatInformation { + + @Id + @Column(name = "great_information_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "information_id") + private Information information; + + public GreatInformation(User user, Information information) { + this.user = user; + this.information = information; + } +} diff --git a/src/main/java/solitour_backend/solitour/great_information/exception/InformationGreatNotExistsException.java b/src/main/java/solitour_backend/solitour/great_information/exception/InformationGreatNotExistsException.java new file mode 100644 index 0000000..2bdf044 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/great_information/exception/InformationGreatNotExistsException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.great_information.exception; + +public class InformationGreatNotExistsException extends RuntimeException { + public InformationGreatNotExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/great_information/repository/GreatInformationRepository.java b/src/main/java/solitour_backend/solitour/great_information/repository/GreatInformationRepository.java new file mode 100644 index 0000000..8ce2e6e --- /dev/null +++ b/src/main/java/solitour_backend/solitour/great_information/repository/GreatInformationRepository.java @@ -0,0 +1,18 @@ +package solitour_backend.solitour.great_information.repository; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import solitour_backend.solitour.great_information.entity.GreatInformation; + +public interface GreatInformationRepository extends JpaRepository { + + @Query("SELECT COUNT(g) FROM GreatInformation g WHERE g.information.id = :informationId") + int countByInformationId(Long informationId); + + Optional findByInformationIdAndUserId(Long informationId, Long userId); + + void deleteAllByInformationId(Long informationId); + + boolean existsByInformationIdAndUserId(Long informationId, Long userId); +} diff --git a/src/main/java/solitour_backend/solitour/great_information/service/GreatInformationService.java b/src/main/java/solitour_backend/solitour/great_information/service/GreatInformationService.java new file mode 100644 index 0000000..8bdc402 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/great_information/service/GreatInformationService.java @@ -0,0 +1,43 @@ +package solitour_backend.solitour.great_information.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import solitour_backend.solitour.great_information.entity.GreatInformation; +import solitour_backend.solitour.great_information.exception.InformationGreatNotExistsException; +import solitour_backend.solitour.great_information.repository.GreatInformationRepository; +import solitour_backend.solitour.information.entity.Information; +import solitour_backend.solitour.information.exception.InformationNotExistsException; +import solitour_backend.solitour.information.repository.InformationRepository; +import solitour_backend.solitour.user.entity.User; +import solitour_backend.solitour.user.repository.UserRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class GreatInformationService { + + private final GreatInformationRepository greatInformationRepository; + private final UserRepository userRepository; + private final InformationRepository informationRepository; + + @Transactional + public GreatInformation createInformationGreat(Long userId, Long infoId) { + User user = userRepository.findByUserId(userId); + Information information = informationRepository.findById(infoId) + .orElseThrow(() -> new InformationNotExistsException("해당 정보가 없습니다.")); + + return greatInformationRepository.findByInformationIdAndUserId(infoId, userId) + .orElseGet( + () -> greatInformationRepository.save(new GreatInformation(user, information))); + } + + @Transactional + public void deleteInformationGreat(Long userId, Long infoId) { + GreatInformation greatInformation = greatInformationRepository.findByInformationIdAndUserId(infoId, + userId) + .orElseThrow(() -> new InformationGreatNotExistsException("해당 정보에는 좋아요를 하지 않았습니다")); + + greatInformationRepository.delete(greatInformation); + } +} diff --git a/src/main/java/solitour_backend/solitour/health_check/HealthCheckController.java b/src/main/java/solitour_backend/solitour/health_check/HealthCheckController.java new file mode 100644 index 0000000..514ae93 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/health_check/HealthCheckController.java @@ -0,0 +1,13 @@ +package solitour_backend.solitour.health_check; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HealthCheckController { + @GetMapping("/healthcheck") + public ResponseEntity healthcheck() { + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/solitour_backend/solitour/image/controller/ImageController.java b/src/main/java/solitour_backend/solitour/image/controller/ImageController.java new file mode 100644 index 0000000..7f1e3ba --- /dev/null +++ b/src/main/java/solitour_backend/solitour/image/controller/ImageController.java @@ -0,0 +1,55 @@ +package solitour_backend.solitour.image.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import solitour_backend.solitour.auth.config.Authenticated; +import solitour_backend.solitour.auth.config.AuthenticationPrincipal; +import solitour_backend.solitour.image.dto.response.S3FileResponse; +import solitour_backend.solitour.image.image_status.ImageType; +import solitour_backend.solitour.image.service.ImageService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/image") +public class ImageController { + + private final ImageService imageService; + @Value("${image.env}") + private String env; + + + @Authenticated + @PostMapping + public ResponseEntity uploadImage(@AuthenticationPrincipal Long userId, + @RequestPart("image") MultipartFile userImage, + @RequestParam ImageType type) { + checkType(type); + String path = env.concat("/").concat(type.getName()); + S3FileResponse s3FileResponse = imageService.uploadImage(userId, userImage, path); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(s3FileResponse); + } + + private void checkType(ImageType type) { + switch (type) { + case USER: + case DIARY: + case GATHERING: + case INFORMATION: + break; + default: + throw new IllegalArgumentException("잘못된 타입입니다."); + } + } + +} diff --git a/src/main/java/solitour_backend/solitour/image/dto/mapper/ImageMapper.java b/src/main/java/solitour_backend/solitour/image/dto/mapper/ImageMapper.java new file mode 100644 index 0000000..eca9e9c --- /dev/null +++ b/src/main/java/solitour_backend/solitour/image/dto/mapper/ImageMapper.java @@ -0,0 +1,24 @@ +package solitour_backend.solitour.image.dto.mapper; + +import java.util.List; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.ReportingPolicy; +import solitour_backend.solitour.image.dto.response.ImageResponse; +import solitour_backend.solitour.image.entity.Image; +import solitour_backend.solitour.image.image_status.ImageStatus; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface ImageMapper { + + @Mapping(source = "imageStatus", target = "imageStatus", qualifiedByName = "mapImageStatus") + ImageResponse toImageResponse(Image image); + + @Named("mapImageStatus") + default String mapImageStatus(ImageStatus imageStatus) { + return imageStatus.getName(); + } + + List toImageResponseList(List images); +} diff --git a/src/main/java/solitour_backend/solitour/image/dto/request/ImageRequest.java b/src/main/java/solitour_backend/solitour/image/dto/request/ImageRequest.java new file mode 100644 index 0000000..eefcebd --- /dev/null +++ b/src/main/java/solitour_backend/solitour/image/dto/request/ImageRequest.java @@ -0,0 +1,14 @@ +package solitour_backend.solitour.image.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ImageRequest { + @NotBlank + @Size(min = 1, max = 200) + private String address; +} diff --git a/src/main/java/solitour_backend/solitour/image/dto/response/ImageResponse.java b/src/main/java/solitour_backend/solitour/image/dto/response/ImageResponse.java new file mode 100644 index 0000000..5bb8c46 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/image/dto/response/ImageResponse.java @@ -0,0 +1,11 @@ +package solitour_backend.solitour.image.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ImageResponse { + private String imageStatus; + private String address; +} \ No newline at end of file diff --git a/src/main/java/solitour_backend/solitour/image/dto/response/S3FileResponse.java b/src/main/java/solitour_backend/solitour/image/dto/response/S3FileResponse.java new file mode 100644 index 0000000..683e80a --- /dev/null +++ b/src/main/java/solitour_backend/solitour/image/dto/response/S3FileResponse.java @@ -0,0 +1,10 @@ +package solitour_backend.solitour.image.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class S3FileResponse { + private String fileUrl; +} diff --git a/src/main/java/solitour_backend/solitour/image/entity/Image.java b/src/main/java/solitour_backend/solitour/image/entity/Image.java new file mode 100644 index 0000000..8e1419e --- /dev/null +++ b/src/main/java/solitour_backend/solitour/image/entity/Image.java @@ -0,0 +1,49 @@ +package solitour_backend.solitour.image.entity; + +import jakarta.persistence.*; + +import java.time.LocalDate; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import solitour_backend.solitour.image.image_status.ImageStatus; +import solitour_backend.solitour.image.image_status.ImageStatusConverter; +import solitour_backend.solitour.information.entity.Information; + +@Entity +@Getter +@Setter +@Table(name = "image") +@NoArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class Image { + + @Id + @Column(name = "image_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "image_status_id") + @Convert(converter = ImageStatusConverter.class) + private ImageStatus imageStatus; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "information_id") + private Information information; + + @Column(name = "image_address") + private String address; + + @CreatedDate + @Column(name = "image_created_date") + private LocalDate createdDate; + + public Image(ImageStatus imageStatus, Information information, String address) { + this.imageStatus = imageStatus; + this.information = information; + this.address = address; + } +} diff --git a/src/main/java/solitour_backend/solitour/image/exception/ImageAlreadyExistsException.java b/src/main/java/solitour_backend/solitour/image/exception/ImageAlreadyExistsException.java new file mode 100644 index 0000000..38dfb41 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/image/exception/ImageAlreadyExistsException.java @@ -0,0 +1,8 @@ +package solitour_backend.solitour.image.exception; + +public class ImageAlreadyExistsException extends RuntimeException { + + public ImageAlreadyExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/image/exception/ImageNotExistsException.java b/src/main/java/solitour_backend/solitour/image/exception/ImageNotExistsException.java new file mode 100644 index 0000000..d3a15f8 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/image/exception/ImageNotExistsException.java @@ -0,0 +1,8 @@ +package solitour_backend.solitour.image.exception; + +public class ImageNotExistsException extends RuntimeException { + + public ImageNotExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/image/exception/ImageRequestValidationFailedException.java b/src/main/java/solitour_backend/solitour/image/exception/ImageRequestValidationFailedException.java new file mode 100644 index 0000000..4f2ed13 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/image/exception/ImageRequestValidationFailedException.java @@ -0,0 +1,8 @@ +package solitour_backend.solitour.image.exception; + +public class ImageRequestValidationFailedException extends RuntimeException { + + public ImageRequestValidationFailedException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/image/image_status/ImageStatus.java b/src/main/java/solitour_backend/solitour/image/image_status/ImageStatus.java new file mode 100644 index 0000000..2387343 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/image/image_status/ImageStatus.java @@ -0,0 +1,25 @@ +package solitour_backend.solitour.image.image_status; + +import java.util.Arrays; +import lombok.Getter; + +@Getter +public enum ImageStatus { + THUMBNAIL("썸네일"), + CONTENT("본문"), + USER("회원"), + NONE("없음"); + + private final String name; + + ImageStatus(String name) { + this.name = name; + } + + public static ImageStatus fromName(String name) { + return Arrays.stream(ImageStatus.values()) + .filter(e -> e.name.equals(name)) + .findAny() + .orElse(null); + } +} \ No newline at end of file diff --git a/src/main/java/solitour_backend/solitour/image/image_status/ImageStatusConverter.java b/src/main/java/solitour_backend/solitour/image/image_status/ImageStatusConverter.java new file mode 100644 index 0000000..deb9b04 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/image/image_status/ImageStatusConverter.java @@ -0,0 +1,18 @@ +package solitour_backend.solitour.image.image_status; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class ImageStatusConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(ImageStatus imageStatus) { + return imageStatus.getName(); + } + + @Override + public ImageStatus convertToEntityAttribute(String dbData) { + return ImageStatus.fromName(dbData); + } +} diff --git a/src/main/java/solitour_backend/solitour/image/image_status/ImageType.java b/src/main/java/solitour_backend/solitour/image/image_status/ImageType.java new file mode 100644 index 0000000..16b474a --- /dev/null +++ b/src/main/java/solitour_backend/solitour/image/image_status/ImageType.java @@ -0,0 +1,25 @@ +package solitour_backend.solitour.image.image_status; + +import java.util.Arrays; +import lombok.Getter; + +@Getter +public enum ImageType { + USER("user"), + DIARY("diary"), + GATHERING("gathering"), + INFORMATION("information"); + + private final String name; + + ImageType(String name) { + this.name = name; + } + + public static ImageType fromName(String name) { + return Arrays.stream(ImageType.values()) + .filter(e -> e.name.equals(name)) + .findAny() + .orElse(null); + } +} \ No newline at end of file diff --git a/src/main/java/solitour_backend/solitour/image/repository/ImageRepository.java b/src/main/java/solitour_backend/solitour/image/repository/ImageRepository.java new file mode 100644 index 0000000..e097eb5 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/image/repository/ImageRepository.java @@ -0,0 +1,25 @@ +package solitour_backend.solitour.image.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import solitour_backend.solitour.image.entity.Image; +import solitour_backend.solitour.image.image_status.ImageStatus; + +public interface ImageRepository extends JpaRepository { + + boolean existsByInformationIdAndImageStatus(Long informationId, ImageStatus imageStatus); + + Optional findImageByAddress(String address); + + List findAllByInformationId(Long informationId); + + boolean existsImageByAddress(String address); + + void deleteByAddress(String address); + + void deleteAllByInformationId(Long informationId); + + boolean existsImageByImageStatusAndInformationId(ImageStatus imageStatus, Long informationId); +} diff --git a/src/main/java/solitour_backend/solitour/image/s3/S3Config.java b/src/main/java/solitour_backend/solitour/image/s3/S3Config.java new file mode 100644 index 0000000..1b6c537 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/image/s3/S3Config.java @@ -0,0 +1,31 @@ +package solitour_backend.solitour.image.s3; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) + .enablePathStyleAccess() + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } + +} diff --git a/src/main/java/solitour_backend/solitour/image/s3/S3Uploader.java b/src/main/java/solitour_backend/solitour/image/s3/S3Uploader.java new file mode 100644 index 0000000..fe474ff --- /dev/null +++ b/src/main/java/solitour_backend/solitour/image/s3/S3Uploader.java @@ -0,0 +1,102 @@ +package solitour_backend.solitour.image.s3; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.*; + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +@RestController +@RequestMapping("/upload") +@RequiredArgsConstructor +public class S3Uploader { + + private final AmazonS3Client amazonS3Client; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + + public String upload(MultipartFile multipartFile, String dirName, Long id) { + + String fileName = dirName + "/" + id + "/" + createFileName(multipartFile.getOriginalFilename()); + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(multipartFile.getSize()); + metadata.setContentType(multipartFile.getContentType()); + + List tags = new ArrayList<>(); + tags.add(new Tag("temp", "true")); + + + try (InputStream inputStream = multipartFile.getInputStream()) { + PutObjectRequest putObjectRequest = + new PutObjectRequest(bucket, fileName, inputStream, metadata).withTagging(new ObjectTagging(tags)); + amazonS3Client.putObject(putObjectRequest); + } catch (IOException e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드에 실패했습니다."); + } + return amazonS3Client.getUrl(bucket, fileName).toString(); + } + + public void markImagePermanent(String imageUrl) { + String fileName = extractFileNameFromUrlUseBucketName(imageUrl); + GetObjectTaggingRequest getTaggingRequest = new GetObjectTaggingRequest(bucket, fileName); + GetObjectTaggingResult getTaggingResult = amazonS3Client.getObjectTagging(getTaggingRequest); + List tags = getTaggingResult.getTagSet(); + + boolean tagFound = false; + + for (Tag tag : tags) { + if (tag.getKey().equals("temp")) { + tag.setValue("false"); + tagFound = true; + break; + } + } + + if (!tagFound) { + tags.add(new Tag("temp", "false")); + } + + SetObjectTaggingRequest setObjectTaggingRequest = new SetObjectTaggingRequest(bucket, fileName, new ObjectTagging(tags)); + amazonS3Client.setObjectTagging(setObjectTaggingRequest); + } + + public void deleteImage(String fileUrl) { + String fileName = extractFileNameFromUrlUseBucketName(fileUrl); + + amazonS3Client.deleteObject(new DeleteObjectRequest(bucket, fileName)); + } + + + private String createFileName(String fileName) { + return UUID.randomUUID().toString().concat(getFileExtension(fileName)); + } + + private String getFileExtension(String fileName) { + try { + return fileName.substring(fileName.lastIndexOf(".")); + } catch (StringIndexOutOfBoundsException se) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "잘못된 형식의 파일 입니다."); + } + } + + private String extractFileNameFromUrl(String url) { + final String splitStr = ".com/"; + return url.substring(url.lastIndexOf(splitStr) + splitStr.length()); + } + + private String extractFileNameFromUrlUseBucketName(String url) { + final String splitStr = "solitour-bucket/"; + return url.substring(url.lastIndexOf(splitStr) + splitStr.length()); + } +} \ No newline at end of file diff --git a/src/main/java/solitour_backend/solitour/image/service/ImageService.java b/src/main/java/solitour_backend/solitour/image/service/ImageService.java new file mode 100644 index 0000000..49fd08b --- /dev/null +++ b/src/main/java/solitour_backend/solitour/image/service/ImageService.java @@ -0,0 +1,23 @@ +package solitour_backend.solitour.image.service; + + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import solitour_backend.solitour.image.dto.response.S3FileResponse; +import solitour_backend.solitour.image.s3.S3Uploader; + +@Service +@Transactional +@RequiredArgsConstructor +public class ImageService { + + private final S3Uploader s3Uploader; + + public S3FileResponse uploadImage(Long id, MultipartFile image, String path) { + String imageUrl = s3Uploader.upload(image, path, id); + + return new S3FileResponse(imageUrl); + } +} diff --git a/src/main/java/solitour_backend/solitour/info_tag/entity/InfoTag.java b/src/main/java/solitour_backend/solitour/info_tag/entity/InfoTag.java new file mode 100644 index 0000000..2af64f4 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/info_tag/entity/InfoTag.java @@ -0,0 +1,40 @@ +package solitour_backend.solitour.info_tag.entity; + +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.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import solitour_backend.solitour.information.entity.Information; +import solitour_backend.solitour.tag.entity.Tag; + +@Entity +@Getter +@Table(name = "info_tag") +@NoArgsConstructor +public class InfoTag { + + @Id + @Column(name = "info_tag_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tag_id") + private Tag tag; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "information_id") + private Information information; + + public InfoTag(Tag tag, Information information) { + this.tag = tag; + this.information = information; + } +} diff --git a/src/main/java/solitour_backend/solitour/info_tag/repository/InfoTagRepository.java b/src/main/java/solitour_backend/solitour/info_tag/repository/InfoTagRepository.java new file mode 100644 index 0000000..3dc611c --- /dev/null +++ b/src/main/java/solitour_backend/solitour/info_tag/repository/InfoTagRepository.java @@ -0,0 +1,13 @@ +package solitour_backend.solitour.info_tag.repository; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import solitour_backend.solitour.info_tag.entity.InfoTag; + +public interface InfoTagRepository extends JpaRepository { + + List findAllByInformationId(Long informationId); + + void deleteAllByInformationId(Long informationId); + +} diff --git a/src/main/java/solitour_backend/solitour/information/controller/InformationController.java b/src/main/java/solitour_backend/solitour/information/controller/InformationController.java new file mode 100644 index 0000000..2953616 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/controller/InformationController.java @@ -0,0 +1,175 @@ +package solitour_backend.solitour.information.controller; + + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; + +import java.io.UnsupportedEncodingException; +import java.util.List; +import java.util.Objects; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; +import solitour_backend.solitour.auth.config.Authenticated; +import solitour_backend.solitour.auth.config.AuthenticationPrincipal; +import solitour_backend.solitour.auth.exception.TokenNotExistsException; +import solitour_backend.solitour.auth.support.CookieExtractor; +import solitour_backend.solitour.auth.support.JwtTokenProvider; +import solitour_backend.solitour.error.Utils; +import solitour_backend.solitour.information.dto.request.InformationCreateRequest; +import solitour_backend.solitour.information.dto.request.InformationPageRequest; +import solitour_backend.solitour.information.dto.request.InformationUpdateRequest; +import solitour_backend.solitour.information.dto.response.InformationBriefResponse; +import solitour_backend.solitour.information.dto.response.InformationDetailResponse; +import solitour_backend.solitour.information.dto.response.InformationMainResponse; +import solitour_backend.solitour.information.dto.response.InformationRankResponse; +import solitour_backend.solitour.information.dto.response.InformationResponse; +import solitour_backend.solitour.information.service.InformationService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/informations") +public class InformationController { + + private final InformationService informationService; + public static final int PAGE_SIZE = 12; + private final JwtTokenProvider jwtTokenProvider; + + + @PostMapping + @Authenticated + public ResponseEntity createInformation(@AuthenticationPrincipal Long userId, + @Valid @RequestBody InformationCreateRequest informationCreateRequest, + BindingResult bindingResult) { + Utils.validationRequest(bindingResult); + + InformationResponse informationResponse = informationService.registerInformation(userId, informationCreateRequest); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(informationResponse); + } + + @GetMapping("/{informationId}") + public ResponseEntity getDetailInformation(@PathVariable Long informationId, + HttpServletRequest request, + HttpServletResponse response) { + Long userId = findUser(request); + InformationDetailResponse informationDetailResponse = informationService.getDetailInformation(userId, informationId, request, response); + + return ResponseEntity + .status(HttpStatus.OK) + .body(informationDetailResponse); + } + + @Authenticated + @PutMapping("/{informationId}") + public ResponseEntity modifyInformation(@AuthenticationPrincipal Long userId, + @PathVariable Long informationId, + @Valid @RequestBody InformationUpdateRequest informationUpdateRequest, + BindingResult bindingResult) { + Utils.validationRequest(bindingResult); + + InformationResponse informationResponse = informationService.updateInformation(userId, informationId, informationUpdateRequest); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(informationResponse); + } + + @Authenticated + @DeleteMapping("/{informationId}") + public ResponseEntity deleteInformation(@AuthenticationPrincipal Long userId, + @PathVariable Long informationId) { + informationService.deleteInformation(userId, informationId); + + return ResponseEntity + .status(HttpStatus.NO_CONTENT) + .build(); + } + + @GetMapping + public ResponseEntity> pageInformationSortAndFilter(@RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "1") Long parentCategoryId, + @Valid @ModelAttribute InformationPageRequest informationPageRequest, + BindingResult bindingResult, + HttpServletRequest request) { + Utils.validationRequest(bindingResult); + Long userId = findUser(request); + + Pageable pageable = PageRequest.of(page, PAGE_SIZE); + Page pageInformation = informationService.getPageInformation(pageable, userId, + parentCategoryId, informationPageRequest); + + return ResponseEntity + .status(HttpStatus.OK) + .body(pageInformation); + } + + @GetMapping("/tag/search") + public ResponseEntity> getPageInformationByTag(@RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "1") Long parentCategoryId, + @Valid @ModelAttribute InformationPageRequest informationPageRequest, + @RequestParam(required = false, name = "tagName") String tag, + BindingResult bindingResult, + HttpServletRequest request) + throws UnsupportedEncodingException { + String decodedValue = java.net.URLDecoder.decode(tag, "UTF-8"); + String filteredTag = decodedValue.replaceAll("[^a-zA-Z0-9가-힣]", ""); + + Utils.validationRequest(bindingResult); + Long userId = findUser(request); + Pageable pageable = PageRequest.of(page, PAGE_SIZE); + Page briefInformationPage = informationService.getPageInformationByTag( + pageable, userId, parentCategoryId, informationPageRequest, filteredTag); + return ResponseEntity + .status(HttpStatus.OK) + .body(briefInformationPage); + } + + @GetMapping("/ranks") + public ResponseEntity> rankInformation() { + List rankInformation = informationService.getRankInformation(); + return ResponseEntity + .status(HttpStatus.OK) + .body(rankInformation); + } + + @GetMapping("/main-page") + public ResponseEntity> mainPageInformation(HttpServletRequest request) { + Long userId = findUser(request); + + List informationList = informationService.getMainPageInformation(userId); + + return ResponseEntity + .status(HttpStatus.OK) + .body(informationList); + } + + + private Long findUser(HttpServletRequest request) { + String token = CookieExtractor.findToken("access_token", request.getCookies()); + + if (Objects.isNull(token)) { + token = CookieExtractor.findToken("refresh_token", request.getCookies()); + } + if (Objects.isNull(token)) { + return (long) 0; + } + + if (jwtTokenProvider.validateTokenNotUsable(token)) { + throw new TokenNotExistsException("토큰이 존재하지 않습니다."); + } + + return jwtTokenProvider.getPayload(token); + } + +} + diff --git a/src/main/java/solitour_backend/solitour/information/dto/mapper/InformationMapper.java b/src/main/java/solitour_backend/solitour/information/dto/mapper/InformationMapper.java new file mode 100644 index 0000000..46a80e9 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/dto/mapper/InformationMapper.java @@ -0,0 +1,12 @@ +package solitour_backend.solitour.information.dto.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import solitour_backend.solitour.information.dto.response.InformationResponse; +import solitour_backend.solitour.information.entity.Information; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface InformationMapper { + + InformationResponse mapToInformationResponse(Information information); +} diff --git a/src/main/java/solitour_backend/solitour/information/dto/request/InformationCreateRequest.java b/src/main/java/solitour_backend/solitour/information/dto/request/InformationCreateRequest.java new file mode 100644 index 0000000..182eee5 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/dto/request/InformationCreateRequest.java @@ -0,0 +1,52 @@ +package solitour_backend.solitour.information.dto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import solitour_backend.solitour.place.dto.request.PlaceRegisterRequest; +import solitour_backend.solitour.tag.dto.request.TagRegisterRequest; + +import java.util.List; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class InformationCreateRequest { + @NotBlank + @Size(min = 1, max = 50) + private String informationTitle; + + @NotBlank + @Size(min = 1, max = 50) + private String informationAddress; + + private String informationContent; + + private String informationTips; + + @NotNull + private PlaceRegisterRequest placeRegisterRequest; + + @NotNull + @Min(1) + private Long categoryId; + + @NotBlank + @Size(min = 1, max = 20) + private String zoneCategoryNameParent; + + @NotBlank + @Size(min = 1, max = 20) + private String zoneCategoryNameChild; + + @NotBlank + private String thumbNailImageUrl; + + private List contentImagesUrl; + + private List tagRegisterRequests; +} diff --git a/src/main/java/solitour_backend/solitour/information/dto/request/InformationPageRequest.java b/src/main/java/solitour_backend/solitour/information/dto/request/InformationPageRequest.java new file mode 100644 index 0000000..2f175e8 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/dto/request/InformationPageRequest.java @@ -0,0 +1,25 @@ +package solitour_backend.solitour.information.dto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class InformationPageRequest { + @Min(1) + private Long childCategoryId; + + @Size(min = 1, max = 10) + private String sort; + + @Min(1) + private Long zoneCategoryId; + + private String search; +} diff --git a/src/main/java/solitour_backend/solitour/information/dto/request/InformationUpdateRequest.java b/src/main/java/solitour_backend/solitour/information/dto/request/InformationUpdateRequest.java new file mode 100644 index 0000000..9de8e60 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/dto/request/InformationUpdateRequest.java @@ -0,0 +1,57 @@ +package solitour_backend.solitour.information.dto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; +import solitour_backend.solitour.image.dto.request.ImageRequest; +import solitour_backend.solitour.place.dto.request.PlaceModifyRequest; +import solitour_backend.solitour.tag.dto.request.TagRegisterRequest; + +import java.util.List; + +@Getter +@NoArgsConstructor +public class InformationUpdateRequest { + + @NotBlank + @Size(min = 1, max = 50) + private String title; + + @NotBlank + @Size(min = 1, max = 50) + private String address; + + private String content; + + private String tips; + + @NotNull + private PlaceModifyRequest placeModifyRequest; + + @NotNull + @Min(1) + private Long categoryId; + + @NotBlank + @Size(min = 1, max = 20) + private String zoneCategoryNameParent; + + @NotBlank + @Size(min = 1, max = 20) + private String zoneCategoryNameChild; + + private ImageRequest newThumbNailUrl; + + private ImageRequest newThumbNailFromContent; + + private ImageRequest moveThumbNailToContent; + + private List deleteImagesUrl; + + private List newContentImagesUrl; + + private List tagRegisterRequests; +} diff --git a/src/main/java/solitour_backend/solitour/information/dto/response/InformationBriefResponse.java b/src/main/java/solitour_backend/solitour/information/dto/response/InformationBriefResponse.java new file mode 100644 index 0000000..2478487 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/dto/response/InformationBriefResponse.java @@ -0,0 +1,22 @@ +package solitour_backend.solitour.information.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@Getter +@AllArgsConstructor +@ToString +public class InformationBriefResponse { + + private Long informationId; + private String title; + private String zoneCategoryParentName; + private String zoneCategoryChildName; + private String categoryName; + private Integer viewCount; + private Boolean isBookMark; + private String thumbNailImage; + private Integer likeCount; + private Boolean isLike; +} diff --git a/src/main/java/solitour_backend/solitour/information/dto/response/InformationDetailResponse.java b/src/main/java/solitour_backend/solitour/information/dto/response/InformationDetailResponse.java new file mode 100644 index 0000000..ef5c47e --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/dto/response/InformationDetailResponse.java @@ -0,0 +1,37 @@ +package solitour_backend.solitour.information.dto.response; + +import java.time.LocalDateTime; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import solitour_backend.solitour.category.dto.response.CategoryResponse; +import solitour_backend.solitour.image.dto.response.ImageResponse; +import solitour_backend.solitour.place.dto.response.PlaceResponse; +import solitour_backend.solitour.tag.dto.response.TagResponse; +import solitour_backend.solitour.user.dto.UserPostingResponse; +import solitour_backend.solitour.zone_category.dto.response.ZoneCategoryResponse; + +@Getter +@AllArgsConstructor +public class InformationDetailResponse { + + private String title; + private String address; + private LocalDateTime createdDate; + private Integer viewCount; + private String content; + private String tip; + + private UserPostingResponse userPostingResponse; + private List tagResponses; + + private PlaceResponse placeResponse; + private ZoneCategoryResponse zoneCategoryResponse; + private CategoryResponse categoryResponse; + private List imageResponses; + private int likeCount; + private String userImage; + private Boolean isLike; + private List recommendInformation; +} diff --git a/src/main/java/solitour_backend/solitour/information/dto/response/InformationMainResponse.java b/src/main/java/solitour_backend/solitour/information/dto/response/InformationMainResponse.java new file mode 100644 index 0000000..e94c535 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/dto/response/InformationMainResponse.java @@ -0,0 +1,19 @@ +package solitour_backend.solitour.information.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class InformationMainResponse { + private Long informationId; + private String title; + private String zoneCategoryParentName; + private String zoneCategoryChildName; + private String parentCategoryName; + private Integer viewCount; + private Boolean isBookMark; + private String thumbNailImage; + private Integer likeCount; + private Boolean isLike; +} diff --git a/src/main/java/solitour_backend/solitour/information/dto/response/InformationRankResponse.java b/src/main/java/solitour_backend/solitour/information/dto/response/InformationRankResponse.java new file mode 100644 index 0000000..9722189 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/dto/response/InformationRankResponse.java @@ -0,0 +1,12 @@ +package solitour_backend.solitour.information.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class InformationRankResponse { + + private Long id; + private String title; +} diff --git a/src/main/java/solitour_backend/solitour/information/dto/response/InformationResponse.java b/src/main/java/solitour_backend/solitour/information/dto/response/InformationResponse.java new file mode 100644 index 0000000..ab11414 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/dto/response/InformationResponse.java @@ -0,0 +1,13 @@ +package solitour_backend.solitour.information.dto.response; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + + +@Getter +@AllArgsConstructor +public class InformationResponse { + + private Long id; +} diff --git a/src/main/java/solitour_backend/solitour/information/entity/Information.java b/src/main/java/solitour_backend/solitour/information/entity/Information.java new file mode 100644 index 0000000..23fc47c --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/entity/Information.java @@ -0,0 +1,83 @@ +package solitour_backend.solitour.information.entity; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import solitour_backend.solitour.category.entity.Category; +import solitour_backend.solitour.place.entity.Place; +import solitour_backend.solitour.user.entity.User; +import solitour_backend.solitour.zone_category.entity.ZoneCategory; + +@Entity +@Getter +@Setter +@Table(name = "information") +@NoArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class Information { + + @Id + @Column(name = "information_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "category_id") + private Category category; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "zone_category_id") + private ZoneCategory zoneCategory; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) + @JoinColumn(name = "place_id") + private Place place; + + + @Column(name = "information_title") + private String title; + + @Column(name = "information_address") + private String address; + + @CreatedDate + @Column(name = "information_created_date") + private LocalDateTime createdDate; + + @Column(name = "information_view_count") + private Integer viewCount; + + @Column(name = "information_content") + private String content; + + @Column(name = "information_tip") + private String tip; + + public Information(Category category, ZoneCategory zoneCategory, User user, Place place, + String title, String address, Integer viewCount, String content, + String tip) { + this.category = category; + this.zoneCategory = zoneCategory; + this.user = user; + this.place = place; + this.title = title; + this.address = address; + this.viewCount = viewCount; + this.content = content; + this.tip = tip; + } + + public void upViewCount() { + this.viewCount++; + } +} diff --git a/src/main/java/solitour_backend/solitour/information/exception/InformationNotExistsException.java b/src/main/java/solitour_backend/solitour/information/exception/InformationNotExistsException.java new file mode 100644 index 0000000..7bfac5d --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/exception/InformationNotExistsException.java @@ -0,0 +1,8 @@ +package solitour_backend.solitour.information.exception; + +public class InformationNotExistsException extends RuntimeException { + + public InformationNotExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/information/exception/InformationNotManageException.java b/src/main/java/solitour_backend/solitour/information/exception/InformationNotManageException.java new file mode 100644 index 0000000..c96a48e --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/exception/InformationNotManageException.java @@ -0,0 +1,8 @@ +package solitour_backend.solitour.information.exception; + +public class InformationNotManageException extends RuntimeException { + + public InformationNotManageException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/information/repository/InformationRepository.java b/src/main/java/solitour_backend/solitour/information/repository/InformationRepository.java new file mode 100644 index 0000000..434f410 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/repository/InformationRepository.java @@ -0,0 +1,9 @@ +package solitour_backend.solitour.information.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import solitour_backend.solitour.information.entity.Information; + + +public interface InformationRepository extends JpaRepository, InformationRepositoryCustom { + +} diff --git a/src/main/java/solitour_backend/solitour/information/repository/InformationRepositoryCustom.java b/src/main/java/solitour_backend/solitour/information/repository/InformationRepositoryCustom.java new file mode 100644 index 0000000..0076f59 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/repository/InformationRepositoryCustom.java @@ -0,0 +1,32 @@ +package solitour_backend.solitour.information.repository; + +import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.NoRepositoryBean; +import solitour_backend.solitour.information.dto.request.InformationPageRequest; +import solitour_backend.solitour.information.dto.response.InformationBriefResponse; +import solitour_backend.solitour.information.dto.response.InformationMainResponse; +import solitour_backend.solitour.information.dto.response.InformationRankResponse; + + +@NoRepositoryBean +public interface InformationRepositoryCustom { + String LIKE_COUNT_SORT = "likes"; + String VIEW_COUNT_SORT = "views"; + Page getPageInformationFilterAndOrder(Pageable pageable, InformationPageRequest informationPageRequest, Long userId, Long parentCategoryId); + +// Page getInformationPageFilterAndOrder(Pageable pageable, +// InformationPageRequest informationPageRequest, +// Long userId, Long parentCategoryId); + + List getInformationRank(); + + List getInformationLikeCountFromCreatedIn3(Long userId); + + List getInformationRecommend(Long informationId, Long childCategoryId, Long userId); + + Page getInformationPageByTag(Pageable pageable, Long userId, Long parentCategoryId, + InformationPageRequest informationPageRequest, + String decodedTag); +} diff --git a/src/main/java/solitour_backend/solitour/information/repository/InformationRepositoryImpl.java b/src/main/java/solitour_backend/solitour/information/repository/InformationRepositoryImpl.java new file mode 100644 index 0000000..c7799cb --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/repository/InformationRepositoryImpl.java @@ -0,0 +1,377 @@ +package solitour_backend.solitour.information.repository; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.*; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; +import solitour_backend.solitour.book_mark_information.entity.QBookMarkInformation; +import solitour_backend.solitour.category.entity.QCategory; +import solitour_backend.solitour.great_information.entity.QGreatInformation; +import solitour_backend.solitour.image.entity.QImage; +import solitour_backend.solitour.image.image_status.ImageStatus; +import solitour_backend.solitour.info_tag.entity.QInfoTag; +import solitour_backend.solitour.information.dto.request.InformationPageRequest; +import solitour_backend.solitour.information.dto.response.InformationBriefResponse; +import solitour_backend.solitour.information.dto.response.InformationMainResponse; +import solitour_backend.solitour.information.dto.response.InformationRankResponse; +import solitour_backend.solitour.information.entity.Information; +import solitour_backend.solitour.information.entity.QInformation; +import solitour_backend.solitour.zone_category.entity.QZoneCategory; + +@Slf4j +public class InformationRepositoryImpl extends QuerydslRepositorySupport implements InformationRepositoryCustom { + + public InformationRepositoryImpl() { + super(Information.class); + } + + QInformation information = QInformation.information; + QZoneCategory zoneCategoryChild = QZoneCategory.zoneCategory; + QZoneCategory zoneCategoryParent = new QZoneCategory("zoneCategoryParent"); + QBookMarkInformation bookMarkInformation = QBookMarkInformation.bookMarkInformation; + QImage image = QImage.image; + QGreatInformation greatInformation = QGreatInformation.greatInformation; + QCategory category = QCategory.category; + QCategory categoryParent = new QCategory("categoryParent"); + QInfoTag infoTag = QInfoTag.infoTag; + + public Page getPageInformationFilterAndOrder(Pageable pageable, InformationPageRequest informationPageRequest, Long userId, Long parentCategoryId) { + BooleanBuilder whereClause = new BooleanBuilder(); + if (Objects.nonNull(informationPageRequest.getZoneCategoryId())) { + whereClause.and( + zoneCategoryParent.id.eq(informationPageRequest.getZoneCategoryId()) + ); + } + + if (Objects.nonNull(informationPageRequest.getChildCategoryId())) { + whereClause.and(category.id.eq(informationPageRequest.getChildCategoryId())); + } else { + whereClause.and(categoryParent.id.eq(parentCategoryId)); + } + if (Objects.nonNull(informationPageRequest.getSearch())) { + String searchKeyword = informationPageRequest.getSearch().trim().replace(" ", ""); + whereClause.and(information.title.trim().containsIgnoreCase(searchKeyword)); + } + + long total = from(information) + .leftJoin(zoneCategoryChild).on(zoneCategoryChild.id.eq(information.zoneCategory.id)) + .leftJoin(zoneCategoryParent).on(zoneCategoryChild.parentZoneCategory.id.eq(zoneCategoryParent.id)) + .leftJoin(category).on(category.id.eq(information.category.id)) + .leftJoin(categoryParent).on(categoryParent.id.eq(category.parentCategory.id)) + .join(image).on(image.information.id.eq(information.id).and(image.imageStatus.eq(ImageStatus.THUMBNAIL))) + .where(whereClause) + .distinct() + .fetchCount(); + + List list = from(information) + .leftJoin(zoneCategoryChild).on(zoneCategoryChild.id.eq(information.zoneCategory.id)) + .leftJoin(zoneCategoryParent).on(zoneCategoryChild.parentZoneCategory.id.eq(zoneCategoryParent.id)) + .leftJoin(category).on(category.id.eq(information.category.id)) + .leftJoin(categoryParent).on(categoryParent.id.eq(category.parentCategory.id)) + .leftJoin(image).on(image.information.id.eq(information.id).and(image.imageStatus.eq(ImageStatus.THUMBNAIL))) + .leftJoin(bookMarkInformation).on(bookMarkInformation.information.id.eq(information.id).and(bookMarkInformation.user.id.eq(userId))) + .leftJoin(greatInformation).on(greatInformation.information.id.eq(information.id)) + .where(whereClause) + .groupBy(information.id, information.createdDate, information.viewCount, zoneCategoryChild.name, bookMarkInformation.id, image.address) + .orderBy(getOrderSpecifiers(informationPageRequest.getSort())) + .select(Projections.constructor( + InformationBriefResponse.class, + information.id, + information.title, + zoneCategoryParent.name, + zoneCategoryChild.name, + information.category.name, + information.viewCount, + isBookMarkBooleanExpression(bookMarkInformation), + image.address, + countGreatInformation(greatInformation), + isGreatBooleanExpression(userId, greatInformation) + )).offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + log.info("정보 페이지 네이션 총 갯수 : " + total + "\n"); + log.info("정보 들 : "); + log.info(list.toString()); + return new PageImpl<>(list, pageable, total); + } + + private OrderSpecifier getOrderSpecifiers(String sort) { + if (Objects.nonNull(sort)) { + if (Objects.equals(LIKE_COUNT_SORT, sort)) { + return countGreatInformation(greatInformation).desc(); + } else if (Objects.equals(VIEW_COUNT_SORT, sort)) { + return information.viewCount.desc(); + } + } + return information.createdDate.desc(); + } + + private BooleanExpression isBookMarkBooleanExpression(QBookMarkInformation bookMarkInformation) { + return new CaseBuilder() + .when(bookMarkInformation.id.isNotNull()) + .then(true) + .otherwise(false); + } + + private BooleanExpression isGreatBooleanExpression(Long userId, QGreatInformation greatInformation) { + return greatInformation.user.id.eq(userId).count().gt(0); + } + + private NumberExpression countGreatInformation(QGreatInformation greatInformation) { + return greatInformation.id.count().intValue(); + } + +// @Override +// public Page getInformationPageFilterAndOrder(Pageable pageable, +// InformationPageRequest informationPageRequest, +// Long userId, Long parentCategoryId) { +// BooleanBuilder whereClause = new BooleanBuilder(); +// +// if (Objects.nonNull(informationPageRequest.getZoneCategoryId())) { +// whereClause.and( +// information.zoneCategory.parentZoneCategory.id.eq(informationPageRequest.getZoneCategoryId())); +// } +// +// BooleanBuilder categoryCondition = new BooleanBuilder(); +// +// if (Objects.nonNull(informationPageRequest.getChildCategoryId())) { +// whereClause.and(information.category.id.eq(informationPageRequest.getChildCategoryId())); +// } else { +// categoryCondition.and(category.parentCategory.id.eq(parentCategoryId)); +// } +// +// if (Objects.nonNull(informationPageRequest.getSearch())) { +// String searchKeyword = informationPageRequest.getSearch().trim().replace(" ", ""); +// whereClause.and(information.title.trim().containsIgnoreCase(searchKeyword)); +// } +// +// +// long total = from(information) +// .where(whereClause) +// .select(information.id).fetchCount(); +// System.out.println("page 네이션 총 데이터 갯수 : " + total); +// List list = from(information) +// .join(zoneCategoryChild).on(zoneCategoryChild.id.eq(information.zoneCategory.id)) +// .leftJoin(zoneCategoryParent).on(zoneCategoryParent.id.eq(zoneCategoryChild.parentZoneCategory.id)) +// .leftJoin(image) +// .on(image.information.id.eq(information.id).and(image.imageStatus.eq(ImageStatus.THUMBNAIL))) +// .join(category).on(category.id.eq(information.category.id).and(categoryCondition)) +// .where(whereClause) +// .groupBy(information.id, zoneCategoryChild.id, zoneCategoryParent.id, image.id) +// .orderBy(getOrderSpecifier(informationPageRequest.getSort(), information.id)) +// .select(Projections.constructor( +// InformationBriefResponse.class, +// information.id, +// information.title, +// zoneCategoryParent.name, +// zoneCategoryChild.name, +// information.category.name, +// information.viewCount, +// isInformationBookmark(userId, information.id), +// image.address, +// countGreatInformationByInformationById(information.id), +// isUserGreatInformation(userId) +// )).offset(pageable.getOffset()) +// .limit(pageable.getPageSize()) +// .fetch(); +// System.out.println(list.size()); +// System.out.println(list); +// +// return new PageImpl<>(list, pageable, total); +// } + + + @Override + public List getInformationLikeCountFromCreatedIn3(Long userId) { + return from(information) + .leftJoin(zoneCategoryChild).on(zoneCategoryChild.id.eq(information.zoneCategory.id)) + .leftJoin(zoneCategoryParent).on(zoneCategoryParent.id.eq(zoneCategoryChild.parentZoneCategory.id)) + .leftJoin(image).on(image.information.id.eq(information.id).and(image.imageStatus.eq(ImageStatus.THUMBNAIL))) + .leftJoin(category).on(category.id.eq(information.category.id)) + .where(information.createdDate.after(LocalDateTime.now().minusMonths(3))) + .groupBy(information.id, zoneCategoryParent.name, zoneCategoryChild.name, image.address) + .orderBy(countGreatInformationByInformationByIdSubQuery(information.id).desc()) + .select(Projections.constructor( + InformationMainResponse.class, + information.id, + information.title, + zoneCategoryParent.name, + zoneCategoryChild.name, + category.parentCategory.name, + information.viewCount, + isInformationBookmarkSubQuery(userId, information.id), + image.address, + countGreatInformationByInformationByIdSubQuery(information.id), // 파라미터 전달 + isUserGreatInformationSubQuery(userId) + )).limit(6).fetch(); + } + + @Override + public List getInformationRecommend(Long informationId, Long childCategoryId, + Long userId) { + return from(information) + .join(zoneCategoryChild).on(zoneCategoryChild.id.eq(information.zoneCategory.id)) + .leftJoin(zoneCategoryParent).on(zoneCategoryParent.id.eq(zoneCategoryChild.parentZoneCategory.id)) + .leftJoin(image).on(image.information.id.eq(information.id) + .and(image.imageStatus.eq(ImageStatus.THUMBNAIL))) + .where(information.category.id.eq(childCategoryId).and(information.id.ne(informationId))) + .groupBy(information.id, zoneCategoryChild.id, zoneCategoryParent.id, image.id) + .orderBy(information.createdDate.desc()) + .select(Projections.constructor( + InformationBriefResponse.class, + information.id, + information.title, + zoneCategoryParent.name, + zoneCategoryChild.name, + information.category.name, + information.viewCount, + isInformationBookmarkSubQuery(userId, information.id), + image.address, + countGreatInformationByInformationByIdSubQuery(information.id), + isUserGreatInformationSubQuery(userId) + )) + .limit(3L) + .fetch(); + } + + @Override + public Page getInformationPageByTag(Pageable pageable, Long userId, Long + parentCategoryId, + InformationPageRequest informationPageRequest, + String decodedTag) { + BooleanBuilder whereClause = new BooleanBuilder(); + + if (Objects.nonNull(informationPageRequest.getZoneCategoryId())) { + whereClause.and( + information.zoneCategory.parentZoneCategory.id.eq(informationPageRequest.getZoneCategoryId())); + } + + BooleanBuilder categoryCondition = new BooleanBuilder(); + + if (Objects.nonNull(informationPageRequest.getChildCategoryId())) { + whereClause.and(information.category.id.eq(informationPageRequest.getChildCategoryId())); + } else { + categoryCondition.and(category.parentCategory.id.eq(parentCategoryId)); + } + + + long total = from(information) + .join(zoneCategoryChild).on(zoneCategoryChild.id.eq(information.zoneCategory.id)) + .leftJoin(zoneCategoryParent).on(zoneCategoryParent.id.eq(zoneCategoryChild.parentZoneCategory.id)) + .leftJoin(bookMarkInformation) + .on(bookMarkInformation.information.id.eq(information.id).and(bookMarkInformation.user.id.eq(userId))) + .leftJoin(image) + .on(image.information.id.eq(information.id).and(image.imageStatus.eq(ImageStatus.THUMBNAIL))) + .join(category).on(category.id.eq(information.category.id).and(categoryCondition)) + .leftJoin(infoTag) + .on(infoTag.information.id.eq(information.id)) + .where(whereClause.and(infoTag.information.id.eq(information.id).and(infoTag.tag.name.eq(decodedTag)))) + .select(information.count()).fetchCount(); + + List list = from(information) + .join(zoneCategoryChild).on(zoneCategoryChild.id.eq(information.zoneCategory.id)) + .leftJoin(zoneCategoryParent).on(zoneCategoryParent.id.eq(zoneCategoryChild.parentZoneCategory.id)) + .leftJoin(bookMarkInformation) + .on(bookMarkInformation.information.id.eq(information.id).and(bookMarkInformation.user.id.eq(userId))) + .leftJoin(image) + .on(image.information.id.eq(information.id).and(image.imageStatus.eq(ImageStatus.THUMBNAIL))) + .join(category).on(category.id.eq(information.category.id).and(categoryCondition)) + .leftJoin(greatInformation).on(greatInformation.information.id.eq(information.id)) + .leftJoin(infoTag) + .on(infoTag.information.id.eq(information.id)) + .where(whereClause) + .groupBy(information.id, zoneCategoryChild.id, zoneCategoryParent.id, image.id, infoTag.id) + .orderBy(getOrderSpecifier(informationPageRequest.getSort(), information.id)) + .select(Projections.constructor( + InformationBriefResponse.class, + information.id, + information.title, + zoneCategoryParent.name, + zoneCategoryChild.name, + information.category.name, + information.viewCount, + bookMarkInformation.user.id.isNotNull(), + image.address, + countGreatInformationByInformationByIdSubQuery(information.id), + isUserGreatInformationSubQuery(userId) + )).offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + return new PageImpl<>(list, pageable, total); + } + + @Override + public List getInformationRank() { + return from(information) + .leftJoin(greatInformation) + .on(greatInformation.information.id.eq(information.id)) + .groupBy(information.id, information.title) + .orderBy(countGreatInformationByInformationByIdSubQuery(information.id).desc()) + .limit(5) + .select(Projections.constructor( + InformationRankResponse.class, + information.id, + information.title + )).fetch(); + } + + private OrderSpecifier getOrderSpecifier(String sort, NumberPath informationId) { + if (Objects.nonNull(sort)) { + if (Objects.equals(LIKE_COUNT_SORT, sort)) { + return countGreatInformationByInformationByIdSubQuery(informationId).desc(); + } else if (Objects.equals(VIEW_COUNT_SORT, sort)) { + return information.viewCount.desc(); + } + } + return information.createdDate.desc(); + } + + private NumberExpression countGreatInformationByInformationByIdSubQuery(NumberPath informationId) { + QGreatInformation greatInformationSub = QGreatInformation.greatInformation; + JPQLQuery likeCountSubQuery = JPAExpressions + .select(greatInformationSub.count()) + .from(greatInformationSub) + .where(greatInformationSub.information.id.eq(informationId)); // 파라미터로 받은 NumberPath와 비교 + + return Expressions.numberTemplate(Long.class, "{0}", likeCountSubQuery) + .coalesce(0L) + .intValue(); + } + + private BooleanExpression isUserGreatInformationSubQuery(Long userId) { + return new CaseBuilder() + .when(JPAExpressions.selectOne() + .from(greatInformation) + .where(greatInformation.information.id.eq(information.id) + .and(greatInformation.user.id.eq(userId))) + .exists()) + .then(true) + .otherwise(false); + } + + private BooleanExpression isInformationBookmarkSubQuery(Long userId, NumberPath informationId) { + return new CaseBuilder() + .when(JPAExpressions.selectOne() + .from(bookMarkInformation) + .where(bookMarkInformation.information.id.eq(informationId) + .and(bookMarkInformation.user.id.eq(userId))) + .exists()) + .then(true) + .otherwise(false); + } + + +} diff --git a/src/main/java/solitour_backend/solitour/information/service/InformationService.java b/src/main/java/solitour_backend/solitour/information/service/InformationService.java new file mode 100644 index 0000000..f0b04ec --- /dev/null +++ b/src/main/java/solitour_backend/solitour/information/service/InformationService.java @@ -0,0 +1,548 @@ +package solitour_backend.solitour.information.service; + +import static solitour_backend.solitour.information.repository.InformationRepositoryCustom.LIKE_COUNT_SORT; +import static solitour_backend.solitour.information.repository.InformationRepositoryCustom.VIEW_COUNT_SORT; + +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import solitour_backend.solitour.book_mark_information.entity.BookMarkInformationRepository; +import solitour_backend.solitour.category.dto.mapper.CategoryMapper; +import solitour_backend.solitour.category.dto.response.CategoryResponse; +import solitour_backend.solitour.category.entity.Category; +import solitour_backend.solitour.category.exception.CategoryNotExistsException; +import solitour_backend.solitour.category.repository.CategoryRepository; +import solitour_backend.solitour.error.exception.RequestValidationFailedException; +import solitour_backend.solitour.great_information.repository.GreatInformationRepository; +import solitour_backend.solitour.image.dto.mapper.ImageMapper; +import solitour_backend.solitour.image.dto.request.ImageRequest; +import solitour_backend.solitour.image.dto.response.ImageResponse; +import solitour_backend.solitour.image.entity.Image; +import solitour_backend.solitour.image.exception.ImageAlreadyExistsException; +import solitour_backend.solitour.image.exception.ImageNotExistsException; +import solitour_backend.solitour.image.image_status.ImageStatus; +import solitour_backend.solitour.image.repository.ImageRepository; +import solitour_backend.solitour.image.s3.S3Uploader; +import solitour_backend.solitour.info_tag.entity.InfoTag; +import solitour_backend.solitour.info_tag.repository.InfoTagRepository; +import solitour_backend.solitour.information.dto.mapper.InformationMapper; +import solitour_backend.solitour.information.dto.request.InformationCreateRequest; +import solitour_backend.solitour.information.dto.request.InformationPageRequest; +import solitour_backend.solitour.information.dto.request.InformationUpdateRequest; +import solitour_backend.solitour.information.dto.response.InformationBriefResponse; +import solitour_backend.solitour.information.dto.response.InformationDetailResponse; +import solitour_backend.solitour.information.dto.response.InformationMainResponse; +import solitour_backend.solitour.information.dto.response.InformationRankResponse; +import solitour_backend.solitour.information.dto.response.InformationResponse; +import solitour_backend.solitour.information.entity.Information; +import solitour_backend.solitour.information.exception.InformationNotExistsException; +import solitour_backend.solitour.information.exception.InformationNotManageException; +import solitour_backend.solitour.information.repository.InformationRepository; +import solitour_backend.solitour.place.dto.mapper.PlaceMapper; +import solitour_backend.solitour.place.dto.request.PlaceModifyRequest; +import solitour_backend.solitour.place.dto.response.PlaceResponse; +import solitour_backend.solitour.place.entity.Place; +import solitour_backend.solitour.place.exception.PlaceNotExistsException; +import solitour_backend.solitour.place.repository.PlaceRepository; +import solitour_backend.solitour.tag.dto.mapper.TagMapper; +import solitour_backend.solitour.tag.dto.response.TagResponse; +import solitour_backend.solitour.tag.entity.Tag; +import solitour_backend.solitour.tag.repository.TagRepository; +import solitour_backend.solitour.user.dto.UserPostingResponse; +import solitour_backend.solitour.user.dto.mapper.UserMapper; +import solitour_backend.solitour.user.entity.User; +import solitour_backend.solitour.user.exception.UserNotExistsException; +import solitour_backend.solitour.user.repository.UserRepository; +import solitour_backend.solitour.user_image.entity.UserImage; +import solitour_backend.solitour.user_image.entity.UserImageRepository; +import solitour_backend.solitour.util.HmacUtils; +import solitour_backend.solitour.zone_category.dto.mapper.ZoneCategoryMapper; +import solitour_backend.solitour.zone_category.dto.response.ZoneCategoryResponse; +import solitour_backend.solitour.zone_category.entity.ZoneCategory; +import solitour_backend.solitour.zone_category.exception.ZoneCategoryNotExistsException; +import solitour_backend.solitour.zone_category.repository.ZoneCategoryRepository; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class InformationService { + + private final InformationRepository informationRepository; + private final CategoryRepository categoryRepository; + private final ZoneCategoryRepository zoneCategoryRepository; + private final PlaceRepository placeRepository; + private final TagRepository tagRepository; + private final TagMapper tagMapper; + private final InfoTagRepository infoTagRepository; + private final InformationMapper informationMapper; + private final UserRepository userRepository; + private final S3Uploader s3Uploader; + private final PlaceMapper placeMapper; + private final ZoneCategoryMapper zoneCategoryMapper; + private final ImageMapper imageMapper; + private final UserMapper userMapper; + private final GreatInformationRepository greatInformationRepository; + private final BookMarkInformationRepository bookMarkInformationRepository; + private final UserImageRepository userImageRepository; + private final ImageRepository imageRepository; + private final CategoryMapper categoryMapper; + + + @Transactional + public InformationResponse registerInformation(Long userId, InformationCreateRequest informationCreateRequest) { + Category category = categoryRepository.findById(informationCreateRequest.getCategoryId()) + .orElseThrow( + () -> new CategoryNotExistsException("해당하는 id의 category 가 없습니다")); + ZoneCategory parentZoneCategory = zoneCategoryRepository.findByName( + informationCreateRequest.getZoneCategoryNameParent()) + .orElseThrow(() -> new ZoneCategoryNotExistsException("해당하는 name의 ZoneCategory 없습니다")); + + ZoneCategory childZoneCategory = zoneCategoryRepository.findByParentZoneCategoryIdAndName( + parentZoneCategory.getId(), informationCreateRequest.getZoneCategoryNameChild()) + .orElseThrow(() -> new ZoneCategoryNotExistsException( + "해당하는 ParentZoneCategoryId 와 name의 ZoneCategory 없습니다")); + + Place savePlace = placeRepository.save( + new Place( + informationCreateRequest.getPlaceRegisterRequest().getSearchId(), + informationCreateRequest.getPlaceRegisterRequest().getName(), + informationCreateRequest.getPlaceRegisterRequest().getXAxis(), + informationCreateRequest.getPlaceRegisterRequest().getYAxis(), + informationCreateRequest.getPlaceRegisterRequest().getAddress()) + ); + + User user = userRepository.findById(userId) + .orElseThrow( + () -> new UserNotExistsException("해당하는 id의 User 가 없습니다")); + + Information information = + new Information( + category, + childZoneCategory, + user, + savePlace, + informationCreateRequest.getInformationTitle(), + informationCreateRequest.getInformationAddress(), + 0, + informationCreateRequest.getInformationContent(), + informationCreateRequest.getInformationTips() + ); + + Information saveInformation = informationRepository.save(information); + + List tags = tagMapper.mapToTags(informationCreateRequest.getTagRegisterRequests()); + List saveTags = tagRepository.saveAll(tags); + + for (Tag tag : saveTags) { + infoTagRepository.save(new InfoTag(tag, saveInformation)); + } + + Image thumbImage = new Image(ImageStatus.THUMBNAIL, saveInformation, informationCreateRequest.getThumbNailImageUrl()); + imageRepository.save(thumbImage); + + s3Uploader.markImagePermanent(thumbImage.getAddress()); + List contentImagesUrl = informationCreateRequest.getContentImagesUrl(); + + if (Objects.nonNull(contentImagesUrl) && !contentImagesUrl.isEmpty()) { + List contentImageList = new ArrayList<>(); + for (String contentImage : contentImagesUrl) { + contentImageList.add(new Image(ImageStatus.CONTENT, saveInformation, contentImage)); + s3Uploader.markImagePermanent(contentImage); + } + imageRepository.saveAll(contentImageList); + } + + + return informationMapper.mapToInformationResponse(saveInformation); + } + + @Transactional + public InformationDetailResponse getDetailInformation(Long userId, Long informationId, HttpServletRequest request, HttpServletResponse response) { + Information information = informationRepository.findById(informationId) + .orElseThrow( + () -> + new InformationNotExistsException("해당하는 id 의 information 이 존재하지 않습니다.")); + + List infoTags = infoTagRepository.findAllByInformationId(information.getId()); + + UserPostingResponse userPostingResponse = userMapper.mapToUserPostingResponse(information.getUser()); + + List tagResponses = new ArrayList<>(); + if (!infoTags.isEmpty()) { + tagResponses = infoTags.stream() + .map(data -> + tagMapper.mapToTagResponse(data.getTag())) + .toList(); + } + PlaceResponse placeResponse = placeMapper.mapToPlaceResponse(information.getPlace()); + ZoneCategoryResponse zoneCategoryResponse = zoneCategoryMapper.mapToZoneCategoryResponse(information.getZoneCategory()); + CategoryResponse categoryResponse = categoryMapper.mapToCategoryResponse(information.getCategory()); + + List images = imageRepository.findAllByInformationId(information.getId()); + + List imageResponseList = imageMapper.toImageResponseList(images); + + int likeCount = greatInformationRepository.countByInformationId(information.getId()); + + List informationRecommend = informationRepository.getInformationRecommend(information.getId(), information.getCategory().getId(), userId); + + boolean isLike = greatInformationRepository.existsByInformationIdAndUserId(information.getId(), userId); + + User user = information.getUser(); + + String userImageUrl = userImageRepository.findById(user.getId()) + .map(UserImage::getAddress) + .orElseGet( + () -> userRepository.getProfileUrl(user.getSex())); + + try { + updateViewCount(information, request, response, userId); + } catch (Exception e) { + throw new IllegalArgumentException(); + } + + return new InformationDetailResponse( + information.getTitle(), + information.getAddress(), + information.getCreatedDate(), + information.getViewCount(), + information.getContent(), + information.getTip(), + userPostingResponse, + tagResponses, + placeResponse, + zoneCategoryResponse, + categoryResponse, + imageResponseList, + likeCount, + userImageUrl, + isLike, + informationRecommend); + } + + @Transactional + public InformationResponse updateInformation(Long userId, Long informationId, InformationUpdateRequest informationUpdateRequest) { + Information information = getInformation(informationId, userId); + + updateInformationDetails(information, informationUpdateRequest); + updatePlaceDetails(information, informationUpdateRequest); + updateCategory(information, informationUpdateRequest); + updateZoneCategory(information, informationUpdateRequest); + updateTags(information, informationUpdateRequest); + handleImageDeletions(informationUpdateRequest); + handleImageAdditions(information, informationUpdateRequest); + updateThumbnail(information, informationUpdateRequest); + + return informationMapper.mapToInformationResponse(information); + } + + private Information getInformation(Long informationId, Long userId) { + Information information = informationRepository.findById(informationId) + .orElseThrow(() -> new InformationNotExistsException("해당하는 id의 information 이 존재하지 않습니다.")); + if (!Objects.equals(information.getUser().getId(), userId)) { + throw new InformationNotManageException("권한이 없습니다"); + } + return information; + } + + private void updateInformationDetails(Information information, InformationUpdateRequest request) { + information.setTitle(request.getTitle()); + information.setAddress(request.getAddress()); + information.setContent(request.getContent()); + information.setTip(request.getTips()); + } + + private void updatePlaceDetails(Information information, InformationUpdateRequest request) { + Place place = placeRepository.findById(information.getPlace().getId()) + .orElseThrow(() -> new PlaceNotExistsException("해당하는 information 의 place 에서의 id가 존재하지 않습니다")); + PlaceModifyRequest placeRequest = request.getPlaceModifyRequest(); + place.setName(placeRequest.getName()); + place.setAddress(placeRequest.getAddress()); + place.setXaxis(placeRequest.getXAxis()); + place.setYaxis(placeRequest.getYAxis()); + place.setSearchId(placeRequest.getSearchId()); + } + + private void updateCategory(Information information, InformationUpdateRequest request) { + Category category = categoryRepository.findById(request.getCategoryId()) + .orElseThrow(() -> new CategoryNotExistsException("해당하는 category Id 가 존재하지 않습니다.")); + + if (Objects.isNull(category.getParentCategory())) { + throw new RequestValidationFailedException("부모 카테고리는 등록이 안됩니다"); + } + information.setCategory(category); + } + + private void updateZoneCategory(Information information, InformationUpdateRequest request) { + ZoneCategory parentZoneCategory = zoneCategoryRepository.findByParentZoneCategoryIdAndName(null, request.getZoneCategoryNameParent()) + .orElseThrow(() -> new ZoneCategoryNotExistsException("해당하는 name 에 대한 zoneCategory 가 존재하지 않습니다")); + + ZoneCategory childZoneCategory = zoneCategoryRepository.findByParentZoneCategoryIdAndName( + parentZoneCategory.getId(), request.getZoneCategoryNameChild()) + .orElseThrow(() -> new ZoneCategoryNotExistsException("해당하는 name에 대한 zoneCategory가 존재하지 않습니다")); + + information.setZoneCategory(childZoneCategory); + } + + private void updateTags(Information information, InformationUpdateRequest request) { + List infoTags = infoTagRepository.findAllByInformationId(information.getId()); + + infoTagRepository.deleteAllByInformationId(information.getId()); + for (InfoTag infoTag : infoTags) { + tagRepository.deleteById(infoTag.getTag().getTagId()); + } + + List saveTags = tagRepository.saveAll(tagMapper.mapToTags(request.getTagRegisterRequests())); + for (Tag tag : saveTags) { + infoTagRepository.save(new InfoTag(tag, information)); + } + } + + private void handleImageDeletions(InformationUpdateRequest request) { + if (Objects.nonNull(request.getDeleteImagesUrl())) { + for (ImageRequest deleteImageUrl : request.getDeleteImagesUrl()) { + if (!imageRepository.existsImageByAddress(deleteImageUrl.getAddress())) { + throw new ImageNotExistsException("해당하는 이미지는 없습니다"); + } + imageRepository.deleteByAddress(deleteImageUrl.getAddress()); + } + } + } + + private void handleImageAdditions(Information information, InformationUpdateRequest request) { + if (Objects.nonNull(request.getNewContentImagesUrl())) { + List contentImageList = new ArrayList<>(); + for (ImageRequest newContentImageUrl : request.getNewContentImagesUrl()) { + contentImageList.add(new Image(ImageStatus.CONTENT, information, newContentImageUrl.getAddress())); + s3Uploader.markImagePermanent(newContentImageUrl.getAddress()); + } + imageRepository.saveAll(contentImageList); + } + } + + private void updateThumbnail(Information information, InformationUpdateRequest request) { + if (isInvalidThumbnailUpdate(request)) { + validateExistingThumbNailImage(information); + } else if (Objects.isNull(request.getNewThumbNailUrl())) { + handleThumbnailUpdateWithoutNewUrl(information, request); + } else { + handleThumbnailUpdateWithNewUrl(information, request); + } + } + + private boolean isInvalidThumbnailUpdate(InformationUpdateRequest request) { + return Objects.isNull(request.getNewThumbNailUrl()) + && Objects.isNull(request.getNewThumbNailFromContent()) + && Objects.isNull(request.getMoveThumbNailToContent()); + } + + private void validateExistingThumbNailImage(Information information) { + if (!imageRepository.existsImageByImageStatusAndInformationId(ImageStatus.THUMBNAIL, information.getId())) { + throw new ImageNotExistsException("썸네일 이미지가 없습니다"); + } + } + + private void handleThumbnailUpdateWithoutNewUrl(Information information, InformationUpdateRequest request) { + if (Objects.nonNull(request.getNewThumbNailFromContent()) && Objects.nonNull(request.getMoveThumbNailToContent())) { + swapThumbnailAndContent(information, request); + } else if (Objects.nonNull(request.getNewThumbNailFromContent())) { + setNewThumbnailFromContent(information, request); + } + } + + private void handleThumbnailUpdateWithNewUrl(Information information, InformationUpdateRequest request) { + if (Objects.nonNull(request.getMoveThumbNailToContent())) { + Image thumbNailImage = imageRepository.findImageByAddress(request.getMoveThumbNailToContent().getAddress()).orElseThrow(); + thumbNailImage.setImageStatus(ImageStatus.CONTENT); + } else { + if (imageRepository.existsImageByImageStatusAndInformationId(ImageStatus.THUMBNAIL, information.getId())) { + throw new ImageAlreadyExistsException("해당 정보에 대한 썸네일 이미지가 존재합니다"); + } + } + Image newImage = new Image(ImageStatus.THUMBNAIL, information, request.getNewThumbNailUrl().getAddress()); + imageRepository.save(newImage); + s3Uploader.markImagePermanent(newImage.getAddress()); + } + + private void swapThumbnailAndContent(Information information, InformationUpdateRequest request) { + validateExistingThumbNailImage(information); + Image content = imageRepository.findImageByAddress(request.getNewThumbNailFromContent().getAddress()).orElseThrow(); + content.setImageStatus(ImageStatus.THUMBNAIL); + Image thumbNail = imageRepository.findImageByAddress(request.getMoveThumbNailToContent().getAddress()).orElseThrow(); + thumbNail.setImageStatus(ImageStatus.CONTENT); + } + + private void setNewThumbnailFromContent(Information information, InformationUpdateRequest request) { + if (imageRepository.existsImageByImageStatusAndInformationId(ImageStatus.THUMBNAIL, information.getId())) { + throw new IllegalStateException("THUMBNAIL image already exists."); + } + Image content = imageRepository.findImageByAddress(request.getNewThumbNailFromContent().getAddress()).orElseThrow(); + content.setImageStatus(ImageStatus.THUMBNAIL); + } + + + @Transactional + public void deleteInformation(Long userId, Long id) { + Information information = informationRepository.findById(id) + .orElseThrow( + () -> + new InformationNotExistsException("해당하는 id의 information 이 존재하지 않습니다.")); + + if (!Objects.equals(information.getUser().getId(), userId)) { + throw new InformationNotManageException("권한이 없습니다"); + } + List infoTags = infoTagRepository.findAllByInformationId(information.getId()); + infoTagRepository.deleteAllByInformationId(information.getId()); + + for (InfoTag infoTag : infoTags) { + tagRepository.deleteById(infoTag.getTag().getTagId()); + } + + greatInformationRepository.deleteAllByInformationId(information.getId()); + + bookMarkInformationRepository.deleteAllByInformationId(information.getId()); + + List allByInformationId = imageRepository.findAllByInformationId(information.getId()); + + for (Image image : allByInformationId) { + s3Uploader.deleteImage(image.getAddress()); + } + + imageRepository.deleteAllByInformationId(information.getId()); + informationRepository.deleteById(id); + + } + + + public Page getPageInformation(Pageable pageable, Long userId, Long parentCategoryId, + InformationPageRequest informationPageRequest) { + if (!categoryRepository.existsByIdAndParentCategoryId(parentCategoryId, null)) { + throw new CategoryNotExistsException("해당하는 id의 부모 category 는 없습니다"); + } + + if (Objects.nonNull(informationPageRequest.getChildCategoryId())) { + Category category = categoryRepository.findById(informationPageRequest.getChildCategoryId()) + .orElseThrow( + () -> new CategoryNotExistsException("해당하는 id의 category 는 없습니다")); + + if (!Objects.equals(category.getParentCategory().getId(), parentCategoryId)) { + throw new RequestValidationFailedException("자식 카테고리의 부모 카테고리와 요청한 부모 카테고리가 다릅니다"); + } + } + + if (Objects.nonNull(informationPageRequest.getZoneCategoryId())) { + if (!zoneCategoryRepository.existsById(informationPageRequest.getZoneCategoryId())) { + throw new ZoneCategoryNotExistsException("해당하는 지역 카테고리가 없습니다"); + } + } + + if (Objects.nonNull(informationPageRequest.getSort())) { + if (!Objects.equals(LIKE_COUNT_SORT, informationPageRequest.getSort()) && !Objects.equals(VIEW_COUNT_SORT, + informationPageRequest.getSort())) { + throw new RequestValidationFailedException("잘못된 정렬 코드입니다."); + } + } + + return informationRepository.getPageInformationFilterAndOrder(pageable, informationPageRequest, userId, parentCategoryId); + } + + public List getRankInformation() { + return informationRepository.getInformationRank(); + } + + public List getMainPageInformation(Long userId) { + return informationRepository.getInformationLikeCountFromCreatedIn3(userId); + } + + public Page getPageInformationByTag(Pageable pageable, Long userId, Long parentCategoryId, + InformationPageRequest informationPageRequest, + String decodedTag) { + return informationRepository.getInformationPageByTag(pageable, userId, parentCategoryId, informationPageRequest, + decodedTag); + } + + public void updateViewCount(Information information, HttpServletRequest request, HttpServletResponse response, Long userId) throws Exception { + String cookieName = "viewed_informations"; + Cookie[] cookies = request.getCookies(); + Cookie postCookie = null; + + if (Objects.nonNull(cookies)) { + postCookie = Arrays.stream(cookies) + .filter(cookie -> cookieName.equals(cookie.getName())) + .findFirst() + .orElse(null); + } + + LocalDate now = LocalDate.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String cookieData = userId + "_" + information.getId() + "_" + now.format(formatter); + + if (Objects.nonNull(postCookie) && postCookie.getValue() != null) { + String[] informationDataArray = URLDecoder.decode(postCookie.getValue(), StandardCharsets.UTF_8).split(","); + boolean isUpdated = false; + boolean hasExistingData = false; + + for (int i = 0; i < informationDataArray.length; i++) { + String[] parts = informationDataArray[i].split("\\|"); + if (parts.length == 2) { + if (HmacUtils.verifyHmac(parts[0], parts[1])) { + String[] cookieInfo = parts[0].split("_"); + Long cookieUserId = Long.parseLong(cookieInfo[0]); + Long cookieGatheringId = Long.parseLong(cookieInfo[1]); + LocalDate lastViewedAt = LocalDate.parse(cookieInfo[2], formatter); + + if (cookieUserId.equals(userId) && cookieGatheringId.equals(information.getId())) { + hasExistingData = true; + if (lastViewedAt.isBefore(now.minusDays(1))) { + incrementInformationViewCount(information); + String newHmac = HmacUtils.generateHmac(cookieData); + informationDataArray[i] = cookieData + "|" + newHmac; + isUpdated = true; + } + break; + } + } + + } + } + + if (isUpdated || !hasExistingData) { + if (!hasExistingData) { + incrementInformationViewCount(information); + } + String hmac = HmacUtils.generateHmac(cookieData); + String updatedValue = String.join(",", informationDataArray) + "," + cookieData + "|" + hmac; + postCookie.setValue(URLEncoder.encode(updatedValue, StandardCharsets.UTF_8)); + postCookie.setPath("/"); + response.addCookie(postCookie); + } + } else { + incrementInformationViewCount(information); + String hmac = HmacUtils.generateHmac(cookieData); + Cookie newCookie = new Cookie(cookieName, URLEncoder.encode(cookieData + "|" + hmac, StandardCharsets.UTF_8)); + newCookie.setMaxAge(60 * 60 * 24); + newCookie.setPath("/"); + response.addCookie(newCookie); + } + } + + private void incrementInformationViewCount(Information information) { + information.upViewCount(); + } + +} diff --git a/src/main/java/solitour_backend/solitour/place/controller/PlaceController.java b/src/main/java/solitour_backend/solitour/place/controller/PlaceController.java new file mode 100644 index 0000000..1058035 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/place/controller/PlaceController.java @@ -0,0 +1,65 @@ +package solitour_backend.solitour.place.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import solitour_backend.solitour.error.Utils; +import solitour_backend.solitour.place.dto.request.PlaceModifyRequest; +import solitour_backend.solitour.place.dto.request.PlaceRegisterRequest; +import solitour_backend.solitour.place.dto.response.PlaceResponse; +import solitour_backend.solitour.place.service.PlaceService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/places") +public class PlaceController { + + private final PlaceService placeService; + + + @GetMapping("/{id}") + public ResponseEntity getPlace(@PathVariable("id") Long id) { + PlaceResponse placeResponse = placeService.getPlace(id); + + return ResponseEntity.status(HttpStatus.OK).body(placeResponse); + } + + @PostMapping + public ResponseEntity registerPlace( + @Valid @RequestBody PlaceRegisterRequest placeRegisterRequest, + BindingResult bindingResult) { + Utils.validationRequest(bindingResult); + PlaceResponse placeResponse = placeService.savePlace(placeRegisterRequest); + + return ResponseEntity.status(HttpStatus.CREATED).body(placeResponse); + } + + @PutMapping("/{id}") + public ResponseEntity modifyPlace(@PathVariable("id") Long id, + @Valid @RequestBody PlaceModifyRequest placeModifyRequest, + BindingResult bindingResult) { + Utils.validationRequest(bindingResult); + PlaceResponse placeResponse = placeService.updatePlace(id, placeModifyRequest); + + return ResponseEntity.status(HttpStatus.OK).body(placeResponse); + } + + @DeleteMapping("/{id}") + public ResponseEntity deletePlace(@PathVariable("id") Long id) { + placeService.deletePlace(id); + + return ResponseEntity.status(HttpStatus.ACCEPTED).build(); + } + + +} diff --git a/src/main/java/solitour_backend/solitour/place/dto/mapper/PlaceMapper.java b/src/main/java/solitour_backend/solitour/place/dto/mapper/PlaceMapper.java new file mode 100644 index 0000000..78692ef --- /dev/null +++ b/src/main/java/solitour_backend/solitour/place/dto/mapper/PlaceMapper.java @@ -0,0 +1,15 @@ +package solitour_backend.solitour.place.dto.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import solitour_backend.solitour.place.dto.response.PlaceResponse; +import solitour_backend.solitour.place.entity.Place; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface PlaceMapper { + + @Mapping(source = "xaxis", target = "xaxis") + @Mapping(source = "yaxis", target = "yaxis") + PlaceResponse mapToPlaceResponse(Place place); +} diff --git a/src/main/java/solitour_backend/solitour/place/dto/request/PlaceModifyRequest.java b/src/main/java/solitour_backend/solitour/place/dto/request/PlaceModifyRequest.java new file mode 100644 index 0000000..36c02f3 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/place/dto/request/PlaceModifyRequest.java @@ -0,0 +1,39 @@ +package solitour_backend.solitour.place.dto.request; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.std.NumberDeserializers; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class PlaceModifyRequest { + + @Nullable + @Size(min = 1, max = 30) + private String searchId; + + @NotBlank + @Size(min = 1, max = 30) + private String name; + + @NotNull + @JsonDeserialize(using = NumberDeserializers.BigDecimalDeserializer.class) + @Digits(integer = 10, fraction = 6) + private BigDecimal xAxis; + + @NotNull + @JsonDeserialize(using = NumberDeserializers.BigDecimalDeserializer.class) + @Digits(integer = 10, fraction = 6) + private BigDecimal yAxis; + + @NotBlank + @Size(min = 1, max = 50) + private String address; +} diff --git a/src/main/java/solitour_backend/solitour/place/dto/request/PlaceRegisterRequest.java b/src/main/java/solitour_backend/solitour/place/dto/request/PlaceRegisterRequest.java new file mode 100644 index 0000000..ca9d425 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/place/dto/request/PlaceRegisterRequest.java @@ -0,0 +1,41 @@ +package solitour_backend.solitour.place.dto.request; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.std.NumberDeserializers; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.math.BigDecimal; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class PlaceRegisterRequest { + + @Nullable + @Size(min = 1, max = 30) + private String searchId; + + @NotBlank + @Size(min = 1, max = 30) + private String name; + + @NotNull + @JsonDeserialize(using = NumberDeserializers.BigDecimalDeserializer.class) + @Digits(integer = 10, fraction = 7) + private BigDecimal xAxis; + + @NotNull + @JsonDeserialize(using = NumberDeserializers.BigDecimalDeserializer.class) + @Digits(integer = 10, fraction = 7) + private BigDecimal yAxis; + + @NotBlank + @Size(min = 1, max = 50) + private String address; +} diff --git a/src/main/java/solitour_backend/solitour/place/dto/response/PlaceResponse.java b/src/main/java/solitour_backend/solitour/place/dto/response/PlaceResponse.java new file mode 100644 index 0000000..61e397a --- /dev/null +++ b/src/main/java/solitour_backend/solitour/place/dto/response/PlaceResponse.java @@ -0,0 +1,17 @@ +package solitour_backend.solitour.place.dto.response; + +import java.math.BigDecimal; +import lombok.AllArgsConstructor; +import lombok.Getter; + + +@Getter +@AllArgsConstructor +public class PlaceResponse { + + private String searchId; + private String name; + private BigDecimal xaxis; + private BigDecimal yaxis; + private String address; +} \ No newline at end of file diff --git a/src/main/java/solitour_backend/solitour/place/entity/Place.java b/src/main/java/solitour_backend/solitour/place/entity/Place.java new file mode 100644 index 0000000..8e54daa --- /dev/null +++ b/src/main/java/solitour_backend/solitour/place/entity/Place.java @@ -0,0 +1,50 @@ +package solitour_backend.solitour.place.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table(name = "place") +@NoArgsConstructor +@AllArgsConstructor +public class Place { + + @Id + @Column(name = "place_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "place_search_id") + private String searchId; + + @Column(name = "place_name") + private String name; + + @Column(name = "place_x_axis") + private BigDecimal xaxis; + + @Column(name = "place_y_axis") + private BigDecimal yaxis; + + @Column(name = "place_address") + private String address; + + public Place(String searchId, String name, BigDecimal xaxis, BigDecimal yaxis, String address) { + this.searchId = searchId; + this.name = name; + this.xaxis = xaxis; + this.yaxis = yaxis; + this.address = address; + } +} \ No newline at end of file diff --git a/src/main/java/solitour_backend/solitour/place/exception/PlaceNotExistsException.java b/src/main/java/solitour_backend/solitour/place/exception/PlaceNotExistsException.java new file mode 100644 index 0000000..a7f3e86 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/place/exception/PlaceNotExistsException.java @@ -0,0 +1,8 @@ +package solitour_backend.solitour.place.exception; + +public class PlaceNotExistsException extends RuntimeException { + + public PlaceNotExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/place/repository/PlaceRepository.java b/src/main/java/solitour_backend/solitour/place/repository/PlaceRepository.java new file mode 100644 index 0000000..8f05901 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/place/repository/PlaceRepository.java @@ -0,0 +1,9 @@ +package solitour_backend.solitour.place.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import solitour_backend.solitour.place.entity.Place; + + +public interface PlaceRepository extends JpaRepository { + +} \ No newline at end of file diff --git a/src/main/java/solitour_backend/solitour/place/service/PlaceService.java b/src/main/java/solitour_backend/solitour/place/service/PlaceService.java new file mode 100644 index 0000000..49dfeed --- /dev/null +++ b/src/main/java/solitour_backend/solitour/place/service/PlaceService.java @@ -0,0 +1,65 @@ +package solitour_backend.solitour.place.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import solitour_backend.solitour.place.dto.mapper.PlaceMapper; +import solitour_backend.solitour.place.dto.request.PlaceModifyRequest; +import solitour_backend.solitour.place.dto.request.PlaceRegisterRequest; +import solitour_backend.solitour.place.dto.response.PlaceResponse; +import solitour_backend.solitour.place.entity.Place; +import solitour_backend.solitour.place.exception.PlaceNotExistsException; +import solitour_backend.solitour.place.repository.PlaceRepository; + +@Service +@Transactional +@RequiredArgsConstructor +public class PlaceService { + + private final PlaceRepository placeRepository; + private final PlaceMapper placeMapper; + + @Transactional(readOnly = true) + public PlaceResponse getPlace(Long id) { + Place place = placeRepository.findById(id) + .orElseThrow(() -> new PlaceNotExistsException("해당 하는 id의 Place가 존재하지 않습니다")); + + return placeMapper.mapToPlaceResponse(place); + } + + + public PlaceResponse savePlace(PlaceRegisterRequest placeRegisterRequest) { + Place place = new Place( + placeRegisterRequest.getSearchId(), + placeRegisterRequest.getName(), + placeRegisterRequest.getXAxis(), + placeRegisterRequest.getYAxis(), + placeRegisterRequest.getAddress() + ); + + Place savedPlace = placeRepository.save(place); + + return placeMapper.mapToPlaceResponse(savedPlace); + } + + + public PlaceResponse updatePlace(Long id, PlaceModifyRequest placeModifyRequest) { + Place savedPlace = placeRepository.findById(id) + .orElseThrow(() -> new PlaceNotExistsException("해당 하는 id의 Place가 존재하지 않습니다")); + + savedPlace.setSearchId(placeModifyRequest.getSearchId()); + savedPlace.setName(placeModifyRequest.getName()); + savedPlace.setXaxis(placeModifyRequest.getXAxis()); + savedPlace.setYaxis(placeModifyRequest.getYAxis()); + savedPlace.setAddress(placeModifyRequest.getAddress()); + + return placeMapper.mapToPlaceResponse(savedPlace); + } + + public void deletePlace(Long id) { + if (!placeRepository.existsById(id)) { + throw new PlaceNotExistsException("해당 하는 id의 Place가 존재하지 않습니다"); + } + placeRepository.deleteById(id); + } +} diff --git a/src/main/java/solitour_backend/solitour/tag/dto/mapper/TagMapper.java b/src/main/java/solitour_backend/solitour/tag/dto/mapper/TagMapper.java new file mode 100644 index 0000000..337e7a1 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/tag/dto/mapper/TagMapper.java @@ -0,0 +1,22 @@ +package solitour_backend.solitour.tag.dto.mapper; + +import java.util.List; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import solitour_backend.solitour.tag.dto.request.TagRegisterRequest; +import solitour_backend.solitour.tag.dto.response.TagResponse; +import solitour_backend.solitour.tag.entity.Tag; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface TagMapper { + + @Mapping(target = "tagId", ignore = true) + Tag mapToTag(TagRegisterRequest tagRegisterRequest); + + List mapToTags(List tagRegisterRequests); + + TagResponse mapToTagResponse(Tag tag); + + List mapToTagResponses(List tags); +} diff --git a/src/main/java/solitour_backend/solitour/tag/dto/request/TagRegisterRequest.java b/src/main/java/solitour_backend/solitour/tag/dto/request/TagRegisterRequest.java new file mode 100644 index 0000000..c991634 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/tag/dto/request/TagRegisterRequest.java @@ -0,0 +1,15 @@ +package solitour_backend.solitour.tag.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Getter +public class TagRegisterRequest { + + @NotBlank + @Size(min = 1, max = 40) + private String name; +} diff --git a/src/main/java/solitour_backend/solitour/tag/dto/response/TagResponse.java b/src/main/java/solitour_backend/solitour/tag/dto/response/TagResponse.java new file mode 100644 index 0000000..456644e --- /dev/null +++ b/src/main/java/solitour_backend/solitour/tag/dto/response/TagResponse.java @@ -0,0 +1,11 @@ +package solitour_backend.solitour.tag.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TagResponse { + + private String name; +} diff --git a/src/main/java/solitour_backend/solitour/tag/entity/Tag.java b/src/main/java/solitour_backend/solitour/tag/entity/Tag.java new file mode 100644 index 0000000..7bbcff1 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/tag/entity/Tag.java @@ -0,0 +1,31 @@ +package solitour_backend.solitour.tag.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Table(name = "tag") +@NoArgsConstructor +@Setter +public class Tag { + + @Id + @Column(name = "tag_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long tagId; + + @Column(name = "tag_name") + private String name; + + public Tag(String name) { + this.name = name; + } +} diff --git a/src/main/java/solitour_backend/solitour/tag/repository/TagRepository.java b/src/main/java/solitour_backend/solitour/tag/repository/TagRepository.java new file mode 100644 index 0000000..3aae4a8 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/tag/repository/TagRepository.java @@ -0,0 +1,8 @@ +package solitour_backend.solitour.tag.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import solitour_backend.solitour.tag.entity.Tag; + +public interface TagRepository extends JpaRepository { + +} diff --git a/src/main/java/solitour_backend/solitour/user/controller/UserController.java b/src/main/java/solitour_backend/solitour/user/controller/UserController.java new file mode 100644 index 0000000..8f586a2 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/controller/UserController.java @@ -0,0 +1,149 @@ +package solitour_backend.solitour.user.controller; + + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import solitour_backend.solitour.auth.config.Authenticated; +import solitour_backend.solitour.auth.config.AuthenticationPrincipal; +import solitour_backend.solitour.gathering.dto.response.GatheringApplicantResponse; +import solitour_backend.solitour.gathering.dto.response.GatheringMypageResponse; +import solitour_backend.solitour.information.dto.response.InformationBriefResponse; +import solitour_backend.solitour.user.dto.UpdateNicknameRequest; +import solitour_backend.solitour.user.dto.request.AgreeUserInfoRequest; +import solitour_backend.solitour.user.dto.request.DisagreeUserInfoRequest; +import solitour_backend.solitour.user.exception.NicknameAlreadyExistsException; +import solitour_backend.solitour.user.exception.UserNotExistsException; +import solitour_backend.solitour.user.service.UserService; +import solitour_backend.solitour.user.service.dto.response.UserInfoResponse; + +@Authenticated +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/users") +public class UserController { + + private final UserService userService; + public static final int PAGE_SIZE = 6; + + @GetMapping("/info") + public ResponseEntity retrieveUserInfo(@AuthenticationPrincipal Long userId) { + UserInfoResponse response = userService.retrieveUserInfo(userId); + + return ResponseEntity.ok(response); + } + + @PutMapping("/info/agree") + public ResponseEntity agreeUserInfo(@AuthenticationPrincipal Long userId, + @RequestBody AgreeUserInfoRequest request) { + userService.agreeUserInfo(userId, request); + + return ResponseEntity.noContent().build(); + } + + @PutMapping("/info/disagree") + public ResponseEntity disagreeUserInfo(@AuthenticationPrincipal Long userId, + @RequestBody DisagreeUserInfoRequest request) { + userService.disagreeUserInfo(userId, request); + + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/profile") + public ResponseEntity deleteUserProfile(@AuthenticationPrincipal Long userId) { + userService.deleteUserProfile(userId); + + return ResponseEntity.ok().build(); + } + + @PutMapping("/nickname") + public ResponseEntity updateNickname(@AuthenticationPrincipal Long userId, + @RequestBody UpdateNicknameRequest request) { + try { + userService.updateNickname(userId, request.nickname()); + return ResponseEntity.ok("Nickname updated successfully"); + } catch (UserNotExistsException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found"); + } catch (NicknameAlreadyExistsException e) { + return ResponseEntity.status(HttpStatus.CONFLICT).body("Nickname already exists"); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An internal error occurred"); + } + } + + @PutMapping("/profile") + public ResponseEntity updateUserProfile(@AuthenticationPrincipal Long userId, + @RequestPart(value = "userProfile", required = false) MultipartFile userProfile) { + userService.updateUserProfile(userId, userProfile); + + return ResponseEntity.ok().build(); + } + + @GetMapping("/mypage/information/owner") + public ResponseEntity> retrieveInformationOwner( + @RequestParam(defaultValue = "0") int page, + @AuthenticationPrincipal Long userId) { + Pageable pageable = PageRequest.of(page, PAGE_SIZE); + Page response = userService.retrieveInformationOwner(pageable, userId); + + return ResponseEntity.ok(response); + } + + @GetMapping("/mypage/information/bookmark") + public ResponseEntity> retrieveInformationBookmark( + @RequestParam(defaultValue = "0") int page, + @AuthenticationPrincipal Long userId) { + + Pageable pageable = PageRequest.of(page, PAGE_SIZE); + Page response = userService.retrieveInformationBookmark(pageable, + userId); + + return ResponseEntity.ok(response); + } + + @GetMapping("/mypage/gathering/host") + public ResponseEntity> retrieveGatheringHost( + @RequestParam(defaultValue = "0") int page, + @AuthenticationPrincipal Long userId) { + Pageable pageable = PageRequest.of(page, PAGE_SIZE); + Page response = userService.retrieveGatheringHost(pageable, userId); + + return ResponseEntity.ok(response); + } + + @GetMapping("/mypage/gathering/bookmark") + public ResponseEntity> retrieveGatheringBookmark( + @RequestParam(defaultValue = "0") int page, + @AuthenticationPrincipal Long userId) { + + Pageable pageable = PageRequest.of(page, PAGE_SIZE); + Page response = userService.retrieveGatheringBookmark(pageable, + userId); + + return ResponseEntity.ok(response); + } + + @GetMapping("/mypage/gathering/applicant") + public ResponseEntity> retrieveGatheringApplicant( + @RequestParam(defaultValue = "0") int page, + @AuthenticationPrincipal Long userId) { + + Pageable pageable = PageRequest.of(page, PAGE_SIZE); + Page response = userService.retrieveGatheringApplicant(pageable, + userId); + + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/solitour_backend/solitour/user/dto/UpdateAgeAndSex.java b/src/main/java/solitour_backend/solitour/user/dto/UpdateAgeAndSex.java new file mode 100644 index 0000000..d626992 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/dto/UpdateAgeAndSex.java @@ -0,0 +1,4 @@ +package solitour_backend.solitour.user.dto; + +public record UpdateAgeAndSex(String age, String sex) { +} diff --git a/src/main/java/solitour_backend/solitour/user/dto/UpdateNicknameRequest.java b/src/main/java/solitour_backend/solitour/user/dto/UpdateNicknameRequest.java new file mode 100644 index 0000000..2684559 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/dto/UpdateNicknameRequest.java @@ -0,0 +1,4 @@ +package solitour_backend.solitour.user.dto; + +public record UpdateNicknameRequest(String nickname) { +} diff --git a/src/main/java/solitour_backend/solitour/user/dto/UserPostingResponse.java b/src/main/java/solitour_backend/solitour/user/dto/UserPostingResponse.java new file mode 100644 index 0000000..5532426 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/dto/UserPostingResponse.java @@ -0,0 +1,12 @@ +package solitour_backend.solitour.user.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class UserPostingResponse { + + private Long id; + private String nickname; +} diff --git a/src/main/java/solitour_backend/solitour/user/dto/mapper/UserMapper.java b/src/main/java/solitour_backend/solitour/user/dto/mapper/UserMapper.java new file mode 100644 index 0000000..67f66d5 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/dto/mapper/UserMapper.java @@ -0,0 +1,17 @@ +package solitour_backend.solitour.user.dto.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import solitour_backend.solitour.user.dto.UserPostingResponse; +import solitour_backend.solitour.user.dto.response.UserGatheringResponse; +import solitour_backend.solitour.user.entity.User; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface UserMapper { + + UserPostingResponse mapToUserPostingResponse(User user); + + @Mapping(source = "userImage.address", target = "profileUrl") + UserGatheringResponse mapToUserGatheringResponse(User user); +} diff --git a/src/main/java/solitour_backend/solitour/user/dto/request/AgreeUserInfoRequest.java b/src/main/java/solitour_backend/solitour/user/dto/request/AgreeUserInfoRequest.java new file mode 100644 index 0000000..d1b2c5e --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/dto/request/AgreeUserInfoRequest.java @@ -0,0 +1,14 @@ +package solitour_backend.solitour.user.dto.request; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Getter +public class AgreeUserInfoRequest { + private Boolean termConditionAgreement; + private Boolean privacyPolicyAgreement; + private String name; + private String age; + private String sex; +} diff --git a/src/main/java/solitour_backend/solitour/user/dto/request/DisagreeUserInfoRequest.java b/src/main/java/solitour_backend/solitour/user/dto/request/DisagreeUserInfoRequest.java new file mode 100644 index 0000000..0acb13d --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/dto/request/DisagreeUserInfoRequest.java @@ -0,0 +1,11 @@ +package solitour_backend.solitour.user.dto.request; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Getter +public class DisagreeUserInfoRequest { + private Boolean termConditionAgreement; + private Boolean privacyPolicyAgreement; +} diff --git a/src/main/java/solitour_backend/solitour/user/dto/response/UserGatheringResponse.java b/src/main/java/solitour_backend/solitour/user/dto/response/UserGatheringResponse.java new file mode 100644 index 0000000..b8573ea --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/dto/response/UserGatheringResponse.java @@ -0,0 +1,14 @@ +package solitour_backend.solitour.user.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class UserGatheringResponse { + private Long id; + private String profileUrl; + private String nickname; + private Integer age; + private String sex; +} diff --git a/src/main/java/solitour_backend/solitour/user/entity/User.java b/src/main/java/solitour_backend/solitour/user/entity/User.java new file mode 100644 index 0000000..f5196a0 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/entity/User.java @@ -0,0 +1,124 @@ +package solitour_backend.solitour.user.entity; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +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.OneToOne; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import solitour_backend.solitour.auth.entity.Term; +import solitour_backend.solitour.user.dto.request.AgreeUserInfoRequest; +import solitour_backend.solitour.user.dto.request.DisagreeUserInfoRequest; +import solitour_backend.solitour.user.user_status.UserStatus; +import solitour_backend.solitour.user.user_status.UserStatusConverter; +import solitour_backend.solitour.user_image.entity.UserImage; + +@Entity +@Getter +@Builder +@Table(name = "user") +@AllArgsConstructor +@NoArgsConstructor +public class User { + + @Id + @Column(name = "user_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_status_id") + @Convert(converter = UserStatusConverter.class) + private UserStatus userStatus; + + @Column(name = "user_oauth_id") + private String oauthId; + + @Column(name = "provider") + private String provider; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_image_id") + private UserImage userImage; + + @OneToOne(mappedBy = "user",cascade = CascadeType.ALL) + private Term term; + + @Column(name = "user_nickname") + private String nickname; + + @Column(name = "user_name") + private String name; + + @Column(name = "user_age") + private Integer age; + + @Column(name = "user_sex") + private String sex; + + @Column(name = "user_email") + private String email; + + @Column(name = "user_phone_number") + private String phoneNumber; + + @Column(name = "is_admin") + private Boolean isAdmin; + + @Column(name = "user_latest_login_at") + private LocalDateTime latestLoginAt; + + @Column(name = "user_created_at") + private LocalDateTime createdAt; + + @Column(name = "user_deleted_at") + private LocalDateTime deletedAt; + + public void updateNickname(String nickname) { + this.nickname = nickname; + } + + public void updateAgeAndSex(String age, String sex) { + this.age = Integer.parseInt(age); + this.sex = sex; + } + + public void deleteUser() { + this.userStatus = UserStatus.DELETE; + this.oauthId = null; + this.provider = null; + this.name = null; + this.age = null; + this.phoneNumber = null; + this.email = null; + this.deletedAt = LocalDateTime.now(); + } + + public void updateUserImage(String imageUrl) { + this.userImage.updateUserImage(imageUrl); + } + + public void updateLoginTime() { + this.latestLoginAt = LocalDateTime.now(); + } + + public void agreeUserInfo(AgreeUserInfoRequest request) { + this.name = request.getName(); + this.userStatus = UserStatus.ACTIVATE; + this.age = Integer.valueOf(request.getAge()); + this.sex = request.getSex(); + } + + public void disagreeUserInfo(DisagreeUserInfoRequest request) { + this.userStatus = UserStatus.INACTIVATE; + } +} diff --git a/src/main/java/solitour_backend/solitour/user/exception/BlockedUserException.java b/src/main/java/solitour_backend/solitour/user/exception/BlockedUserException.java new file mode 100644 index 0000000..4587340 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/exception/BlockedUserException.java @@ -0,0 +1,8 @@ +package solitour_backend.solitour.user.exception; + +public class BlockedUserException extends RuntimeException { + + public BlockedUserException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/user/exception/DeletedUserException.java b/src/main/java/solitour_backend/solitour/user/exception/DeletedUserException.java new file mode 100644 index 0000000..1daf94b --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/exception/DeletedUserException.java @@ -0,0 +1,8 @@ +package solitour_backend.solitour.user.exception; + +public class DeletedUserException extends RuntimeException { + + public DeletedUserException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/user/exception/DormantUserException.java b/src/main/java/solitour_backend/solitour/user/exception/DormantUserException.java new file mode 100644 index 0000000..d93ac89 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/exception/DormantUserException.java @@ -0,0 +1,8 @@ +package solitour_backend.solitour.user.exception; + +public class DormantUserException extends RuntimeException { + + public DormantUserException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/user/exception/NicknameAlreadyExistsException.java b/src/main/java/solitour_backend/solitour/user/exception/NicknameAlreadyExistsException.java new file mode 100644 index 0000000..b874213 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/exception/NicknameAlreadyExistsException.java @@ -0,0 +1,7 @@ +package solitour_backend.solitour.user.exception; + +public class NicknameAlreadyExistsException extends RuntimeException { + public NicknameAlreadyExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/user/exception/UserNotExistsException.java b/src/main/java/solitour_backend/solitour/user/exception/UserNotExistsException.java new file mode 100644 index 0000000..6f160a0 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/exception/UserNotExistsException.java @@ -0,0 +1,8 @@ +package solitour_backend.solitour.user.exception; + +public class UserNotExistsException extends RuntimeException { + + public UserNotExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/user/repository/UserRepository.java b/src/main/java/solitour_backend/solitour/user/repository/UserRepository.java new file mode 100644 index 0000000..e518bdb --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/repository/UserRepository.java @@ -0,0 +1,17 @@ +package solitour_backend.solitour.user.repository; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import solitour_backend.solitour.user.entity.User; + +public interface UserRepository extends JpaRepository, UserRepositoryCustom { + + @Query("SELECT u FROM User u JOIN FETCH u.userImage WHERE u.id = :userId AND u.userStatus NOT IN ('차단', '휴먼','삭제')") + User findByUserId(Long userId); + + @Query("SELECT u FROM User u JOIN FETCH u.userImage WHERE u.oauthId = :oauthId AND u.userStatus NOT IN ('차단', '휴먼','삭제')") + Optional findByOauthId(String oauthId); + + boolean existsByNickname(String nickname); +} diff --git a/src/main/java/solitour_backend/solitour/user/repository/UserRepositoryCustom.java b/src/main/java/solitour_backend/solitour/user/repository/UserRepositoryCustom.java new file mode 100644 index 0000000..3276509 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/repository/UserRepositoryCustom.java @@ -0,0 +1,25 @@ +package solitour_backend.solitour.user.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.NoRepositoryBean; +import solitour_backend.solitour.gathering.dto.response.GatheringApplicantResponse; +import solitour_backend.solitour.gathering.dto.response.GatheringMypageResponse; +import solitour_backend.solitour.information.dto.response.InformationBriefResponse; + + +@NoRepositoryBean +public interface UserRepositoryCustom { + + Page retrieveInformationOwner(Pageable pageable, Long userId); + + Page retrieveInformationBookmark(Pageable pageable, Long userId); + + Page retrieveGatheringHost(Pageable pageable, Long userId); + + Page retrieveGatheringBookmark(Pageable pageable, Long userId); + + Page retrieveGatheringApplicant(Pageable pageable, Long userId); + + String getProfileUrl(String gender); +} diff --git a/src/main/java/solitour_backend/solitour/user/repository/UserRepositoryImpl.java b/src/main/java/solitour_backend/solitour/user/repository/UserRepositoryImpl.java new file mode 100644 index 0000000..0ee5b1d --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/repository/UserRepositoryImpl.java @@ -0,0 +1,369 @@ +package solitour_backend.solitour.user.repository; + +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.core.types.dsl.StringExpression; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; +import java.util.List; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; +import solitour_backend.solitour.book_mark_gathering.entity.QBookMarkGathering; +import solitour_backend.solitour.book_mark_information.entity.QBookMarkInformation; +import solitour_backend.solitour.gathering.dto.response.GatheringApplicantResponse; +import solitour_backend.solitour.gathering.dto.response.GatheringMypageResponse; +import solitour_backend.solitour.gathering.entity.Gathering; +import solitour_backend.solitour.gathering.entity.QGathering; +import solitour_backend.solitour.gathering_applicants.entity.GatheringStatus; +import solitour_backend.solitour.gathering_applicants.entity.QGatheringApplicants; +import solitour_backend.solitour.gathering_category.entity.QGatheringCategory; +import solitour_backend.solitour.great_gathering.entity.QGreatGathering; +import solitour_backend.solitour.great_information.entity.QGreatInformation; +import solitour_backend.solitour.image.entity.QImage; +import solitour_backend.solitour.image.image_status.ImageStatus; +import solitour_backend.solitour.information.dto.response.InformationBriefResponse; +import solitour_backend.solitour.information.entity.Information; +import solitour_backend.solitour.information.entity.QInformation; +import solitour_backend.solitour.place.entity.QPlace; +import solitour_backend.solitour.user.entity.User; +import solitour_backend.solitour.zone_category.entity.QZoneCategory; + +public class UserRepositoryImpl extends QuerydslRepositorySupport implements UserRepositoryCustom { + + public UserRepositoryImpl() { + super(User.class); + } + + @Value("${user.profile.url.male}") + private String maleProfileUrl; + + @Value("${user.profile.url.female}") + private String femaleProfileUrl; + + QInformation information = QInformation.information; + QZoneCategory zoneCategoryChild = QZoneCategory.zoneCategory; + QZoneCategory zoneCategoryParent = new QZoneCategory("zoneCategoryParent"); + QBookMarkInformation bookMarkInformation = QBookMarkInformation.bookMarkInformation; + QImage image = QImage.image; + QGreatInformation greatInformation = QGreatInformation.greatInformation; + QGathering gathering = QGathering.gathering; + QGatheringCategory gatheringCategory = QGatheringCategory.gatheringCategory; + QPlace place = QPlace.place; + QBookMarkGathering bookMarkGathering = QBookMarkGathering.bookMarkGathering; + QGatheringApplicants gatheringApplicants = QGatheringApplicants.gatheringApplicants; + QGreatGathering greatGathering = QGreatGathering.greatGathering; + + @Override + public Page retrieveInformationOwner(Pageable pageable, Long userId) { + JPQLQuery query = from(information) + .leftJoin(bookMarkInformation) + .on(bookMarkInformation.information.id.eq(information.id)) + .leftJoin(zoneCategoryParent) + .on(zoneCategoryParent.id.eq(information.zoneCategory.parentZoneCategory.id)) + .leftJoin(image).on(image.information.id.eq(information.id) + .and(image.imageStatus.eq(ImageStatus.THUMBNAIL))) + .leftJoin(greatInformation).on(greatInformation.information.id.eq(information.id)) + .where(information.user.id.eq(userId)); + + List list = query + .groupBy(information.id, zoneCategoryParent.id, zoneCategoryChild.id, + information.id, bookMarkInformation.id, image.id) + .orderBy(information.createdDate.desc()) + .select(Projections.constructor( + InformationBriefResponse.class, + information.id, + information.title, + zoneCategoryParent.name, + zoneCategoryChild.name, + information.category.name, + information.viewCount, + bookMarkInformation.user.id.isNotNull(), + image.address, + greatInformation.information.count().coalesce(0L).intValue(), + isUserGreatInformation(userId) + )) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + long total = query.fetchCount(); + + return new PageImpl<>(list, pageable, total); + } + + @Override + public Page retrieveInformationBookmark(Pageable pageable, Long userId) { + JPQLQuery query = from(information) + .leftJoin(bookMarkInformation) + .on(bookMarkInformation.information.id.eq(information.id)) + .leftJoin(zoneCategoryParent) + .on(zoneCategoryParent.id.eq(information.zoneCategory.parentZoneCategory.id)) + .leftJoin(image).on(image.information.id.eq(information.id) + .and(image.imageStatus.eq(ImageStatus.THUMBNAIL))) + .leftJoin(greatInformation).on(greatInformation.information.id.eq(information.id)) + .where(bookMarkInformation.user.id.eq(userId)); + + List list = query + .groupBy(information.id, zoneCategoryParent.id, zoneCategoryChild.id, + information.id, bookMarkInformation.id, image.id) + .orderBy(information.createdDate.desc()) + .select(Projections.constructor( + InformationBriefResponse.class, + information.id, + information.title, + zoneCategoryParent.name, + zoneCategoryChild.name, + information.category.name, + information.viewCount, + bookMarkInformation.user.id.isNotNull(), + image.address, + greatInformation.information.count().coalesce(0L).intValue(), + isUserGreatInformation(userId) + )) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + long total = query.fetchCount(); + + return new PageImpl<>(list, pageable, total); + } + + @Override + public Page retrieveGatheringHost(Pageable pageable, Long userId) { + NumberExpression likeCount = countGreatGatheringByGatheringById(); + BooleanExpression isBookMark = isGatheringBookmark(userId); + + JPQLQuery query = from(gathering) + .leftJoin(zoneCategoryParent) + .on(zoneCategoryParent.id.eq(gathering.zoneCategory.parentZoneCategory.id)) + .leftJoin(gatheringCategory) + .on(gatheringCategory.id.eq(gathering.gatheringCategory.id)) + .leftJoin(gatheringApplicants) + .on(gatheringApplicants.gathering.id.eq(gathering.id) + .and(gatheringApplicants.gatheringStatus.eq(GatheringStatus.CONSENT))) + .orderBy(gathering.createdAt.desc()) + .where(gathering.user.id.eq(userId).and(gathering.isDeleted.eq(false))); + + List list = query + .groupBy(gathering.id, zoneCategoryParent.id, zoneCategoryChild.id, + gatheringCategory.id) + .select(Projections.constructor( + GatheringMypageResponse.class, + gathering.id, + gathering.title, + zoneCategoryParent.name, + zoneCategoryChild.name, + gathering.viewCount, + isBookMark, + likeCount, + gatheringCategory.name, + gathering.user.nickname, + gathering.scheduleStartDate, + gathering.scheduleEndDate, + gathering.deadline, + gathering.allowedSex, + gathering.startAge, + gathering.endAge, + gathering.personCount, + gatheringApplicants.count().coalesce(0L).intValue(), + isUserGreatGathering(userId), + gathering.isFinish + )) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + long total = query.fetchCount(); + + return new PageImpl<>(list, pageable, total); + } + + @Override + public Page retrieveGatheringBookmark(Pageable pageable, Long userId) { + NumberExpression likeCount = countGreatGatheringByGatheringById(); + BooleanExpression isBookMark = isGatheringBookmark(userId); + + JPQLQuery query = from(gathering) + .leftJoin(zoneCategoryParent) + .on(zoneCategoryParent.id.eq(gathering.zoneCategory.parentZoneCategory.id)) + .leftJoin(gatheringCategory) + .on(gatheringCategory.id.eq(gathering.gatheringCategory.id)) + .leftJoin(gatheringApplicants) + .on(gatheringApplicants.gathering.id.eq(gathering.id) + .and(gatheringApplicants.gatheringStatus.eq(GatheringStatus.CONSENT))) + .leftJoin(bookMarkGathering) + .on(bookMarkGathering.gathering.id.eq(gathering.id)) + .orderBy(gathering.createdAt.desc()) + .where(bookMarkGathering.user.id.eq(userId).and(gathering.isDeleted.eq(false))); + + List list = query + .groupBy(gathering.id, zoneCategoryParent.id, zoneCategoryChild.id, + gatheringCategory.id) + .select(Projections.constructor( + GatheringMypageResponse.class, + gathering.id, + gathering.title, + zoneCategoryParent.name, + zoneCategoryChild.name, + gathering.viewCount, + isBookMark, + likeCount, + gatheringCategory.name, + gathering.user.nickname, + gathering.scheduleStartDate, + gathering.scheduleEndDate, + gathering.deadline, + gathering.allowedSex, + gathering.startAge, + gathering.endAge, + gathering.personCount, + gatheringApplicants.count().coalesce(0L).intValue(), + isUserGreatGathering(userId), + gathering.isFinish + )) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + long total = query.fetchCount(); + + return new PageImpl<>(list, pageable, total); + } + + @Override + public Page retrieveGatheringApplicant(Pageable pageable, Long userId) { + NumberExpression likeCount = countGreatGatheringByGatheringById(); + BooleanExpression isBookMark = isGatheringBookmark(userId); + StringExpression gatheringStatus = getGatheringStatus(); + NumberExpression gatheringApplicantCount = countGatheringApplicant(gathering.id); + + JPQLQuery query = from(gathering) + .leftJoin(zoneCategoryParent) + .on(zoneCategoryParent.id.eq(gathering.zoneCategory.parentZoneCategory.id)) + .leftJoin(gatheringCategory) + .on(gatheringCategory.id.eq(gathering.gatheringCategory.id)) + .leftJoin(gatheringApplicants) + .on(gatheringApplicants.gathering.id.eq(gathering.id)) + .orderBy(gathering.createdAt.desc()) + .where(gatheringApplicants.user.id.eq(userId).and(gathering.user.id.eq(userId).not()).and(gathering.isDeleted.eq(false))); + + List list = query + .groupBy(gathering.id, zoneCategoryParent.id, zoneCategoryChild.id, + gatheringCategory.id, gatheringApplicants.id) + .select(Projections.constructor( + GatheringApplicantResponse.class, + gathering.id, + gathering.title, + zoneCategoryParent.name, + zoneCategoryChild.name, + gathering.viewCount, + isBookMark, + likeCount, + gatheringCategory.name, + gathering.user.nickname, + gathering.scheduleStartDate, + gathering.scheduleEndDate, + gathering.deadline, + gathering.allowedSex, + gathering.startAge, + gathering.endAge, + gathering.personCount, + gatheringApplicantCount, + isUserGreatGathering(userId), + gatheringStatus, + gathering.isFinish + )) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + long total = query.fetchCount(); + + return new PageImpl<>(list, pageable, total); + } + + private NumberExpression countGatheringApplicant(NumberPath gatheringId) { + QGatheringApplicants gatheringApplicants = QGatheringApplicants.gatheringApplicants; + + JPQLQuery countApplicant = JPAExpressions + .select(gatheringApplicants.count().intValue()) + .from(gatheringApplicants) + .where(gatheringApplicants.gathering.id.eq(gatheringId) + .and(gatheringApplicants.gatheringStatus.eq(GatheringStatus.CONSENT))); + + return Expressions.numberTemplate(Integer.class, "{0}", countApplicant) + .coalesce(0); + } + + @Override + public String getProfileUrl(String gender) { + if ("male".equalsIgnoreCase(gender)) { + return maleProfileUrl; + } else if ("female".equalsIgnoreCase(gender)) { + return femaleProfileUrl; + } + return null; // Or return a default URL + } + + private StringExpression getGatheringStatus() { + QGatheringApplicants gatheringApplicants = QGatheringApplicants.gatheringApplicants; + return new CaseBuilder() + .when(gatheringApplicants.gatheringStatus.eq(GatheringStatus.WAIT)) + .then("WAIT") + .when(gatheringApplicants.gatheringStatus.eq(GatheringStatus.CONSENT)) + .then("CONSENT") + .otherwise("REFUSE"); + } + + private NumberExpression countGreatGatheringByGatheringById() { + QGreatGathering greatGatheringSub = QGreatGathering.greatGathering; + JPQLQuery likeCountSubQuery = JPAExpressions + .select(greatGatheringSub.count()) + .from(greatGatheringSub) + .where(greatGatheringSub.gathering.id.eq(gathering.id)); + + return Expressions.numberTemplate(Long.class, "{0}", likeCountSubQuery) + .coalesce(0L) + .intValue(); + } + + private BooleanExpression isUserGreatInformation(Long userId) { + return new CaseBuilder() + .when(JPAExpressions.selectOne() + .from(greatInformation) + .where(greatInformation.information.id.eq(information.id) + .and(greatInformation.user.id.eq(userId))) + .exists()) + .then(true) + .otherwise(false); + } + + private BooleanExpression isUserGreatGathering(Long userId) { + return new CaseBuilder() + .when(JPAExpressions.selectOne() + .from(greatGathering) + .where(greatGathering.gathering.id.eq(gathering.id) + .and(greatGathering.user.id.eq(userId))) + .exists()) + .then(true) + .otherwise(false); + } + + private BooleanExpression isGatheringBookmark(Long userId) { + return new CaseBuilder() + .when(JPAExpressions.selectOne() + .from(bookMarkGathering) + .where(bookMarkGathering.gathering.id.eq(gathering.id) + .and(bookMarkGathering.user.id.eq(userId))) + .exists()) + .then(true) + .otherwise(false); + } + + +} diff --git a/src/main/java/solitour_backend/solitour/user/service/UserService.java b/src/main/java/solitour_backend/solitour/user/service/UserService.java new file mode 100644 index 0000000..03d06b0 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/service/UserService.java @@ -0,0 +1,145 @@ +package solitour_backend.solitour.user.service; + +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import solitour_backend.solitour.auth.entity.Term; +import solitour_backend.solitour.auth.entity.TermRepository; +import solitour_backend.solitour.gathering.dto.response.GatheringApplicantResponse; +import solitour_backend.solitour.gathering.dto.response.GatheringMypageResponse; +import solitour_backend.solitour.image.s3.S3Uploader; +import solitour_backend.solitour.information.dto.response.InformationBriefResponse; +import solitour_backend.solitour.user.dto.request.AgreeUserInfoRequest; +import solitour_backend.solitour.user.dto.request.DisagreeUserInfoRequest; +import solitour_backend.solitour.user.entity.User; +import solitour_backend.solitour.user.exception.NicknameAlreadyExistsException; +import solitour_backend.solitour.user.repository.UserRepository; +import solitour_backend.solitour.user.service.dto.response.UserInfoResponse; +import solitour_backend.solitour.user_image.dto.UserImageResponse; +import solitour_backend.solitour.user_image.service.UserImageService; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserService { + + private final UserRepository userRepository; + private final UserImageService userImageService; + private final TermRepository termRepository; + private final S3Uploader s3Uploader; + @Value("${user.profile.url.female}") + private String femaleProfileUrl; + @Value("${user.profile.url.male}") + private String maleProfileUrl; + @Value("${user.profile.url.none}") + private String noneProfileUrl; + + public UserInfoResponse retrieveUserInfo(Long userId) { + User user = userRepository.findByUserId(userId); + + return new UserInfoResponse(user); + } + + @Transactional + public void updateNickname(Long userId, String nickname) { + if (userRepository.existsByNickname(nickname)) { + throw new NicknameAlreadyExistsException("이미 존재하는 닉네임입니다."); + } + User user = userRepository.findByUserId(userId); + user.updateNickname(nickname); + } + + public Page retrieveInformationOwner(Pageable pageable, Long userId) { + return userRepository.retrieveInformationOwner(pageable, userId); + } + + public Page retrieveInformationBookmark(Pageable pageable, Long userId) { + return userRepository.retrieveInformationBookmark(pageable, userId); + } + + @Transactional + public void updateUserProfile(Long userId, MultipartFile userProfile) { + UserImageResponse response = userImageService.updateUserProfile(userId, userProfile); + User user = userRepository.findByUserId(userId); + checkUserProfile(user.getUserImage().getAddress()); + user.updateUserImage(response.getImageUrl()); + } + + @Transactional + public void deleteUserProfile(Long userId) { + User user = userRepository.findByUserId(userId); + resetUserProfile(user, user.getUserImage().getAddress(), user.getSex()); + } + + public Page retrieveGatheringHost(Pageable pageable, Long userId) { + return userRepository.retrieveGatheringHost(pageable, userId); + } + + public Page retrieveGatheringBookmark(Pageable pageable, Long userId) { + return userRepository.retrieveGatheringBookmark(pageable, userId); + } + + public Page retrieveGatheringApplicant(Pageable pageable, Long userId) { + return userRepository.retrieveGatheringApplicant(pageable, userId); + } + + @Transactional + public void agreeUserInfo(Long userId, AgreeUserInfoRequest request) { + User user = userRepository.findByUserId(userId); + changeUserProfile(user, request); + if(!termRepository.findByUser(user).isPresent()){ + saveTerm(user, request.getTermConditionAgreement(),request.getPrivacyPolicyAgreement()); + } + user.agreeUserInfo(request); + } + + @Transactional + public void disagreeUserInfo(Long userId, DisagreeUserInfoRequest request) { + User user = userRepository.findByUserId(userId); + saveTerm(user, request.getTermConditionAgreement(),request.getPrivacyPolicyAgreement()); + user.disagreeUserInfo(request); + } + + private void saveTerm(User user, Boolean termCondition, Boolean termPrivacy) { + Term term = Term.builder() + .user(user) + .termCondition(termCondition) + .termPrivacy(termPrivacy) + .createdAt(LocalDateTime.now()) + .build(); + termRepository.save(term); + } + + private void changeUserProfile(User user, AgreeUserInfoRequest request) { + String sex = request.getSex(); + + if(user.getUserImage().equals(noneProfileUrl) && sex.equals("male")){ + user.updateUserImage(maleProfileUrl); + } else if (user.getUserImage().equals(noneProfileUrl) && sex.equals("female")) { + user.updateUserImage(femaleProfileUrl); + } + } + + private void resetUserProfile(User user, String imageUrl, String sex) { + checkUserProfile(imageUrl); + if (sex.equals("male")) { + user.updateUserImage(maleProfileUrl); + } else if (sex.equals("female")) { + user.updateUserImage(femaleProfileUrl); + } else { + user.updateUserImage(noneProfileUrl); + } + } + + private void checkUserProfile(String imageUrl) { + if (imageUrl.equals(femaleProfileUrl) || imageUrl.equals(maleProfileUrl) || imageUrl.equals(noneProfileUrl)) { + return; + } + s3Uploader.deleteImage(imageUrl); + } +} diff --git a/src/main/java/solitour_backend/solitour/user/service/dto/response/UserInfoResponse.java b/src/main/java/solitour_backend/solitour/user/service/dto/response/UserInfoResponse.java new file mode 100644 index 0000000..ff2b475 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/service/dto/response/UserInfoResponse.java @@ -0,0 +1,36 @@ +package solitour_backend.solitour.user.service.dto.response; + +import java.time.LocalDateTime; +import lombok.Getter; +import solitour_backend.solitour.user.entity.User; +import solitour_backend.solitour.user_image.entity.UserImage; + +@Getter +public class UserInfoResponse { + + private final Long id; + private final String userStatus; + private final UserImage userImage; + private final String nickname; + private final Integer age; + private final String sex; + private final String email; + private final String provider; + private final String phoneNumber; + private final LocalDateTime createdAt; + private final Boolean isAdmin; + + public UserInfoResponse(User user) { + this.id = user.getId(); + this.userStatus = user.getUserStatus().getName(); + this.userImage = user.getUserImage(); + this.nickname = user.getNickname(); + this.age = user.getAge(); + this.sex = user.getSex(); + this.email = user.getEmail(); + this.provider = user.getProvider(); + this.phoneNumber = user.getPhoneNumber(); + this.createdAt = user.getCreatedAt(); + this.isAdmin = user.getIsAdmin(); + } +} diff --git a/src/main/java/solitour_backend/solitour/user/user_status/UserStatus.java b/src/main/java/solitour_backend/solitour/user/user_status/UserStatus.java new file mode 100644 index 0000000..a18f459 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/user_status/UserStatus.java @@ -0,0 +1,28 @@ +package solitour_backend.solitour.user.user_status; + +import java.util.Arrays; +import lombok.Getter; + +@Getter +public enum UserStatus { + PENDING("대기"), + ACTIVATE("활성화"), + INACTIVATE("비활성화"), + BLOCK("차단"), + DORMANT("휴먼"), + DELETE("삭제"), + MANAGER("관리자"); + + private final String name; + + UserStatus(String name) { + this.name = name; + } + + public static UserStatus fromName(String name) { + return Arrays.stream(UserStatus.values()) + .filter(e -> e.getName().equals(name)) + .findAny() + .orElse(null); + } +} diff --git a/src/main/java/solitour_backend/solitour/user/user_status/UserStatusConverter.java b/src/main/java/solitour_backend/solitour/user/user_status/UserStatusConverter.java new file mode 100644 index 0000000..cbc3b3d --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user/user_status/UserStatusConverter.java @@ -0,0 +1,18 @@ +package solitour_backend.solitour.user.user_status; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class UserStatusConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(UserStatus userStatus) { + return userStatus.getName(); + } + + @Override + public UserStatus convertToEntityAttribute(String dbData) { + return UserStatus.fromName(dbData); + } +} diff --git a/src/main/java/solitour_backend/solitour/user_image/controller/UserImageController.java b/src/main/java/solitour_backend/solitour/user_image/controller/UserImageController.java new file mode 100644 index 0000000..bb79834 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user_image/controller/UserImageController.java @@ -0,0 +1,38 @@ +package solitour_backend.solitour.user_image.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import solitour_backend.solitour.error.Utils; +import solitour_backend.solitour.user_image.dto.UserImageRequest; +import solitour_backend.solitour.user_image.dto.UserImageResponse; +import solitour_backend.solitour.user_image.service.UserImageService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/user-image") +public class UserImageController { + + private final UserImageService userImageService; + + + @PostMapping + public ResponseEntity createUserImage(@RequestPart("request") UserImageRequest imageRequest, + @RequestPart("userImage") MultipartFile userImage, + BindingResult bindingResult) { + Utils.validationRequest(bindingResult); + UserImageResponse informationResponse = userImageService.updateUserProfile( + imageRequest.getUserId(), userImage); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(informationResponse); + } + +} diff --git a/src/main/java/solitour_backend/solitour/user_image/dto/UserImageRequest.java b/src/main/java/solitour_backend/solitour/user_image/dto/UserImageRequest.java new file mode 100644 index 0000000..58c5001 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user_image/dto/UserImageRequest.java @@ -0,0 +1,13 @@ +package solitour_backend.solitour.user_image.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class UserImageRequest { + + @NotBlank + private Long userId; +} \ No newline at end of file diff --git a/src/main/java/solitour_backend/solitour/user_image/dto/UserImageResponse.java b/src/main/java/solitour_backend/solitour/user_image/dto/UserImageResponse.java new file mode 100644 index 0000000..5cecb66 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user_image/dto/UserImageResponse.java @@ -0,0 +1,11 @@ +package solitour_backend.solitour.user_image.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class UserImageResponse { + + private String imageUrl; +} diff --git a/src/main/java/solitour_backend/solitour/user_image/entity/UserImage.java b/src/main/java/solitour_backend/solitour/user_image/entity/UserImage.java new file mode 100644 index 0000000..10418f9 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user_image/entity/UserImage.java @@ -0,0 +1,42 @@ +package solitour_backend.solitour.user_image.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDate; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "user_image") +@NoArgsConstructor +public class UserImage { + + @Id + @Column(name = "user_image_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_image_address") + private String address; + + @Column(name = "user_image_created_date") + private LocalDate createdDate; + + public UserImage(String address, LocalDate createdDate) { + this.address = address; + this.createdDate = createdDate; + } + + public void updateUserImage(String imageUrl) { + this.address = imageUrl; + } + + public void changeToDefaultProfile(String defaultImageUrl) { + this.address = defaultImageUrl; + } +} diff --git a/src/main/java/solitour_backend/solitour/user_image/entity/UserImageRepository.java b/src/main/java/solitour_backend/solitour/user_image/entity/UserImageRepository.java new file mode 100644 index 0000000..faf5685 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user_image/entity/UserImageRepository.java @@ -0,0 +1,8 @@ +package solitour_backend.solitour.user_image.entity; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserImageRepository extends JpaRepository { + + UserImage save(UserImage userImage); +} diff --git a/src/main/java/solitour_backend/solitour/user_image/service/UserImageService.java b/src/main/java/solitour_backend/solitour/user_image/service/UserImageService.java new file mode 100644 index 0000000..4881516 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/user_image/service/UserImageService.java @@ -0,0 +1,39 @@ +package solitour_backend.solitour.user_image.service; + +import java.time.LocalDate; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import solitour_backend.solitour.image.s3.S3Uploader; +import solitour_backend.solitour.user_image.dto.UserImageResponse; +import solitour_backend.solitour.user_image.entity.UserImage; +import solitour_backend.solitour.user_image.entity.UserImageRepository; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class UserImageService { + + private final UserImageRepository userImageRepository; + private final S3Uploader s3Uploader; + public static final String IMAGE_PATH = "user"; + + @Transactional + public UserImage saveUserImage(String imageUrl) { + UserImage userImage = new UserImage(imageUrl, LocalDate.now()); + + userImageRepository.save(userImage); + + return userImage; + } + + @Transactional + public UserImageResponse updateUserProfile(Long userId, MultipartFile userImage) { + + String userImageUrl = s3Uploader.upload(userImage, IMAGE_PATH, userId); + s3Uploader.markImagePermanent(userImageUrl); + + return new UserImageResponse(userImageUrl); + } +} diff --git a/src/main/java/solitour_backend/solitour/util/HmacUtils.java b/src/main/java/solitour_backend/solitour/util/HmacUtils.java new file mode 100644 index 0000000..23b8ac9 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/util/HmacUtils.java @@ -0,0 +1,41 @@ +package solitour_backend.solitour.util; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.util.Base64; + +@Component +public class HmacUtils { + private HmacUtils() { + } + + @Value("${view.count.cookie.key}") + private String viewCountKeyTemp; + + private static String viewCountKey; + + public static final String HMAC_SHA256 = "HmacSHA256"; + + @PostConstruct + public void init() { + viewCountKey = this.viewCountKeyTemp; + } + + public static String generateHmac(String data) throws Exception { + Mac mac = Mac.getInstance(HMAC_SHA256); + SecretKeySpec secretKeySpec = new SecretKeySpec(viewCountKey.getBytes(), HMAC_SHA256); + mac.init(secretKeySpec); + + byte[] hmacBytes = mac.doFinal(data.getBytes()); + return Base64.getEncoder().encodeToString(hmacBytes); // Base64로 인코딩된 HMAC 값 반환 + } + + public static boolean verifyHmac(String data, String providedHmac) throws Exception { + String generatedHmac = generateHmac(data); + return providedHmac.equals(generatedHmac); // 제공된 HMAC과 생성된 HMAC 비교 + } +} diff --git a/src/main/java/solitour_backend/solitour/util/TimeUtil.java b/src/main/java/solitour_backend/solitour/util/TimeUtil.java new file mode 100644 index 0000000..f5a41d0 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/util/TimeUtil.java @@ -0,0 +1,22 @@ +package solitour_backend.solitour.util; + +import java.time.Duration; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import org.springframework.stereotype.Component; + +@Component +public class TimeUtil { + + public long getSecondsUntilMidnightInKST() { + ZoneId koreaZoneId = ZoneId.of("Asia/Seoul"); + ZonedDateTime now = ZonedDateTime.now(koreaZoneId); + ZonedDateTime midnight = now.toLocalDate().atTime(LocalTime.MIDNIGHT) + .plusDays(1) + .atZone(koreaZoneId); + Duration duration = Duration.between(now, midnight); + return duration.getSeconds(); + } + +} diff --git a/src/main/java/solitour_backend/solitour/zone_category/controller/ZoneCategoryController.java b/src/main/java/solitour_backend/solitour/zone_category/controller/ZoneCategoryController.java new file mode 100644 index 0000000..fd0f111 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/zone_category/controller/ZoneCategoryController.java @@ -0,0 +1,79 @@ +package solitour_backend.solitour.zone_category.controller; + + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import solitour_backend.solitour.error.exception.RequestValidationFailedException; +import solitour_backend.solitour.zone_category.dto.request.ZoneCategoryModifyRequest; +import solitour_backend.solitour.zone_category.dto.request.ZoneCategoryRegisterRequest; +import solitour_backend.solitour.zone_category.dto.response.ZoneCategoryResponse; +import solitour_backend.solitour.zone_category.service.ZoneCategoryService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/zoneCategories") +public class ZoneCategoryController { + + private final ZoneCategoryService zoneCategoryService; + + @GetMapping("/{id}") + public ResponseEntity getZoneCategory(@PathVariable Long id) { + ZoneCategoryResponse zoneCategoryResponse = zoneCategoryService.getZoneCategoryById(id); + + return ResponseEntity + .status(HttpStatus.OK) + .body(zoneCategoryResponse); + } + + @PostMapping + public ResponseEntity registerZoneCategory( + @Valid @RequestBody ZoneCategoryRegisterRequest zoneCategoryRegisterRequest, + BindingResult bindingResult) { + if (bindingResult.hasErrors()) { + throw new RequestValidationFailedException(bindingResult); + } + ZoneCategoryResponse zoneCategoryResponse = zoneCategoryService.registerZoneCategory( + zoneCategoryRegisterRequest); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(zoneCategoryResponse); + } + + @PutMapping("/{id}") + public ResponseEntity modifyZoneCategory(@PathVariable Long id, + @Valid @RequestBody ZoneCategoryModifyRequest zoneCategoryModifyRequest, + BindingResult bindingResult) { + if (bindingResult.hasErrors()) { + throw new RequestValidationFailedException(bindingResult); + } + ZoneCategoryResponse zoneCategoryResponse = zoneCategoryService.modifyZoneCategory(id, + zoneCategoryModifyRequest); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(zoneCategoryResponse); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteZoneCategory(@PathVariable Long id) { + zoneCategoryService.deleteZoneCategory(id); + + return ResponseEntity + .status(HttpStatus.NO_CONTENT) + .build(); + } + + +} diff --git a/src/main/java/solitour_backend/solitour/zone_category/dto/mapper/ZoneCategoryMapper.java b/src/main/java/solitour_backend/solitour/zone_category/dto/mapper/ZoneCategoryMapper.java new file mode 100644 index 0000000..c0d1666 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/zone_category/dto/mapper/ZoneCategoryMapper.java @@ -0,0 +1,12 @@ +package solitour_backend.solitour.zone_category.dto.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import solitour_backend.solitour.zone_category.dto.response.ZoneCategoryResponse; +import solitour_backend.solitour.zone_category.entity.ZoneCategory; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface ZoneCategoryMapper { + + ZoneCategoryResponse mapToZoneCategoryResponse(ZoneCategory zoneCategory); +} diff --git a/src/main/java/solitour_backend/solitour/zone_category/dto/request/ZoneCategoryModifyRequest.java b/src/main/java/solitour_backend/solitour/zone_category/dto/request/ZoneCategoryModifyRequest.java new file mode 100644 index 0000000..ca0c98f --- /dev/null +++ b/src/main/java/solitour_backend/solitour/zone_category/dto/request/ZoneCategoryModifyRequest.java @@ -0,0 +1,21 @@ +package solitour_backend.solitour.zone_category.dto.request; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ZoneCategoryModifyRequest { + + @Nullable + @Min(1) + private Long parentId; + + @NotBlank + @Size(max = 20) + private String name; +} diff --git a/src/main/java/solitour_backend/solitour/zone_category/dto/request/ZoneCategoryRegisterRequest.java b/src/main/java/solitour_backend/solitour/zone_category/dto/request/ZoneCategoryRegisterRequest.java new file mode 100644 index 0000000..a772eda --- /dev/null +++ b/src/main/java/solitour_backend/solitour/zone_category/dto/request/ZoneCategoryRegisterRequest.java @@ -0,0 +1,21 @@ +package solitour_backend.solitour.zone_category.dto.request; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ZoneCategoryRegisterRequest { + + @Min(1) + @Nullable + private Long parentId; + + @NotBlank + @Size(max = 20) + private String name; +} diff --git a/src/main/java/solitour_backend/solitour/zone_category/dto/response/ZoneCategoryResponse.java b/src/main/java/solitour_backend/solitour/zone_category/dto/response/ZoneCategoryResponse.java new file mode 100644 index 0000000..15154c4 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/zone_category/dto/response/ZoneCategoryResponse.java @@ -0,0 +1,12 @@ +package solitour_backend.solitour.zone_category.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ZoneCategoryResponse { + + private ZoneCategoryResponse parentZoneCategory; + private String name; +} diff --git a/src/main/java/solitour_backend/solitour/zone_category/entity/ZoneCategory.java b/src/main/java/solitour_backend/solitour/zone_category/entity/ZoneCategory.java new file mode 100644 index 0000000..dad0d1f --- /dev/null +++ b/src/main/java/solitour_backend/solitour/zone_category/entity/ZoneCategory.java @@ -0,0 +1,41 @@ +package solitour_backend.solitour.zone_category.entity; + +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.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table(name = "zone_category") +@NoArgsConstructor +@AllArgsConstructor +public class ZoneCategory { + + @Id + @Column(name = "zone_category_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "parent_zone_category_id") + private ZoneCategory parentZoneCategory; + + @Column(name = "zone_category_name") + private String name; + + public ZoneCategory(ZoneCategory parentZoneCategory, String name) { + this.parentZoneCategory = parentZoneCategory; + this.name = name; + } +} diff --git a/src/main/java/solitour_backend/solitour/zone_category/exception/ZoneCategoryAlreadyExistsException.java b/src/main/java/solitour_backend/solitour/zone_category/exception/ZoneCategoryAlreadyExistsException.java new file mode 100644 index 0000000..e2ce0ae --- /dev/null +++ b/src/main/java/solitour_backend/solitour/zone_category/exception/ZoneCategoryAlreadyExistsException.java @@ -0,0 +1,8 @@ +package solitour_backend.solitour.zone_category.exception; + +public class ZoneCategoryAlreadyExistsException extends RuntimeException { + + public ZoneCategoryAlreadyExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/zone_category/exception/ZoneCategoryNotExistsException.java b/src/main/java/solitour_backend/solitour/zone_category/exception/ZoneCategoryNotExistsException.java new file mode 100644 index 0000000..8ea7032 --- /dev/null +++ b/src/main/java/solitour_backend/solitour/zone_category/exception/ZoneCategoryNotExistsException.java @@ -0,0 +1,8 @@ +package solitour_backend.solitour.zone_category.exception; + +public class ZoneCategoryNotExistsException extends RuntimeException { + + public ZoneCategoryNotExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/solitour_backend/solitour/zone_category/repository/ZoneCategoryRepository.java b/src/main/java/solitour_backend/solitour/zone_category/repository/ZoneCategoryRepository.java new file mode 100644 index 0000000..24e599c --- /dev/null +++ b/src/main/java/solitour_backend/solitour/zone_category/repository/ZoneCategoryRepository.java @@ -0,0 +1,12 @@ +package solitour_backend.solitour.zone_category.repository; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import solitour_backend.solitour.zone_category.entity.ZoneCategory; + +public interface ZoneCategoryRepository extends JpaRepository { + + Optional findByName(String name); + + Optional findByParentZoneCategoryIdAndName(Long parentZoneCategoryId, String name); +} diff --git a/src/main/java/solitour_backend/solitour/zone_category/service/ZoneCategoryService.java b/src/main/java/solitour_backend/solitour/zone_category/service/ZoneCategoryService.java new file mode 100644 index 0000000..4f808ab --- /dev/null +++ b/src/main/java/solitour_backend/solitour/zone_category/service/ZoneCategoryService.java @@ -0,0 +1,79 @@ +package solitour_backend.solitour.zone_category.service; + +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import solitour_backend.solitour.zone_category.dto.mapper.ZoneCategoryMapper; +import solitour_backend.solitour.zone_category.dto.request.ZoneCategoryModifyRequest; +import solitour_backend.solitour.zone_category.dto.request.ZoneCategoryRegisterRequest; +import solitour_backend.solitour.zone_category.dto.response.ZoneCategoryResponse; +import solitour_backend.solitour.zone_category.entity.ZoneCategory; +import solitour_backend.solitour.zone_category.exception.ZoneCategoryNotExistsException; +import solitour_backend.solitour.zone_category.repository.ZoneCategoryRepository; + +@Service +@Transactional +@RequiredArgsConstructor +public class ZoneCategoryService { + + private final ZoneCategoryRepository zoneCategoryRepository; + private final ZoneCategoryMapper zoneCategoryMapper; + + @Transactional(readOnly = true) + public ZoneCategoryResponse getZoneCategoryById(Long id) { + ZoneCategory zoneCategory = zoneCategoryRepository.findById(id) + .orElseThrow( + () -> new ZoneCategoryNotExistsException("해당 하는 id의 ZoneCategory가 존재하지 않습니다")); + return zoneCategoryMapper.mapToZoneCategoryResponse(zoneCategory); + } + + public ZoneCategoryResponse registerZoneCategory(ZoneCategoryRegisterRequest zoneCategoryRegisterRequest) { + ZoneCategory parentZoneCategory; + if (Objects.isNull(zoneCategoryRegisterRequest.getParentId())) { + parentZoneCategory = null; + } else { + parentZoneCategory = + zoneCategoryRepository.findById(zoneCategoryRegisterRequest.getParentId()) + .orElseThrow( + () -> new ZoneCategoryNotExistsException( + "해당 하는 id의 ZoneCategory가 존재하지 않습니다")); + } + + ZoneCategory zoneCategory = new ZoneCategory(parentZoneCategory, + zoneCategoryRegisterRequest.getName()); + ZoneCategory savedZoneCategory = zoneCategoryRepository.save(zoneCategory); + + return zoneCategoryMapper.mapToZoneCategoryResponse(savedZoneCategory); + } + + public ZoneCategoryResponse modifyZoneCategory(Long id, ZoneCategoryModifyRequest zoneCategoryModifyRequest) { + ZoneCategory zoneCategory = zoneCategoryRepository.findById(id) + .orElseThrow( + () -> new ZoneCategoryNotExistsException("해당 하는 id의 ZoneCategory가 존재하지 않습니다")); + + ZoneCategory parentZoneCategory; + if (Objects.isNull(zoneCategoryModifyRequest.getParentId())) { + parentZoneCategory = null; + } else { + parentZoneCategory = + zoneCategoryRepository.findById(zoneCategoryModifyRequest.getParentId()) + .orElseThrow( + () -> new ZoneCategoryNotExistsException( + "해당 하는 id의 ZoneCategory가 존재하지 않습니다")); + } + + zoneCategory.setName(zoneCategoryModifyRequest.getName()); + zoneCategory.setParentZoneCategory(parentZoneCategory); + + return zoneCategoryMapper.mapToZoneCategoryResponse(zoneCategory); + } + + public void deleteZoneCategory(Long id) { + if (zoneCategoryRepository.existsById(id)) { + zoneCategoryRepository.deleteById(id); + } else { + throw new ZoneCategoryNotExistsException("해당 하는 id의 ZoneCategory가 존재하지 않습니다"); + } + } +} diff --git a/src/main/resources/category.sql b/src/main/resources/category.sql new file mode 100644 index 0000000..24ee1e9 --- /dev/null +++ b/src/main/resources/category.sql @@ -0,0 +1,22 @@ +INSERT INTO `category` (`parent_category_id`, `category_name`) +VALUES (NULL, '맛집'), + (NULL, '숙박'), + (NULL, '액티비티'); + +INSERT INTO `category` (`parent_category_id`, `category_name`) +VALUES (1, '혼카페'), + (1, '혼밥'), + (1, '혼술'); + +INSERT INTO `category` (`parent_category_id`, `category_name`) +VALUES (2, '호텔/펜션'), + (2, '게스트하우스'), + (2, '모텔'), + (2, '홈/빌라'), + (2, '한옥'); + +INSERT INTO `category` (`parent_category_id`, `category_name`) +VALUES (3, '수상레저'), + (3, '관광지'), + (3, '전시'), + (3, '편집/소품샵'); diff --git a/src/main/resources/gathering_category.sql b/src/main/resources/gathering_category.sql new file mode 100644 index 0000000..73bb723 --- /dev/null +++ b/src/main/resources/gathering_category.sql @@ -0,0 +1,3 @@ +INSERT INTO `gathering_category` (`gathering_category_name`) +VALUES ('취미'), + ('활동'); \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..f4e92b0 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,327 @@ +DROP TABLE IF EXISTS `great_information`; +DROP TABLE IF EXISTS `great_gathering`; +DROP TABLE IF EXISTS `book_mark_information`; +DROP TABLE IF EXISTS `book_mark_gathering`; +DROP TABLE IF EXISTS `info_tag`; +DROP TABLE IF EXISTS `gathering_tag`; +DROP TABLE IF EXISTS `comment`; +DROP TABLE IF EXISTS `image`; +DROP TABLE IF EXISTS `information`; +DROP TABLE IF EXISTS `gathering_applicants`; +DROP TABLE IF EXISTS `gathering`; +DROP TABLE IF EXISTS `tag`; +DROP TABLE IF EXISTS `category`; +DROP TABLE IF EXISTS `gathering_category`; +DROP TABLE IF EXISTS `zone_category`; +DROP TABLE IF EXISTS `place`; +DROP TABLE IF EXISTS `token`; +DROP TABLE IF EXISTS `banner`; +DROP TABLE IF EXISTS `notice`; +DROP TABLE IF EXISTS `qna_message`; +DROP TABLE IF EXISTS `qna`; +DROP TABLE IF EXISTS `term`; +DROP TABLE IF EXISTS `user`; +DROP TABLE IF EXISTS `user_image`; +DROP TABLE IF EXISTS `diary_day_content`; +DROP TABLE IF EXISTS `diary`; + +CREATE TABLE `user_image` +( + `user_image_id` BIGINT NOT NULL AUTO_INCREMENT, + `user_image_address` VARCHAR(200) NOT NULL, + `user_image_created_date` DATE NOT NULL, + CONSTRAINT PK_user_image PRIMARY KEY (`user_image_id`) +); + +CREATE TABLE `user` +( + `user_id` BIGINT NOT NULL AUTO_INCREMENT, + `user_image_id` BIGINT NOT NULL, + `user_status_id` VARCHAR(20) NOT NULL, + `user_oauth_id` VARCHAR(100) NULL UNIQUE, + `provider` VARCHAR(10) NULL, + `user_nickname` VARCHAR(30) NULL, + `user_name` VARCHAR(20) NULL, + `user_age` INT NULL, + `user_sex` VARCHAR(10) NULL, + `user_email` VARCHAR(30) NULL, + `user_phone_number` VARCHAR(13) NULL, + `is_admin` BOOLEAN NOT NULL, + `user_latest_login_at` DATETIME NULL, + `user_created_at` DATETIME NOT NULL, + `user_deleted_at` DATETIME NULL, + CONSTRAINT PK_USER PRIMARY KEY (`user_id`), + CONSTRAINT FK_USER_IMAGE_TO_USER FOREIGN KEY (`user_image_id`) REFERENCES `user_image` (`user_image_id`) +); + +CREATE TABLE `token` +( + `token_id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `refresh_token` VARCHAR(250) NULL, + `oauth_token` VARCHAR(250) NULL, + CONSTRAINT PK_TOKEN PRIMARY KEY (`token_id`), + CONSTRAINT FK_USER_TO_TOKEN FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) +); + +CREATE TABLE `place` +( + `place_id` BIGINT NOT NULL AUTO_INCREMENT, + `place_search_id` VARCHAR(30) NULL, + `place_name` VARCHAR(30) NOT NULL, + `place_x_axis` DECIMAL(10, 7) NOT NULL, + `place_y_axis` DECIMAL(10, 7) NOT NULL, + `place_address` VARCHAR(50) NOT NULL, + CONSTRAINT PK_PLACE PRIMARY KEY (`place_id`) +); + +CREATE TABLE `zone_category` +( + `zone_category_id` BIGINT NOT NULL AUTO_INCREMENT, + `parent_zone_category_id` BIGINT NULL, + `zone_category_name` VARCHAR(20) NOT NULL, + CONSTRAINT PK_ZONE_CATEGORY PRIMARY KEY (`zone_category_id`), + CONSTRAINT FK_zone_category_TO_zone_category FOREIGN KEY (`parent_zone_category_id`) REFERENCES `zone_category` (`zone_category_id`) +); + +CREATE TABLE `category` +( + `category_id` BIGINT NOT NULL AUTO_INCREMENT, + `parent_category_id` BIGINT NULL, + `category_name` VARCHAR(20) NOT NULL, + CONSTRAINT PK_CATEGORY PRIMARY KEY (`category_id`), + CONSTRAINT FK_category_TO_category FOREIGN KEY (`parent_category_id`) REFERENCES `category` (`category_id`) +); + +CREATE TABLE `gathering_category` +( + `gathering_category_id` BIGINT NOT NULL AUTO_INCREMENT, + `gathering_category_name` VARCHAR(20) NOT NULL, + CONSTRAINT PK_GATHERING_CATEGORY PRIMARY KEY (`gathering_category_id`) +); + +CREATE TABLE `information` +( + `information_id` BIGINT NOT NULL AUTO_INCREMENT, + `category_id` BIGINT NOT NULL, + `zone_category_id` BIGINT NOT NULL, + `user_id` BIGINT NOT NULL, + `place_id` BIGINT NOT NULL, + `information_title` VARCHAR(50) NOT NULL, + `information_address` VARCHAR(50) NOT NULL, + `information_created_date` DATETIME NOT NULL, + `information_view_count` INT NOT NULL DEFAULT 0, + `information_content` TEXT NULL, + `information_tip` TEXT NULL, + CONSTRAINT PK_information PRIMARY KEY (`information_id`), + CONSTRAINT FK_category_TO_information FOREIGN KEY (`category_id`) REFERENCES `category` (`category_id`), + CONSTRAINT FK_zone_category_TO_information FOREIGN KEY (`zone_category_id`) REFERENCES `zone_category` (`zone_category_id`), + CONSTRAINT FK_user_TO_information FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`), + CONSTRAINT FK_place_TO_information FOREIGN KEY (`place_id`) REFERENCES `place` (`place_id`) +); + +CREATE TABLE `gathering` +( + `gathering_id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `zone_category_id` BIGINT NOT NULL, + `gathering_category_id` BIGINT NOT NULL, + `place_id` BIGINT NOT NULL, + `gathering_title` VARCHAR(50) NULL, + `gathering_content` TEXT NULL, + `gathering_person_count` INT NULL, + `gathering_view_count` INT NULL, + `gathering_created_at` DATETIME NULL, + `gathering_edited_at` DATETIME NULL, + `gathering_schedule_start_date` DATETIME NULL, + `gathering_schedule_end_date` DATETIME NULL, + `gathering_is_finish` BOOLEAN NULL, + `gathering_deadline` DATETIME NULL, + `gathering_allowed_sex` VARCHAR(30) NOT NULL, + `gathering_start_age` INT NOT NULL, + `gathering_end_age` INT NOT NULL, + `gathering_is_deleted` BOOLEAN NOT NULL DEFAULT FALSE, + `gathering_open_chatting_url` VARCHAR(255) NULL, + + CONSTRAINT PK_gathering PRIMARY KEY (`gathering_id`), + CONSTRAINT FK_user_TO_gathering FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`), + CONSTRAINT FK_gathering_category_TO_information FOREIGN KEY (`gathering_category_id`) REFERENCES `gathering_category` (`gathering_category_id`), + CONSTRAINT FK_zone_category_TO_gathering FOREIGN KEY (`zone_category_id`) REFERENCES `zone_category` (`zone_category_id`), + CONSTRAINT FK_place_TO_gathering FOREIGN KEY (`place_id`) REFERENCES `place` (`place_id`) +); + +CREATE TABLE `gathering_applicants` +( + `gathering_applicants_id` BIGINT NOT NULL AUTO_INCREMENT, + `gathering_id` BIGINT NOT NULL, + `user_id` BIGINT NOT NULL, + `gathering_applicants_state` VARCHAR(20) NOT NULL, + CONSTRAINT PK_GATHERING_APPLICANTS PRIMARY KEY (`gathering_applicants_id`), + CONSTRAINT FK_GATHERING_TO_GATHERING_APPLICANTS FOREIGN KEY (`gathering_id`) REFERENCES `gathering` (`gathering_id`), + CONSTRAINT FK_user_TO_GATHERING_APPLICANTS FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) +); + +CREATE TABLE `image` +( + `image_id` BIGINT NOT NULL AUTO_INCREMENT, + `image_status_id` VARCHAR(20) NOT NULL, + `information_id` BIGINT NOT NULL, + `image_address` VARCHAR(200) NOT NULL, + `image_created_date` DATE NOT NULL, + CONSTRAINT PK_image PRIMARY KEY (`image_id`), + CONSTRAINT FK_information_id_TO_image FOREIGN KEY (`information_id`) REFERENCES `information` (`information_id`) +); + +CREATE TABLE `great_information` +( + `great_information_id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `information_id` BIGINT NOT NULL, + CONSTRAINT PK_great_information PRIMARY KEY (`great_information_id`), + CONSTRAINT FK_great_information_TO_user FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`), + CONSTRAINT FK_great_information_TO_information FOREIGN KEY (`information_id`) REFERENCES `information` (`information_id`), + CONSTRAINT UK_great_information UNIQUE (`user_id`, `information_id`) +); + +CREATE TABLE `great_gathering` +( + `great_gathering_id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `gathering_id` BIGINT NOT NULL, + CONSTRAINT PK_great_gathering PRIMARY KEY (`great_gathering_id`), + CONSTRAINT FK_great_gathering_TO_user FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`), + CONSTRAINT FK_great_gathering_TO_gathering FOREIGN KEY (`gathering_id`) REFERENCES `gathering` (`gathering_id`), + CONSTRAINT UK_great_gathering UNIQUE (`user_id`, `gathering_id`) +); + +CREATE TABLE `book_mark_information` +( + `book_mark_information_id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `information_id` BIGINT NOT NULL, + CONSTRAINT PK_book_mark_information PRIMARY KEY (`book_mark_information_id`), + CONSTRAINT FK_book_mark_information_TO_user FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`), + CONSTRAINT FK_book_mark_information_TO_information FOREIGN KEY (`information_id`) REFERENCES `information` (`information_id`), + CONSTRAINT UK_book_mark_information UNIQUE (`user_id`, `information_id`) +); + +CREATE TABLE `book_mark_gathering` +( + `book_mark_gathering_id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `gathering_id` BIGINT NOT NULL, + CONSTRAINT PK_book_mark_gathering PRIMARY KEY (`book_mark_gathering_id`), + CONSTRAINT FK_book_mark_gathering_TO_user FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`), + CONSTRAINT FK_book_mark_gathering_TO_gathering FOREIGN KEY (`gathering_id`) REFERENCES `gathering` (`gathering_id`), + CONSTRAINT UK_book_mark_gathering UNIQUE (`user_id`, `gathering_id`) +); + +CREATE TABLE `tag` +( + `tag_id` BIGINT NOT NULL AUTO_INCREMENT, + `tag_name` VARCHAR(16) NOT NULL, + CONSTRAINT PK_tag PRIMARY KEY (`tag_id`) +); + +CREATE TABLE `info_tag` +( + `info_tag_id` BIGINT NOT NULL AUTO_INCREMENT, + `tag_id` BIGINT NOT NULL, + `information_id` BIGINT NOT NULL, + CONSTRAINT PK_info_tag PRIMARY KEY (`info_tag_id`), + CONSTRAINT FK_info_tag_TO_tag FOREIGN KEY (`tag_id`) REFERENCES `tag` (`tag_id`), + CONSTRAINT FK_info_tag_TO_information FOREIGN KEY (`information_id`) REFERENCES `information` (`information_id`), + CONSTRAINT UK_info_tag UNIQUE (`tag_id`, `information_id`) +); + +CREATE TABLE `gathering_tag` +( + `gathering_tag_id` BIGINT NOT NULL AUTO_INCREMENT, + `tag_id` BIGINT NOT NULL, + `gathering_id` BIGINT NOT NULL, + CONSTRAINT PK_gathering_tag PRIMARY KEY (`gathering_tag_id`), + CONSTRAINT FK_gathering_tag_TO_tag FOREIGN KEY (`tag_id`) REFERENCES `tag` (`tag_id`), + CONSTRAINT FK_gathering_tag_TO_gathering FOREIGN KEY (`gathering_id`) REFERENCES `gathering` (`gathering_id`), + CONSTRAINT UK_gathering_tag UNIQUE (`tag_id`, `gathering_id`) +); + +CREATE TABLE `banner` +( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL, + `url` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE `qna` +( + `qna_id` BIGINT NOT NULL AUTO_INCREMENT, + `qna_category_name` VARCHAR(255) DEFAULT NULL, + `qna_created_at` DATETIME DEFAULT NULL, + `qna_status` VARCHAR(255) DEFAULT NULL, + `qna_title` VARCHAR(255) DEFAULT NULL, + `qna_updated_at` DATETIME DEFAULT NULL, + `user_id` BIGINT DEFAULT NULL, + PRIMARY KEY (`qna_id`), + CONSTRAINT FK_qna_user FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) +); + +CREATE TABLE `qna_message` +( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `qna_message_content` TEXT DEFAULT NULL, + `qna_message_created_at` DATETIME DEFAULT NULL, + `qna_message_user_id` BIGINT DEFAULT NULL, + `qna_id` BIGINT DEFAULT NULL, + PRIMARY KEY (`id`), + CONSTRAINT FK_qna_message_qna FOREIGN KEY (`qna_id`) REFERENCES `qna` (`qna_id`), + CONSTRAINT FK_qna_message_user FOREIGN KEY (`qna_message_user_id`) REFERENCES `user` (`user_id`) +); + +CREATE TABLE `notice` +( + `notice_id` BIGINT NOT NULL AUTO_INCREMENT, + `notice_category_name` VARCHAR(255) DEFAULT NULL, + `notice_content` TEXT DEFAULT NULL, + `notice_created_at` DATETIME DEFAULT NULL, + `notice_is_deleted` BOOLEAN DEFAULT FALSE, + `notice_title` VARCHAR(255) DEFAULT NULL, + PRIMARY KEY (`notice_id`) +); + +CREATE TABLE `diary` +( + `diary_id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `diary_title` VARCHAR(50) NOT NULL, + `diary_title_image` VARCHAR(200) DEFAULT NULL, + `diary_start_date` DATETIME NOT NULL, + `diary_end_date` DATETIME NOT NULL, + `diary_created_date` DATETIME NOT NULL, + `diary_edited_date` DATETIME DEFAULT NULL, + PRIMARY KEY (`diary_id`) +); + +CREATE TABLE `diary_day_content` +( + `diary_day_content_id` BIGINT NOT NULL AUTO_INCREMENT, + `diary_id` BIGINT NOT NULL, + `diary_day_content_place` VARCHAR(50) NOT NULL, + `diary_day_content_content` TEXT NOT NULL, + `diary_day_content_feeling_status` VARCHAR(20) NOT NULL, + `diary_day_content_image` TEXT DEFAULT NULL, + PRIMARY KEY (`diary_day_content_id`), + CONSTRAINT `FK_diary_day_content_TO_diary` FOREIGN KEY (`diary_id`) REFERENCES `diary` (`diary_id`) +); + + +CREATE TABLE `term` +( + `term_id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `term_condition_agreement` BOOLEAN NOT NULL, + `term_privacy_agreement` BOOLEAN NOT NULL, + `term_created_at` DATETIME NOT NULL, + PRIMARY KEY (`term_id`), + CONSTRAINT `FK_term_TO_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) +); diff --git a/src/main/resources/schema_test.sql b/src/main/resources/schema_test.sql new file mode 100644 index 0000000..e05c3e1 --- /dev/null +++ b/src/main/resources/schema_test.sql @@ -0,0 +1,290 @@ +-- 기존 테이블 삭제 +DROP TABLE IF EXISTS "user_image"; +DROP TABLE IF EXISTS "user"; +DROP TABLE IF EXISTS "token"; +DROP TABLE IF EXISTS "place"; +DROP TABLE IF EXISTS "zone_category"; +DROP TABLE IF EXISTS "category"; +DROP TABLE IF EXISTS "gathering_category"; +DROP TABLE IF EXISTS "information"; +DROP TABLE IF EXISTS "gathering"; +DROP TABLE IF EXISTS "gathering_applicants"; +DROP TABLE IF EXISTS "image"; +DROP TABLE IF EXISTS "great_information"; +DROP TABLE IF EXISTS "great_gathering"; +DROP TABLE IF EXISTS "book_mark_information"; +DROP TABLE IF EXISTS "book_mark_gathering"; +DROP TABLE IF EXISTS "tag"; +DROP TABLE IF EXISTS "info_tag"; +DROP TABLE IF EXISTS "gathering_tag"; +DROP TABLE IF EXISTS "banner"; +DROP TABLE IF EXISTS "qna"; +DROP TABLE IF EXISTS "qna_message"; +DROP TABLE IF EXISTS "notice"; +DROP TABLE IF EXISTS "diary"; +DROP TABLE IF EXISTS "diary_day_content"; +DROP TABLE IF EXISTS `term`; + +-- 테이블 생성 +CREATE TABLE "user_image" +( + "user_image_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "user_image_address" VARCHAR(200) NOT NULL, + "user_image_created_date" DATE NOT NULL +); + +CREATE TABLE "user" +( + "user_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "user_image_id" BIGINT NOT NULL, + "user_status_id" VARCHAR(20) NOT NULL, + "user_oauth_id" VARCHAR(100), + "provider" VARCHAR(10), + "user_nickname" VARCHAR(30), + "user_name" VARCHAR(20), + "user_age" INT, + "user_sex" VARCHAR(10), + "user_email" VARCHAR(30), + "user_phone_number" VARCHAR(13), + "is_admin" BOOLEAN NOT NULL, + "user_latest_login_at" DATETIME, + "user_created_at" DATETIME NOT NULL, + "user_deleted_at" DATETIME, + FOREIGN KEY ("user_image_id") REFERENCES "user_image" ("user_image_id") +); + +CREATE TABLE "token" +( + "token_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "user_id" BIGINT NOT NULL, + "refresh_token" VARCHAR(250) NOT NULL, + FOREIGN KEY ("user_id") REFERENCES "user" ("user_id") +); + +CREATE TABLE "place" +( + "place_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "place_search_id" VARCHAR(30), + "place_name" VARCHAR(30) NOT NULL, + "place_x_axis" DECIMAL(10, 7) NOT NULL, + "place_y_axis" DECIMAL(10, 7) NOT NULL, + "place_address" VARCHAR(50) NOT NULL +); + +CREATE TABLE "zone_category" +( + "zone_category_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "parent_zone_category_id" BIGINT, + "zone_category_name" VARCHAR(20) NOT NULL, + FOREIGN KEY ("parent_zone_category_id") REFERENCES "zone_category" ("zone_category_id") +); + +CREATE TABLE "category" +( + "category_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "parent_category_id" BIGINT, + "category_name" VARCHAR(20) NOT NULL, + FOREIGN KEY ("parent_category_id") REFERENCES "category" ("category_id") +); + +CREATE TABLE "gathering_category" +( + "gathering_category_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "gathering_category_name" VARCHAR(20) NOT NULL +); + +CREATE TABLE "information" +( + "information_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "category_id" BIGINT NOT NULL, + "zone_category_id" BIGINT NOT NULL, + "user_id" BIGINT NOT NULL, + "place_id" BIGINT NOT NULL, + "information_title" VARCHAR(50) NOT NULL, + "information_address" VARCHAR(50) NOT NULL, + "information_created_date" DATETIME NOT NULL, + "information_view_count" INT NOT NULL DEFAULT 0, + "information_content" TEXT, + "information_tip" TEXT, + FOREIGN KEY ("category_id") REFERENCES "category" ("category_id"), + FOREIGN KEY ("zone_category_id") REFERENCES "zone_category" ("zone_category_id"), + FOREIGN KEY ("user_id") REFERENCES "user" ("user_id"), + FOREIGN KEY ("place_id") REFERENCES "place" ("place_id") +); + +CREATE TABLE "gathering" +( + "gathering_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "user_id" BIGINT NOT NULL, + "zone_category_id" BIGINT NOT NULL, + "gathering_category_id" BIGINT NOT NULL, + "place_id" BIGINT NOT NULL, + "gathering_title" VARCHAR(50), + "gathering_content" TEXT, + "gathering_person_count" INT, + "gathering_view_count" INT, + "gathering_created_at" DATETIME, + "gathering_edited_at" DATETIME, + "gathering_schedule_start_date" DATETIME, + "gathering_schedule_end_date" DATETIME, + "gathering_is_finish" BOOLEAN, + "gathering_deadline" DATETIME, + "gathering_allowed_sex" VARCHAR(30) NOT NULL, + "gathering_start_age" INT NOT NULL, + "gathering_end_age" INT NOT NULL, + "gathering_is_deleted" BOOLEAN NOT NULL DEFAULT FALSE, + "gathering_open_chatting_url" VARCHAR(255), + FOREIGN KEY ("user_id") REFERENCES "user" ("user_id"), + FOREIGN KEY ("gathering_category_id") REFERENCES "gathering_category" ("gathering_category_id"), + FOREIGN KEY ("zone_category_id") REFERENCES "zone_category" ("zone_category_id"), + FOREIGN KEY ("place_id") REFERENCES "place" ("place_id") +); + +CREATE TABLE "gathering_applicants" +( + "gathering_applicants_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "gathering_id" BIGINT NOT NULL, + "user_id" BIGINT NOT NULL, + "gathering_applicants_state" VARCHAR(20) NOT NULL, + FOREIGN KEY ("gathering_id") REFERENCES "gathering" ("gathering_id"), + FOREIGN KEY ("user_id") REFERENCES "user" ("user_id") +); + +CREATE TABLE "image" +( + "image_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "image_status_id" VARCHAR(20) NOT NULL, + "information_id" BIGINT NOT NULL, + "image_address" VARCHAR(200) NOT NULL, + "image_created_date" DATE NOT NULL, + FOREIGN KEY ("information_id") REFERENCES "information" ("information_id") +); + +CREATE TABLE "great_information" +( + "great_information_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "user_id" BIGINT NOT NULL, + "information_id" BIGINT NOT NULL, + FOREIGN KEY ("user_id") REFERENCES "user" ("user_id"), + FOREIGN KEY ("information_id") REFERENCES "information" ("information_id"), + UNIQUE ("user_id", "information_id") +); + +CREATE TABLE "great_gathering" +( + "great_gathering_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "user_id" BIGINT NOT NULL, + "gathering_id" BIGINT NOT NULL, + FOREIGN KEY ("user_id") REFERENCES "user" ("user_id"), + FOREIGN KEY ("gathering_id") REFERENCES "gathering" ("gathering_id"), + UNIQUE ("user_id", "gathering_id") +); + +CREATE TABLE "book_mark_information" +( + "book_mark_information_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "user_id" BIGINT NOT NULL, + "information_id" BIGINT NOT NULL, + FOREIGN KEY ("user_id") REFERENCES "user" ("user_id"), + FOREIGN KEY ("information_id") REFERENCES "information" ("information_id"), + UNIQUE ("user_id", "information_id") +); + +CREATE TABLE "book_mark_gathering" +( + "book_mark_gathering_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "user_id" BIGINT NOT NULL, + "gathering_id" BIGINT NOT NULL, + FOREIGN KEY ("user_id") REFERENCES "user" ("user_id"), + FOREIGN KEY ("gathering_id") REFERENCES "gathering" ("gathering_id"), + UNIQUE ("user_id", "gathering_id") +); + +CREATE TABLE "tag" +( + "tag_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "tag_name" VARCHAR(16) NOT NULL +); + +CREATE TABLE "info_tag" +( + "info_tag_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "tag_id" BIGINT NOT NULL, + "information_id" BIGINT NOT NULL, + FOREIGN KEY ("tag_id") REFERENCES "tag" ("tag_id"), + FOREIGN KEY ("information_id") REFERENCES "information" ("information_id"), + UNIQUE ("tag_id", "information_id") +); + +CREATE TABLE "gathering_tag" +( + "gathering_tag_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "tag_id" BIGINT NOT NULL, + "gathering_id" BIGINT NOT NULL, + FOREIGN KEY ("tag_id") REFERENCES "tag" ("tag_id"), + FOREIGN KEY ("gathering_id") REFERENCES "gathering" ("gathering_id"), + UNIQUE ("tag_id", "gathering_id") +); + +CREATE TABLE "banner" +( + "id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "name" VARCHAR(255) NOT NULL, + "url" VARCHAR(255) NOT NULL +); + +CREATE TABLE "qna" +( + "qna_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "qna_category_name" VARCHAR(255), + "qna_created_at" DATETIME, + "qna_status" VARCHAR(255), + "qna_title" VARCHAR(255), + "qna_updated_at" DATETIME, + "user_id" BIGINT, + FOREIGN KEY ("user_id") REFERENCES "user" ("user_id") +); + +CREATE TABLE "qna_message" +( + "id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "qna_message_content" TEXT, + "qna_message_created_at" DATETIME, + "qna_message_user_id" BIGINT, + "qna_id" BIGINT, + FOREIGN KEY ("qna_id") REFERENCES "qna" ("qna_id"), + FOREIGN KEY ("qna_message_user_id") REFERENCES "user" ("user_id") +); + +CREATE TABLE "notice" +( + "notice_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "notice_category_name" VARCHAR(255), + "notice_content" TEXT, + "notice_created_at" DATETIME, + "notice_is_deleted" BOOLEAN DEFAULT FALSE, + "notice_title" VARCHAR(255) +); + +CREATE TABLE "diary" +( + "diary_id" BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + "user_id" BIGINT NOT NULL, + "diary_title" VARCHAR(50) NOT NULL, + "diary_title_image" VARCHAR(200) DEFAULT NULL, + "diary_start_date" DATETIME NOT NULL, + "diary_end_date" DATETIME NOT NULL, + "diary_created_date" DATETIME NOT NULL, + "diary_edited_date" DATETIME DEFAULT NULL +); + +CREATE TABLE `term` +( + `term_id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `term_condition_agreement` BOOLEAN NOT NULL, + `term_privacy_agreement` BOOLEAN NOT NULL, + `term_created_at` DATETIME NOT NULL, + PRIMARY KEY (`term_id`), + CONSTRAINT `FK_term_TO_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) +); diff --git a/src/main/resources/zone_category.sql b/src/main/resources/zone_category.sql new file mode 100644 index 0000000..ab22ac7 --- /dev/null +++ b/src/main/resources/zone_category.sql @@ -0,0 +1,281 @@ +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (NULL, '서울'), + (NULL, '광주'), + (NULL, '인천'), + (NULL, '대전'), + (NULL, '대구'), + (NULL, '전남'), + (NULL, '경북'), + (NULL, '경남'), + (NULL, '부산'), + (NULL, '울산'), + (NULL, '제주'), + (NULL, '경기'), + (NULL, '강원'), + (NULL, '충북'), + (NULL, '충남'), + (NULL, '전북'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (1, '강남구'), + (1, '강동구'), + (1, '강북구'), + (1, '강서구'), + (1, '관악구'), + (1, '광진구'), + (1, '구로구'), + (1, '금천구'), + (1, '노원구'), + (1, '도봉구'), + (1, '동대문구'), + (1, '동작구'), + (1, '마포구'), + (1, '서대문구'), + (1, '서초구'), + (1, '성동구'), + (1, '성북구'), + (1, '송파구'), + (1, '양천구'), + (1, '영등포구'), + (1, '용산구'), + (1, '은평구'), + (1, '종로구'), + (1, '중구'), + (1, '중랑구'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (2, '동구'), + (2, '서구'), + (2, '남구'), + (2, '북구'), + (2, '광산구'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (3, '중구'), + (3, '동구'), + (3, '미추홀구'), + (3, '연수구'), + (3, '남동구'), + (3, '부평구'), + (3, '계양구'), + (3, '서구'), + (3, '강화군'), + (3, '옹진군'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (4, '동구'), + (4, '중구'), + (4, '서구'), + (4, '유성구'), + (4, '대덕구'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (5, '중구'), + (5, '동구'), + (5, '서구'), + (5, '남구'), + (5, '북구'), + (5, '수성구'), + (5, '달서구'), + (5, '달성군'), + (5, '군위군'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (6, '목포시'), + (6, '여수시'), + (6, '순천시'), + (6, '나주시'), + (6, '광양시'), + (6, '담양군'), + (6, '곡성군'), + (6, '구례군'), + (6, '고흥군'), + (6, '보성군'), + (6, '화순군'), + (6, '장흥군'), + (6, '강진군'), + (6, '해남군'), + (6, '영암군'), + (6, '무안군'), + (6, '함평군'), + (6, '영광군'), + (6, '장성군'), + (6, '완도군'), + (6, '진도군'), + (6, '신안군'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (7, '포항시'), + (7, '경주시'), + (7, '김천시'), + (7, '안동시'), + (7, '구미시'), + (7, '영주시'), + (7, '영천시'), + (7, '상주시'), + (7, '문경시'), + (7, '경산시'), + (7, '의성군'), + (7, '청송군'), + (7, '영양군'), + (7, '영덕군'), + (7, '청도군'), + (7, '고령군'), + (7, '성주군'), + (7, '칠곡군'), + (7, '예천군'), + (7, '봉화군'), + (7, '울진군'), + (7, '울릉군'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (8, '창원시'), + (8, '김해시'), + (8, '진주시'), + (8, '양산시'), + (8, '거제시'), + (8, '통영시'), + (8, '사천시'), + (8, '밀양시'), + (8, '함안군'), + (8, '거창군'), + (8, '창녕군'), + (8, '고성군'), + (8, '하동군'), + (8, '합천군'), + (8, '남해군'), + (8, '함양군'), + (8, '산청군'), + (8, '의령군'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (9, '중구'), + (9, '서구'), + (9, '동구'), + (9, '영도구'), + (9, '부산진구'), + (9, '동래구'), + (9, '남구'), + (9, '북구'), + (9, '해운대구'), + (9, '사하구'), + (9, '금정구'), + (9, '강서구'), + (9, '연제구'), + (9, '수영구'), + (9, '사상구'), + (9, '기장군'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (10, '중구'), + (10, '남구'), + (10, '동구'), + (10, '북구'), + (10, '울주군'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (11, '서귀포시'), + (11, '제주시'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (12, '수원시'), + (12, '성남시'), + (12, '용인시'), + (12, '안양시'), + (12, '안산시'), + (12, '과천시'), + (12, '광명시'), + (12, '광주시'), + (12, '군포시'), + (12, '부천시'), + (12, '시흥시'), + (12, '김포시'), + (12, '안성시'), + (12, '오산시'), + (12, '의왕시'), + (12, '이천시'), + (12, '평택시'), + (12, '하남시'), + (12, '화성시'), + (12, '여주시'), + (12, '양평군'), + (12, '고양시'), + (12, '구리시'), + (12, '남양주시'), + (12, '동두천시'), + (12, '양주시'), + (12, '의정부시'), + (12, '파주시'), + (12, '포천시'), + (12, '연천군'), + (12, '가평군'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (13, '원주시'), + (13, '춘천시'), + (13, '강릉시'), + (13, '동해시'), + (13, '속초시'), + (13, '삼척시'), + (13, '태백시'), + (13, '홍천군'), + (13, '철원군'), + (13, '횡성군'), + (13, '평창군'), + (13, '정선군'), + (13, '영월군'), + (13, '인제군'), + (13, '고성군'), + (13, '양양군'), + (13, '화천군'), + (13, '양구군'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (14, '청주시'), + (14, '충주시'), + (14, '제천시'), + (14, '보은군'), + (14, '옥천군'), + (14, '영동군'), + (14, '증평군'), + (14, '진천군'), + (14, '괴산군'), + (14, '음성군'), + (14, '단양군'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (15, '천안시'), + (15, '공주시'), + (15, '보령시'), + (15, '아산시'), + (15, '서산시'), + (15, '논산시'), + (15, '계룡시'), + (15, '당진시'), + (15, '금산군'), + (15, '부여군'), + (15, '서천군'), + (15, '청양군'), + (15, '홍성군'), + (15, '예산군'), + (15, '태안군'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) +VALUES (16, '전주시'), + (16, '익산시'), + (16, '군산시'), + (16, '정읍시'), + (16, '김제시'), + (16, '남원시'), + (16, '완주군'), + (16, '고창군'), + (16, '부안군'), + (16, '임실군'), + (16, '순창군'), + (16, '진안군'), + (16, '무주군'), + (16, '장수군'); + +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) VALUES (NULL, '세종'); +INSERT INTO `zone_category` (`parent_zone_category_id`, `zone_category_name`) VALUES (245, '세종'); + diff --git a/src/test/java/solitour_backend/solitour/SolitourApplicationTests.java b/src/test/java/solitour_backend/solitour/SolitourApplicationTests.java new file mode 100644 index 0000000..aac74b1 --- /dev/null +++ b/src/test/java/solitour_backend/solitour/SolitourApplicationTests.java @@ -0,0 +1,15 @@ +package solitour_backend.solitour; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +class SolitourApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/solitour_backend/solitour/information/controller/InformationControllerTest.java b/src/test/java/solitour_backend/solitour/information/controller/InformationControllerTest.java new file mode 100644 index 0000000..f85f4be --- /dev/null +++ b/src/test/java/solitour_backend/solitour/information/controller/InformationControllerTest.java @@ -0,0 +1,42 @@ +package solitour_backend.solitour.information.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import solitour_backend.solitour.information.service.InformationService; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; + +@WebMvcTest(InformationController.class) +@ExtendWith({MockitoExtension.class, RestDocumentationExtension.class}) +class InformationControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private InformationService informationService; + + @BeforeEach + void setUp(WebApplicationContext webApplicationContext, + RestDocumentationContextProvider restDocumentation) { + + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) + .apply(documentationConfiguration(restDocumentation) + .operationPreprocessors() + .withRequestDefaults(modifyUris(), prettyPrint()) + .withResponseDefaults(prettyPrint())) + .build(); + } +} \ No newline at end of file