diff --git a/.gitignore b/.gitignore index 485dee6..b72a702 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ .idea +api.txt +db.txt +log.txt \ No newline at end of file diff --git a/back/.gitattributes b/back/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/back/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/back/.gitignore b/back/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/back/.gitignore @@ -0,0 +1,37 @@ +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/ diff --git a/back/build.gradle.kts b/back/build.gradle.kts new file mode 100644 index 0000000..d5aa642 --- /dev/null +++ b/back/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + java + id("org.springframework.boot") version "3.5.5" + id("io.spring.dependency-management") version "1.1.7" +} + +group = "com" +version = "0.0.1-SNAPSHOT" +description = "back" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +configurations { + compileOnly { + extendsFrom(configurations.annotationProcessor.get()) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-oauth2-client") // OAuth2 Client 추가 + // Swagger + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9") + // JWT + implementation("io.jsonwebtoken:jjwt-api:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5") + compileOnly("org.projectlombok:lombok") + runtimeOnly("com.h2database:h2") + runtimeOnly("org.postgresql:postgresql") + annotationProcessor("org.projectlombok:lombok") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.security:spring-security-test") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/back/gradle/wrapper/gradle-wrapper.jar b/back/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/back/gradle/wrapper/gradle-wrapper.jar differ diff --git a/back/gradle/wrapper/gradle-wrapper.properties b/back/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d4081da --- /dev/null +++ b/back/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/back/gradlew b/back/gradlew new file mode 100644 index 0000000..23d15a9 --- /dev/null +++ b/back/gradlew @@ -0,0 +1,251 @@ +#!/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\n' "$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="\\\"\\\"" + + +# 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, 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" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# 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/back/gradlew.bat b/back/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/back/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= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +: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/back/settings.gradle.kts b/back/settings.gradle.kts new file mode 100644 index 0000000..8b4d566 --- /dev/null +++ b/back/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "back" diff --git a/back/src/main/java/com/back/BackApplication.java b/back/src/main/java/com/back/BackApplication.java new file mode 100644 index 0000000..a15f5d5 --- /dev/null +++ b/back/src/main/java/com/back/BackApplication.java @@ -0,0 +1,15 @@ +package com.back; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@EnableJpaAuditing +@SpringBootApplication +public class BackApplication { + + public static void main(String[] args) { + SpringApplication.run(BackApplication.class, args); + } + +} diff --git a/back/src/main/java/com/back/domain/comment/controller/CommentController.java b/back/src/main/java/com/back/domain/comment/controller/CommentController.java new file mode 100644 index 0000000..ce82570 --- /dev/null +++ b/back/src/main/java/com/back/domain/comment/controller/CommentController.java @@ -0,0 +1,18 @@ +package com.back.domain.comment.controller; + +import com.back.domain.comment.service.CommentService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 댓글 관련 API 요청을 처리하는 컨트롤러. + */ +@RestController +@RequestMapping("/api/v1/posts/{postId}/comments") +@RequiredArgsConstructor +public class CommentController { + + private final CommentService commentService; + +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/comment/entity/Comment.java b/back/src/main/java/com/back/domain/comment/entity/Comment.java new file mode 100644 index 0000000..83c1e03 --- /dev/null +++ b/back/src/main/java/com/back/domain/comment/entity/Comment.java @@ -0,0 +1,59 @@ +package com.back.domain.comment.entity; + +import com.back.domain.post.entity.Post; +import com.back.domain.user.entity.User; +import com.back.global.baseentity.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.ColumnDefault; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 댓글 엔티티. + * 게시글에 대한 사용자 댓글 정보를 저장합니다. + * 계층형 댓글 구조를 지원합니다. + */ +@Entity +@Table(name = "comments") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Comment extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", nullable = false) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_comment_id") + private Comment parent; + + @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true) + private List children = new ArrayList<>(); + + @Column(nullable = false, columnDefinition = "TEXT") + private String content; + + @Column(nullable = false) + @ColumnDefault("true") + private boolean hide; + + private int likeCount; + + @LastModifiedDate + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/comment/repository/CommentRepository.java b/back/src/main/java/com/back/domain/comment/repository/CommentRepository.java new file mode 100644 index 0000000..4ff32c9 --- /dev/null +++ b/back/src/main/java/com/back/domain/comment/repository/CommentRepository.java @@ -0,0 +1,12 @@ +package com.back.domain.comment.repository; + +import com.back.domain.comment.entity.Comment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * 댓글 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository. + */ +@Repository +public interface CommentRepository extends JpaRepository { +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/comment/service/CommentService.java b/back/src/main/java/com/back/domain/comment/service/CommentService.java new file mode 100644 index 0000000..d3ccb16 --- /dev/null +++ b/back/src/main/java/com/back/domain/comment/service/CommentService.java @@ -0,0 +1,16 @@ +package com.back.domain.comment.service; + +import com.back.domain.comment.repository.CommentRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 댓글 관련 비즈니스 로직을 처리하는 서비스. + */ +@Service +@RequiredArgsConstructor +public class CommentService { + + private final CommentRepository commentRepository; + +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/like/controller/LikeController.java b/back/src/main/java/com/back/domain/like/controller/LikeController.java new file mode 100644 index 0000000..55c5f73 --- /dev/null +++ b/back/src/main/java/com/back/domain/like/controller/LikeController.java @@ -0,0 +1,16 @@ +package com.back.domain.like.controller; + +import com.back.domain.like.service.LikeService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RestController; + +/** + * 좋아요 관련 API 요청을 처리하는 컨트롤러. + */ +@RestController +@RequiredArgsConstructor +public class LikeController { + + private final LikeService likeService; + +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/like/entity/CommentLike.java b/back/src/main/java/com/back/domain/like/entity/CommentLike.java new file mode 100644 index 0000000..0ec8a57 --- /dev/null +++ b/back/src/main/java/com/back/domain/like/entity/CommentLike.java @@ -0,0 +1,40 @@ +package com.back.domain.like.entity; + +import com.back.domain.comment.entity.Comment; +import com.back.domain.user.entity.User; +import com.back.global.baseentity.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * 댓글 좋아요 엔티티. + * 사용자가 특정 댓글에 좋아요를 표시한 정보를 저장합니다. + */ +@Entity +@Table(name = "comment_likes", + uniqueConstraints = { + @UniqueConstraint( + name = "comment_like_uk", + columnNames = {"comment_id", "user_id"} + ) + } +) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CommentLike extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "comment_id") + private Comment comment; +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/like/entity/PostLike.java b/back/src/main/java/com/back/domain/like/entity/PostLike.java new file mode 100644 index 0000000..fdab231 --- /dev/null +++ b/back/src/main/java/com/back/domain/like/entity/PostLike.java @@ -0,0 +1,40 @@ +package com.back.domain.like.entity; + +import com.back.domain.post.entity.Post; +import com.back.domain.user.entity.User; +import com.back.global.baseentity.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * 게시글 좋아요 엔티티. + * 사용자가 특정 게시글에 좋아요를 표시한 정보를 저장합니다. + */ +@Entity +@Table(name = "post_likes", + uniqueConstraints = { + @UniqueConstraint( + name = "post_like_uk", + columnNames = {"post_id", "user_id"} + ) + } +) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PostLike extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/like/repository/CommentLikeRepository.java b/back/src/main/java/com/back/domain/like/repository/CommentLikeRepository.java new file mode 100644 index 0000000..b5823fa --- /dev/null +++ b/back/src/main/java/com/back/domain/like/repository/CommentLikeRepository.java @@ -0,0 +1,12 @@ +package com.back.domain.like.repository; + +import com.back.domain.like.entity.CommentLike; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * 댓글 좋아요 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository. + */ +@Repository +public interface CommentLikeRepository extends JpaRepository { +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/like/repository/PostLikeRepository.java b/back/src/main/java/com/back/domain/like/repository/PostLikeRepository.java new file mode 100644 index 0000000..8a44f7e --- /dev/null +++ b/back/src/main/java/com/back/domain/like/repository/PostLikeRepository.java @@ -0,0 +1,12 @@ +package com.back.domain.like.repository; + +import com.back.domain.like.entity.PostLike; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * 게시글 좋아요 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository. + */ +@Repository +public interface PostLikeRepository extends JpaRepository { +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/like/service/LikeService.java b/back/src/main/java/com/back/domain/like/service/LikeService.java new file mode 100644 index 0000000..d1c921d --- /dev/null +++ b/back/src/main/java/com/back/domain/like/service/LikeService.java @@ -0,0 +1,18 @@ +package com.back.domain.like.service; + +import com.back.domain.like.repository.CommentLikeRepository; +import com.back.domain.like.repository.PostLikeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 좋아요 관련 비즈니스 로직을 처리하는 서비스. + */ +@Service +@RequiredArgsConstructor +public class LikeService { + + private final PostLikeRepository postLikeRepository; + private final CommentLikeRepository commentLikeRepository; + +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/node/controller/NodeController.java b/back/src/main/java/com/back/domain/node/controller/NodeController.java new file mode 100644 index 0000000..30da1e7 --- /dev/null +++ b/back/src/main/java/com/back/domain/node/controller/NodeController.java @@ -0,0 +1,19 @@ +package com.back.domain.node.controller; + +import com.back.domain.node.service.NodeService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 노드(분기) 관련 API 요청을 처리하는 컨트롤러. + * 사용자의 삶의 분기점 및 선택에 대한 정보를 관리합니다. + */ +@RestController +@RequestMapping("/api/v1") +@RequiredArgsConstructor +public class NodeController { + + private final NodeService nodeService; + +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/node/entity/BaseLine.java b/back/src/main/java/com/back/domain/node/entity/BaseLine.java new file mode 100644 index 0000000..621059e --- /dev/null +++ b/back/src/main/java/com/back/domain/node/entity/BaseLine.java @@ -0,0 +1,28 @@ +package com.back.domain.node.entity; + +import com.back.domain.user.entity.User; +import com.back.global.baseentity.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * 사용자의 현재 삶의 베이스라인을 나타내는 엔티티. + * 각 사용자는 하나의 베이스라인을 가집니다. + */ +@Entity +@Table(name = "base_lines") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BaseLine extends BaseEntity { + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/node/entity/BaseNode.java b/back/src/main/java/com/back/domain/node/entity/BaseNode.java new file mode 100644 index 0000000..a8ad343 --- /dev/null +++ b/back/src/main/java/com/back/domain/node/entity/BaseNode.java @@ -0,0 +1,57 @@ +package com.back.domain.node.entity; + +import com.back.domain.user.entity.User; +import com.back.global.baseentity.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +/** + * 사용자의 현재 삶의 분기점(노드)을 나타내는 엔티티. + * 베이스라인에 속하며, 계층 구조를 가질 수 있습니다. + */ +@Entity +@Table(name = "base_nodes") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BaseNode extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private NodeType nodeKind; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "base_line_id") + private BaseLine baseLine; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_node_id") + private BaseNode parent; + + @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true) + private List children = new ArrayList<>(); + + @Enumerated(EnumType.STRING) + private NodeCategory category; + + @Column(columnDefinition = "TEXT") + private String situation; + + @Column(columnDefinition = "TEXT") + private String decision; + + private int decisionDate; +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/node/entity/DecisionLine.java b/back/src/main/java/com/back/domain/node/entity/DecisionLine.java new file mode 100644 index 0000000..b746ac0 --- /dev/null +++ b/back/src/main/java/com/back/domain/node/entity/DecisionLine.java @@ -0,0 +1,34 @@ +package com.back.domain.node.entity; + +import com.back.domain.user.entity.User; +import com.back.global.baseentity.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +/** + * 사용자의 선택 분기(Decision Node)들의 연결을 나타내는 엔티티. + * 각 사용자는 여러 개의 DecisionLine을 가질 수 있습니다. + */ +@Entity +@Table(name = "decision_lines") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DecisionLine extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @OneToMany(mappedBy = "decisionLine", cascade = CascadeType.ALL, orphanRemoval = true) + private List decisionNodes = new ArrayList<>(); +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/node/entity/DecisionNode.java b/back/src/main/java/com/back/domain/node/entity/DecisionNode.java new file mode 100644 index 0000000..ddc705f --- /dev/null +++ b/back/src/main/java/com/back/domain/node/entity/DecisionNode.java @@ -0,0 +1,61 @@ +package com.back.domain.node.entity; + +import com.back.domain.user.entity.User; +import com.back.global.baseentity.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +/** + * 사용자의 선택 분기(노드)를 나타내는 엔티티. + * DecisionLine에 속하며, 계층 구조를 가질 수 있습니다. + */ +@Entity +@Table(name = "decision_nodes") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DecisionNode extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private NodeType nodeKind; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "dec_line_id") + private DecisionLine decisionLine; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "base_node_id") + private BaseNode baseNode; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_node_id") + private DecisionNode parent; + + @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true) + private List children = new ArrayList<>(); + + @Enumerated(EnumType.STRING) + private NodeCategory category; + + @Column(columnDefinition = "TEXT") + private String situation; + + @Column(columnDefinition = "TEXT") + private String decision; + + private int decisionDate; +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/node/entity/NodeCategory.java b/back/src/main/java/com/back/domain/node/entity/NodeCategory.java new file mode 100644 index 0000000..15abfe3 --- /dev/null +++ b/back/src/main/java/com/back/domain/node/entity/NodeCategory.java @@ -0,0 +1,14 @@ +package com.back.domain.node.entity; + +/** + * 노드(분기)의 카테고리를 정의하는 Enum. + */ +public enum NodeCategory { + EDUCATION, + CAREER, + RELATIONSHIP, + FINANCE, + HEALTH, + LOCATION, + ETC +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/node/entity/NodeType.java b/back/src/main/java/com/back/domain/node/entity/NodeType.java new file mode 100644 index 0000000..c6cb76f --- /dev/null +++ b/back/src/main/java/com/back/domain/node/entity/NodeType.java @@ -0,0 +1,9 @@ +package com.back.domain.node.entity; + +/** + * 노드(분기)의 타입을 정의하는 Enum. + * BASE는 루트 분기를, DECISION은 사용자의 선택 분기를 나타냅니다. + */ +public enum NodeType { + BASE, DECISION +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/node/repository/BaseLineRepository.java b/back/src/main/java/com/back/domain/node/repository/BaseLineRepository.java new file mode 100644 index 0000000..672c1e3 --- /dev/null +++ b/back/src/main/java/com/back/domain/node/repository/BaseLineRepository.java @@ -0,0 +1,12 @@ +package com.back.domain.node.repository; + +import com.back.domain.node.entity.BaseLine; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * 베이스라인 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository. + */ +@Repository +public interface BaseLineRepository extends JpaRepository { +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/node/repository/BaseNodeRepository.java b/back/src/main/java/com/back/domain/node/repository/BaseNodeRepository.java new file mode 100644 index 0000000..6cc21d1 --- /dev/null +++ b/back/src/main/java/com/back/domain/node/repository/BaseNodeRepository.java @@ -0,0 +1,12 @@ +package com.back.domain.node.repository; + +import com.back.domain.node.entity.BaseNode; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * 베이스 노드 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository. + */ +@Repository +public interface BaseNodeRepository extends JpaRepository { +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/node/repository/DecisionLineRepository.java b/back/src/main/java/com/back/domain/node/repository/DecisionLineRepository.java new file mode 100644 index 0000000..282f68d --- /dev/null +++ b/back/src/main/java/com/back/domain/node/repository/DecisionLineRepository.java @@ -0,0 +1,12 @@ +package com.back.domain.node.repository; + +import com.back.domain.node.entity.DecisionLine; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * 결정 라인 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository. + */ +@Repository +public interface DecisionLineRepository extends JpaRepository { +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/node/repository/DecisionNodeRepository.java b/back/src/main/java/com/back/domain/node/repository/DecisionNodeRepository.java new file mode 100644 index 0000000..27eedc3 --- /dev/null +++ b/back/src/main/java/com/back/domain/node/repository/DecisionNodeRepository.java @@ -0,0 +1,12 @@ +package com.back.domain.node.repository; + +import com.back.domain.node.entity.DecisionNode; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * 결정 노드 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository. + */ +@Repository +public interface DecisionNodeRepository extends JpaRepository { +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/node/service/NodeService.java b/back/src/main/java/com/back/domain/node/service/NodeService.java new file mode 100644 index 0000000..af75c22 --- /dev/null +++ b/back/src/main/java/com/back/domain/node/service/NodeService.java @@ -0,0 +1,23 @@ +package com.back.domain.node.service; + +import com.back.domain.node.repository.BaseLineRepository; +import com.back.domain.node.repository.BaseNodeRepository; +import com.back.domain.node.repository.DecisionLineRepository; +import com.back.domain.node.repository.DecisionNodeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 노드(분기) 관련 비즈니스 로직을 처리하는 서비스. + * 사용자의 삶의 분기점 및 선택에 대한 정보를 관리합니다. + */ +@Service +@RequiredArgsConstructor +public class NodeService { + + private final BaseLineRepository baseLineRepository; + private final BaseNodeRepository baseNodeRepository; + private final DecisionLineRepository decisionLineRepository; + private final DecisionNodeRepository decisionNodeRepository; + +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/poll/controller/PollVoteController.java b/back/src/main/java/com/back/domain/poll/controller/PollVoteController.java new file mode 100644 index 0000000..773f091 --- /dev/null +++ b/back/src/main/java/com/back/domain/poll/controller/PollVoteController.java @@ -0,0 +1,18 @@ +package com.back.domain.poll.controller; + +import com.back.domain.poll.service.PollVoteService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 투표 관련 API 요청을 처리하는 컨트롤러. + */ +@RestController +@RequestMapping("/api/v1/polls") +@RequiredArgsConstructor +public class PollVoteController { + + private final PollVoteService pollVoteService; + +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/poll/entity/PollVote.java b/back/src/main/java/com/back/domain/poll/entity/PollVote.java new file mode 100644 index 0000000..f6c1e88 --- /dev/null +++ b/back/src/main/java/com/back/domain/poll/entity/PollVote.java @@ -0,0 +1,49 @@ +package com.back.domain.poll.entity; + +import com.back.domain.post.entity.Post; +import com.back.domain.user.entity.User; +import com.back.global.baseentity.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.UUID; + +/** + * 투표 참여 정보를 저장하는 엔티티. + * 사용자가 특정 게시글의 투표에 참여한 내역을 기록합니다. + */ +@Entity +@Table(name = "poll_votes", + uniqueConstraints = { + @UniqueConstraint(name = "uq_logged_in_once", columnNames = {"post_id", "pollUid", "user_id"}), + @UniqueConstraint(name = "uq_anonymous_once", columnNames = {"post_id", "pollUid", "userHash"}) + } +) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PollVote extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", nullable = false) + private Post post; + + @Column(nullable = false) + private UUID pollUid; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column(length = 128) + private String userHash; + + @Column(nullable = false, columnDefinition = "jsonb") + private String choiceJson; +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/poll/repository/PollVoteRepository.java b/back/src/main/java/com/back/domain/poll/repository/PollVoteRepository.java new file mode 100644 index 0000000..9b06c92 --- /dev/null +++ b/back/src/main/java/com/back/domain/poll/repository/PollVoteRepository.java @@ -0,0 +1,12 @@ +package com.back.domain.poll.repository; + +import com.back.domain.poll.entity.PollVote; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * 투표 참여 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository. + */ +@Repository +public interface PollVoteRepository extends JpaRepository { +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/poll/service/PollVoteService.java b/back/src/main/java/com/back/domain/poll/service/PollVoteService.java new file mode 100644 index 0000000..df47e4f --- /dev/null +++ b/back/src/main/java/com/back/domain/poll/service/PollVoteService.java @@ -0,0 +1,16 @@ +package com.back.domain.poll.service; + +import com.back.domain.poll.repository.PollVoteRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 투표 관련 비즈니스 로직을 처리하는 서비스. + */ +@Service +@RequiredArgsConstructor +public class PollVoteService { + + private final PollVoteRepository pollVoteRepository; + +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/post/controller/PostController.java b/back/src/main/java/com/back/domain/post/controller/PostController.java new file mode 100644 index 0000000..2e50ef5 --- /dev/null +++ b/back/src/main/java/com/back/domain/post/controller/PostController.java @@ -0,0 +1,18 @@ +package com.back.domain.post.controller; + +import com.back.domain.post.service.PostService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 게시글 관련 API 요청을 처리하는 컨트롤러. + */ +@RestController +@RequestMapping("/api/v1/posts") +@RequiredArgsConstructor +public class PostController { + + private final PostService postService; + +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/post/entity/Post.java b/back/src/main/java/com/back/domain/post/entity/Post.java new file mode 100644 index 0000000..4de5159 --- /dev/null +++ b/back/src/main/java/com/back/domain/post/entity/Post.java @@ -0,0 +1,52 @@ +package com.back.domain.post.entity; + +import com.back.domain.user.entity.User; +import com.back.global.baseentity.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.ColumnDefault; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.LocalDateTime; + +/** + * 게시글 엔티티. + * 사용자가 작성한 게시글의 정보를 저장합니다. + */ +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Post extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column(length = 200) + private String title; + + @Column(length = 200) + private String category; + + @Column(columnDefinition = "TEXT") + private String content; + + @Column(columnDefinition = "jsonb") + private String voteContent; + + @Column(nullable = false) + @ColumnDefault("true") + private boolean hide; + + private int likeCount; + + @LastModifiedDate + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/post/repository/PostRepository.java b/back/src/main/java/com/back/domain/post/repository/PostRepository.java new file mode 100644 index 0000000..0fbf138 --- /dev/null +++ b/back/src/main/java/com/back/domain/post/repository/PostRepository.java @@ -0,0 +1,12 @@ +package com.back.domain.post.repository; + +import com.back.domain.post.entity.Post; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * 게시글 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository. + */ +@Repository +public interface PostRepository extends JpaRepository { +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/post/service/PostService.java b/back/src/main/java/com/back/domain/post/service/PostService.java new file mode 100644 index 0000000..63f4233 --- /dev/null +++ b/back/src/main/java/com/back/domain/post/service/PostService.java @@ -0,0 +1,16 @@ +package com.back.domain.post.service; + +import com.back.domain.post.repository.PostRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 게시글 관련 비즈니스 로직을 처리하는 서비스. + */ +@Service +@RequiredArgsConstructor +public class PostService { + + private final PostRepository postRepository; + +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/scenario/controller/ScenarioController.java b/back/src/main/java/com/back/domain/scenario/controller/ScenarioController.java new file mode 100644 index 0000000..a0ab3b6 --- /dev/null +++ b/back/src/main/java/com/back/domain/scenario/controller/ScenarioController.java @@ -0,0 +1,19 @@ +package com.back.domain.scenario.controller; + +import com.back.domain.scenario.service.ScenarioService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 시나리오 관련 API 요청을 처리하는 컨트롤러. + * 시나리오 추출, 상세 조회, 비교 등의 기능을 제공합니다. + */ +@RestController +@RequestMapping("/api/v1/scenarios") +@RequiredArgsConstructor +public class ScenarioController { + + private final ScenarioService scenarioService; + +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/scenario/entity/Scenario.java b/back/src/main/java/com/back/domain/scenario/entity/Scenario.java new file mode 100644 index 0000000..d74abcc --- /dev/null +++ b/back/src/main/java/com/back/domain/scenario/entity/Scenario.java @@ -0,0 +1,56 @@ +package com.back.domain.scenario.entity; + +import com.back.domain.post.entity.Post; +import com.back.domain.user.entity.User; +import com.back.global.baseentity.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; + +/** + * 시나리오 엔티티. + * AI를 통해 추출된 시나리오의 상세 정보를 저장합니다. + */ +@Entity +@Table(name = "scenarios") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Scenario extends BaseEntity { + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "scenario_request_id", nullable = false, unique = true) + private ScenarioRequest scenarioRequest; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @Column(length = 200) + private String job; + + private BigDecimal total; + + @Column(columnDefinition = "TEXT") + private String summary; + + @Column(columnDefinition = "TEXT") + private String description; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "scene_compare_id") + private SceneCompare sceneCompare; + + private String img; +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/scenario/entity/ScenarioRequest.java b/back/src/main/java/com/back/domain/scenario/entity/ScenarioRequest.java new file mode 100644 index 0000000..ccfb1af --- /dev/null +++ b/back/src/main/java/com/back/domain/scenario/entity/ScenarioRequest.java @@ -0,0 +1,47 @@ +package com.back.domain.scenario.entity; + +import com.back.domain.user.entity.User; +import com.back.global.baseentity.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.LocalDateTime; + +/** + * 시나리오 추출 요청 정보를 저장하는 엔티티. + * AI를 통해 시나리오를 생성하기 위한 요청의 상세 정보와 상태를 관리합니다. + */ +@Entity +@Table(name = "scenario_requests") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ScenarioRequest extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(nullable = false) + private Long optionId; + + @Column(columnDefinition = "jsonb") + private String constraintsJson; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ScenarioStatus status; + + @Column(columnDefinition = "TEXT") + private String errorMessage; + + @LastModifiedDate + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/scenario/entity/ScenarioStatus.java b/back/src/main/java/com/back/domain/scenario/entity/ScenarioStatus.java new file mode 100644 index 0000000..fc9ba84 --- /dev/null +++ b/back/src/main/java/com/back/domain/scenario/entity/ScenarioStatus.java @@ -0,0 +1,8 @@ +package com.back.domain.scenario.entity; + +/** + * 시나리오 요청의 처리 상태를 정의하는 Enum. + */ +public enum ScenarioStatus { + QUEUED, RUNNING, READY, FAILED +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/scenario/entity/SceneCompare.java b/back/src/main/java/com/back/domain/scenario/entity/SceneCompare.java new file mode 100644 index 0000000..d4c45b0 --- /dev/null +++ b/back/src/main/java/com/back/domain/scenario/entity/SceneCompare.java @@ -0,0 +1,30 @@ +package com.back.domain.scenario.entity; + +import com.back.global.baseentity.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * 시나리오 비교 결과를 저장하는 엔티티. + * 두 시나리오 간의 비교 분석 정보를 포함합니다. + */ +@Entity +@Table(name = "scene_compare") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SceneCompare extends BaseEntity { + + @Column(columnDefinition = "TEXT") + private String compareResult; + + @Enumerated(EnumType.STRING) + private SceneCompareResultType resultType; + +} diff --git a/back/src/main/java/com/back/domain/scenario/entity/SceneCompareResultType.java b/back/src/main/java/com/back/domain/scenario/entity/SceneCompareResultType.java new file mode 100644 index 0000000..bc4d7f3 --- /dev/null +++ b/back/src/main/java/com/back/domain/scenario/entity/SceneCompareResultType.java @@ -0,0 +1,8 @@ +package com.back.domain.scenario.entity; + +/** + * 시나리오 비교 결과의 타입을 정의하는 Enum. + */ +public enum SceneCompareResultType { + TOTAL, 경제, 행복, 관계, 건강, 직업 +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/scenario/entity/SceneType.java b/back/src/main/java/com/back/domain/scenario/entity/SceneType.java new file mode 100644 index 0000000..77e2f9f --- /dev/null +++ b/back/src/main/java/com/back/domain/scenario/entity/SceneType.java @@ -0,0 +1,34 @@ +package com.back.domain.scenario.entity; + +import com.back.global.baseentity.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * 시나리오의 특정 유형(경제, 행복 등)에 대한 상세 분석 정보를 저장하는 엔티티. + */ +@Entity +@Table(name = "scene_type") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SceneType extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "scenarios_id") + private Scenario scenario; + + @Enumerated(EnumType.STRING) + private Type type; + + private int point; + + @Column(columnDefinition = "TEXT") + private String analysis; +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/scenario/entity/Type.java b/back/src/main/java/com/back/domain/scenario/entity/Type.java new file mode 100644 index 0000000..1fadcbe --- /dev/null +++ b/back/src/main/java/com/back/domain/scenario/entity/Type.java @@ -0,0 +1,8 @@ +package com.back.domain.scenario.entity; + +/** + * 시나리오 분석의 특정 유형을 정의하는 Enum. + */ +public enum Type { + 경제, 행복, 관계, 직업, 건강 +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/scenario/repository/ScenarioRepository.java b/back/src/main/java/com/back/domain/scenario/repository/ScenarioRepository.java new file mode 100644 index 0000000..6af8dc3 --- /dev/null +++ b/back/src/main/java/com/back/domain/scenario/repository/ScenarioRepository.java @@ -0,0 +1,12 @@ +package com.back.domain.scenario.repository; + +import com.back.domain.scenario.entity.Scenario; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * 시나리오 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository. + */ +@Repository +public interface ScenarioRepository extends JpaRepository { +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/scenario/repository/ScenarioRequestRepository.java b/back/src/main/java/com/back/domain/scenario/repository/ScenarioRequestRepository.java new file mode 100644 index 0000000..4b2962c --- /dev/null +++ b/back/src/main/java/com/back/domain/scenario/repository/ScenarioRequestRepository.java @@ -0,0 +1,12 @@ +package com.back.domain.scenario.repository; + +import com.back.domain.scenario.entity.ScenarioRequest; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * 시나리오 요청 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository. + */ +@Repository +public interface ScenarioRequestRepository extends JpaRepository { +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/scenario/repository/SceneTypeRepository.java b/back/src/main/java/com/back/domain/scenario/repository/SceneTypeRepository.java new file mode 100644 index 0000000..9464487 --- /dev/null +++ b/back/src/main/java/com/back/domain/scenario/repository/SceneTypeRepository.java @@ -0,0 +1,12 @@ +package com.back.domain.scenario.repository; + +import com.back.domain.scenario.entity.SceneType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * 시나리오 유형 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository. + */ +@Repository +public interface SceneTypeRepository extends JpaRepository { +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/scenario/service/ScenarioService.java b/back/src/main/java/com/back/domain/scenario/service/ScenarioService.java new file mode 100644 index 0000000..ca0074f --- /dev/null +++ b/back/src/main/java/com/back/domain/scenario/service/ScenarioService.java @@ -0,0 +1,21 @@ +package com.back.domain.scenario.service; + +import com.back.domain.scenario.repository.ScenarioRepository; +import com.back.domain.scenario.repository.ScenarioRequestRepository; +import com.back.domain.scenario.repository.SceneTypeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 시나리오 관련 비즈니스 로직을 처리하는 서비스. + * 시나리오 추출, 상세 조회, 비교 등의 기능을 제공합니다. + */ +@Service +@RequiredArgsConstructor +public class ScenarioService { + + private final ScenarioRequestRepository scenarioRequestRepository; + private final ScenarioRepository scenarioRepository; + private final SceneTypeRepository sceneTypeRepository; + +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/session/controller/SessionController.java b/back/src/main/java/com/back/domain/session/controller/SessionController.java new file mode 100644 index 0000000..bdb5243 --- /dev/null +++ b/back/src/main/java/com/back/domain/session/controller/SessionController.java @@ -0,0 +1,69 @@ +package com.back.domain.session.controller; + +import com.back.domain.session.service.SessionService; +import com.back.domain.user.service.UserService; +import com.back.global.config.JwtTokenProvider; +import com.back.global.config.TokenInfo; +import com.back.global.dto.LoginRequest; +import com.back.global.dto.SignupRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +/** + * 사용자 인증 및 세션 관련 API 요청을 처리하는 컨트롤러. + * 회원가입, 로그인, 게스트 로그인, 로그아웃 기능을 제공합니다. + */ +@RestController +@RequestMapping("/users-auth") +@RequiredArgsConstructor +public class SessionController { + + private final SessionService sessionService; + private final UserService userService; + private final JwtTokenProvider jwtTokenProvider; + private final AuthenticationManager authenticationManager; + + @PostMapping("/signup") + public ResponseEntity signup(@Valid @RequestBody SignupRequest signupRequest) { + // 사용자 회원가입 처리 + userService.signup(signupRequest); + return ResponseEntity.ok("회원가입 성공"); + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest loginRequest, HttpServletRequest request, HttpServletResponse response) { + // 세션 기반 로그인 후 JWT 토큰 발급 + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(loginRequest.getLoginId(), loginRequest.getPassword()) + ); + SecurityContextHolder.getContext().setAuthentication(authentication); + + TokenInfo tokenInfo = jwtTokenProvider.generateToken(authentication); + + return ResponseEntity.ok(tokenInfo); + } + + @PostMapping("/guest") + public ResponseEntity guestLogin() { + // 게스트 토큰 발급 + Authentication guestAuthentication = sessionService.authenticateGuest(); + SecurityContextHolder.getContext().setAuthentication(guestAuthentication); + + TokenInfo tokenInfo = jwtTokenProvider.generateToken(guestAuthentication); + return ResponseEntity.ok(tokenInfo); + } + + @PostMapping("/logout") + public ResponseEntity logout(HttpServletRequest request, HttpServletResponse response) { + // 로그아웃 처리 + return ResponseEntity.ok("로그아웃 성공"); + } +} diff --git a/back/src/main/java/com/back/domain/session/entity/Session.java b/back/src/main/java/com/back/domain/session/entity/Session.java new file mode 100644 index 0000000..36e5ab0 --- /dev/null +++ b/back/src/main/java/com/back/domain/session/entity/Session.java @@ -0,0 +1,46 @@ +package com.back.domain.session.entity; + +import com.back.domain.user.entity.User; +import com.back.global.baseentity.BaseEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.ColumnDefault; + +import java.time.LocalDateTime; + +/** + * 사용자 세션 정보를 저장하는 엔티티. + * 로그인된 사용자 또는 게스트의 세션 상태를 관리합니다. + */ +@Entity +@Table(name = "sessions") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Session extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + @ColumnDefault("'GUEST'") + private SessionType sessionKind; + + @Column(unique = true, nullable = false, length = 191) + private String jwtId; + + @Column(nullable = false) + private LocalDateTime expiresAt; + + @Column(nullable = false) + @ColumnDefault("false") + private boolean revoked; +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/session/entity/SessionType.java b/back/src/main/java/com/back/domain/session/entity/SessionType.java new file mode 100644 index 0000000..3034fc0 --- /dev/null +++ b/back/src/main/java/com/back/domain/session/entity/SessionType.java @@ -0,0 +1,9 @@ +package com.back.domain.session.entity; + +/** + * 사용자 세션의 타입을 정의하는 Enum. + * USER, GUEST, ADMIN 세 가지 유형이 있습니다. + */ +public enum SessionType { + USER, GUEST, ADMIN +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/session/repository/SessionRepository.java b/back/src/main/java/com/back/domain/session/repository/SessionRepository.java new file mode 100644 index 0000000..6007651 --- /dev/null +++ b/back/src/main/java/com/back/domain/session/repository/SessionRepository.java @@ -0,0 +1,12 @@ +package com.back.domain.session.repository; + +import com.back.domain.session.entity.Session; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * 세션 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository. + */ +@Repository +public interface SessionRepository extends JpaRepository { +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/session/service/SessionService.java b/back/src/main/java/com/back/domain/session/service/SessionService.java new file mode 100644 index 0000000..27787b9 --- /dev/null +++ b/back/src/main/java/com/back/domain/session/service/SessionService.java @@ -0,0 +1,60 @@ +package com.back.domain.session.service; + +import com.back.domain.session.repository.SessionRepository; +import com.back.domain.user.entity.Gender; +import com.back.domain.user.entity.Mbti; +import com.back.domain.user.entity.Role; +import com.back.domain.user.entity.User; +import com.back.domain.user.repository.UserRepository; +import com.back.global.config.CustomUserDetails; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.UUID; + +/** + * 세션 관련 비즈니스 로직을 처리하는 서비스. + * 게스트 사용자 인증 및 세션 관리를 담당합니다. + */ +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class SessionService { + + private final SessionRepository sessionRepository; + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + public Authentication authenticateGuest() { + // 게스트 유저를 생성하고 Authentication 객체를 반환 + String guestLoginId = "guest_" + UUID.randomUUID().toString().substring(0, 8); + String guestEmail = guestLoginId + "@example.com"; + String guestPassword = UUID.randomUUID().toString(); + + User guestUser = User.builder() + .loginId(guestLoginId) + .email(guestEmail) + .password(guestPassword) + .nickname("게스트_" + UUID.randomUUID().toString().substring(0, 4)) + .birthdayAt(LocalDateTime.now()) + .gender(Gender.N) + .mbti(Mbti.INFP) + .beliefs("자유") + .role(Role.GUEST) + .build(); + + CustomUserDetails guestUserDetails = new CustomUserDetails(guestUser); + return new UsernamePasswordAuthenticationToken( + guestUserDetails, + guestUser.getPassword(), + Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + guestUser.getRole().name())) + ); + } +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/user/controller/UserController.java b/back/src/main/java/com/back/domain/user/controller/UserController.java new file mode 100644 index 0000000..ef4b7c9 --- /dev/null +++ b/back/src/main/java/com/back/domain/user/controller/UserController.java @@ -0,0 +1,19 @@ +package com.back.domain.user.controller; + +import com.back.domain.user.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 사용자 관련 API 요청을 처리하는 컨트롤러. + * 사용자 정보 조회, 수정, 통계, 목록 등의 기능을 제공합니다. + */ +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/user/entity/AuthProvider.java b/back/src/main/java/com/back/domain/user/entity/AuthProvider.java new file mode 100644 index 0000000..164e562 --- /dev/null +++ b/back/src/main/java/com/back/domain/user/entity/AuthProvider.java @@ -0,0 +1,9 @@ +package com.back.domain.user.entity; + +/** + * 사용자 인증 제공자(Provider)를 정의하는 Enum. + * 일반 로그인, 소셜 로그인(Google, Kakao, Naver, GitHub, Apple), 게스트 로그인 등을 포함합니다. + */ +public enum AuthProvider { + LOCAL, GOOGLE, KAKAO, NAVER, GITHUB, APPLE, GUEST +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/user/entity/Gender.java b/back/src/main/java/com/back/domain/user/entity/Gender.java new file mode 100644 index 0000000..79d668a --- /dev/null +++ b/back/src/main/java/com/back/domain/user/entity/Gender.java @@ -0,0 +1,9 @@ +package com.back.domain.user.entity; + +/** + * 사용자의 성별을 정의하는 Enum. + * F(여성), M(남성), O(기타), N(선택 안 함) 네 가지 유형이 있습니다. + */ +public enum Gender { + F, M, O, N +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/user/entity/Mbti.java b/back/src/main/java/com/back/domain/user/entity/Mbti.java new file mode 100644 index 0000000..57d796b --- /dev/null +++ b/back/src/main/java/com/back/domain/user/entity/Mbti.java @@ -0,0 +1,8 @@ +package com.back.domain.user.entity; + +/** + * 사용자의 MBTI 유형을 정의하는 Enum. + */ +public enum Mbti { + ISTJ, ISFJ, INFJ, INTJ, ISTP, ISFP, INFP, INTP, ESTP, ESFP, ENFP, ENTP, ESTJ, ESFJ, ENFJ, ENTJ +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/user/entity/Role.java b/back/src/main/java/com/back/domain/user/entity/Role.java new file mode 100644 index 0000000..d35de73 --- /dev/null +++ b/back/src/main/java/com/back/domain/user/entity/Role.java @@ -0,0 +1,9 @@ +package com.back.domain.user.entity; + +/** + * 사용자 역할을 정의하는 Enum. + * USER, GUEST, ADMIN 세 가지 유형이 있습니다. + */ +public enum Role { + USER, GUEST, ADMIN +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/user/entity/User.java b/back/src/main/java/com/back/domain/user/entity/User.java new file mode 100644 index 0000000..5e679bc --- /dev/null +++ b/back/src/main/java/com/back/domain/user/entity/User.java @@ -0,0 +1,68 @@ +package com.back.domain.user.entity; + +import com.back.global.baseentity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.ColumnDefault; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.LocalDateTime; + +/** + * 사용자 정보를 저장하는 엔티티. + * 일반 로그인, OAuth2 로그인, 게스트 로그인 사용자를 포함합니다. + */ +@Entity +@Table(name = "users") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class User extends BaseEntity { + + @Column(unique = true) + private String loginId; + + @Column(unique = true) + private String email; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + @ColumnDefault("'GUEST'") + private Role role; + + @Column(nullable = true) + private String password; + + @Column(length = 80) + private String nickname; + + @Column(nullable = false) + private LocalDateTime birthdayAt; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Gender gender; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Mbti mbti; + + @Column(nullable = false) + private String beliefs; + + private String lifeSatis; + + private String relationship; + + private String workLifeBal; + + private String riskAvoid; + + @Enumerated(EnumType.STRING) + private AuthProvider authProvider; + + @LastModifiedDate + private LocalDateTime updatedAt; +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/user/repository/UserRepository.java b/back/src/main/java/com/back/domain/user/repository/UserRepository.java new file mode 100644 index 0000000..617d6d1 --- /dev/null +++ b/back/src/main/java/com/back/domain/user/repository/UserRepository.java @@ -0,0 +1,16 @@ +package com.back.domain.user.repository; + +import com.back.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * 사용자 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository. + */ +@Repository +public interface UserRepository extends JpaRepository { + Optional findByLoginId(String loginId); + Optional findByEmail(String email); +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/user/service/UserService.java b/back/src/main/java/com/back/domain/user/service/UserService.java new file mode 100644 index 0000000..9891fea --- /dev/null +++ b/back/src/main/java/com/back/domain/user/service/UserService.java @@ -0,0 +1,56 @@ +package com.back.domain.user.service; + +import com.back.domain.user.entity.Role; +import com.back.domain.user.entity.User; +import com.back.domain.user.repository.UserRepository; +import com.back.global.dto.SignupRequest; +import com.back.global.exception.ApiException; +import com.back.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 사용자 관련 비즈니스 로직을 처리하는 서비스. + * 사용자 회원가입, 정보 조회 등의 기능을 제공합니다. + */ +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + @Transactional + public void signup(SignupRequest signupRequest) { + // 사용자 회원가입 처리 + if (userRepository.findByLoginId(signupRequest.getLoginId()).isPresent()) { + throw new ApiException(ErrorCode.LOGIN_ID_DUPLICATION); + } + if (userRepository.findByEmail(signupRequest.getEmail()).isPresent()) { + throw new ApiException(ErrorCode.EMAIL_DUPLICATION); + } + + User user = User.builder() + .loginId(signupRequest.getLoginId()) + .email(signupRequest.getEmail()) + .password(passwordEncoder.encode(signupRequest.getPassword())) + .nickname(signupRequest.getNickname()) + .birthdayAt(signupRequest.getBirthdayAt()) + .gender(signupRequest.getGender()) + .mbti(signupRequest.getMbti()) + .beliefs(signupRequest.getBeliefs()) + .role(Role.USER) + .build(); + + userRepository.save(user); + } + + public User findByLoginId(String loginId) { + // 로그인 ID로 사용자 정보 조회 + return userRepository.findByLoginId(loginId) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + } +} diff --git a/back/src/main/java/com/back/global/baseentity/BaseEntity.java b/back/src/main/java/com/back/global/baseentity/BaseEntity.java new file mode 100644 index 0000000..c524c5a --- /dev/null +++ b/back/src/main/java/com/back/global/baseentity/BaseEntity.java @@ -0,0 +1,25 @@ +package com.back.global.baseentity; + +import jakarta.persistence.*; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +/** + * 모든 엔티티의 공통 필드(ID, 생성일)를 정의하는 추상 기본 엔티티. + */ +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdDate; +} diff --git a/back/src/main/java/com/back/global/config/CustomOAuth2UserService.java b/back/src/main/java/com/back/global/config/CustomOAuth2UserService.java new file mode 100644 index 0000000..01271a2 --- /dev/null +++ b/back/src/main/java/com/back/global/config/CustomOAuth2UserService.java @@ -0,0 +1,66 @@ +package com.back.global.config; + +import com.back.domain.user.entity.AuthProvider; +import com.back.domain.user.entity.Role; +import com.back.domain.user.entity.User; +import com.back.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService implements OAuth2UserService { + + private final UserRepository userRepository; + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2UserService delegate = new DefaultOAuth2UserService(); + OAuth2User oAuth2User = delegate.loadUser(userRequest); + + String registrationId = userRequest.getClientRegistration().getRegistrationId(); + String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); + + OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes()); + + User user = saveOrUpdate(attributes, registrationId); + + return new CustomUserDetails(user, oAuth2User.getAttributes()); + } + + private User saveOrUpdate(OAuthAttributes attributes, String registrationId) { + Optional userOptional = userRepository.findByEmail(attributes.getEmail()); + User user; + + if (userOptional.isPresent()) { + user = userOptional.get(); + // 이미 존재하는 유저라면 정보 업데이트 (예: 닉네임, 프로필 이미지 등) + // user.update(attributes.getName(), attributes.getPicture()); + } else { + // 새로운 유저라면 회원가입 처리 + AuthProvider authProvider = AuthProvider.valueOf(registrationId.toUpperCase()); + user = User.builder() + .loginId(attributes.getEmail()) // OAuth2에서는 이메일을 loginId로 사용 + .email(attributes.getEmail()) + .nickname(attributes.getName()) + .role(Role.USER) + .authProvider(authProvider) // AuthProvider 추가 + .password("oauth2_user") // OAuth2 유저는 비밀번호가 필요 없음 (임시 값) + .birthdayAt(LocalDateTime.now()) // 임시 값 + .gender(null) // 임시 값 + .mbti(null) // 임시 값 + .beliefs("OAuth2 User") // 임시 값 + .build(); + } + return userRepository.save(user); + } +} diff --git a/back/src/main/java/com/back/global/config/CustomUserDetails.java b/back/src/main/java/com/back/global/config/CustomUserDetails.java new file mode 100644 index 0000000..e91775d --- /dev/null +++ b/back/src/main/java/com/back/global/config/CustomUserDetails.java @@ -0,0 +1,77 @@ +package com.back.global.config; + +import com.back.domain.user.entity.User; +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +/** + * Spring Security의 UserDetails 및 OAuth2User 인터페이스를 구현하여 + * 애플리케이션의 사용자 정보와 OAuth2 인증 정보를 통합 관리합니다. + */ +@Getter +public class CustomUserDetails implements UserDetails, OAuth2User { + + private final User user; + private Map attributes; + + public CustomUserDetails(User user) { + this.user = user; + } + + public CustomUserDetails(User user, Map attributes) { + this.user = user; + this.attributes = attributes; + } + + @Override + public Collection getAuthorities() { + return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole().name())); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getLoginId(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public String getName() { + return user.getLoginId(); + } +} \ No newline at end of file diff --git a/back/src/main/java/com/back/global/config/CustomUserDetailsService.java b/back/src/main/java/com/back/global/config/CustomUserDetailsService.java new file mode 100644 index 0000000..8837ff6 --- /dev/null +++ b/back/src/main/java/com/back/global/config/CustomUserDetailsService.java @@ -0,0 +1,29 @@ +package com.back.global.config; + +import com.back.domain.user.entity.User; +import com.back.domain.user.repository.UserRepository; +import com.back.global.exception.ApiException; +import com.back.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +/** + * Spring Security에서 사용자 인증을 위해 사용자 정보를 로드하는 서비스. + */ +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String loginId) throws UsernameNotFoundException { + // 사용자 정보를 데이터베이스에서 조회하여 UserDetails 객체로 반환 + User user = userRepository.findByLoginId(loginId) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + return new CustomUserDetails(user); + } +} diff --git a/back/src/main/java/com/back/global/config/JwtAuthenticationFilter.java b/back/src/main/java/com/back/global/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..b71d81c --- /dev/null +++ b/back/src/main/java/com/back/global/config/JwtAuthenticationFilter.java @@ -0,0 +1,45 @@ +package com.back.global.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; + +/** + * JWT 토큰을 사용하여 인증을 처리하는 필터. + * 요청 헤더에서 JWT 토큰을 추출하고 유효성을 검사하여 Spring Security 컨텍스트에 인증 정보를 설정합니다. + */ +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends GenericFilterBean { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + // 요청을 필터링하여 JWT 토큰을 검사하고 인증 정보를 설정 + String token = resolveToken((HttpServletRequest) request); + + if (token != null && jwtTokenProvider.validateToken(token)) { + Authentication authentication = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + chain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + // HttpServletRequest의 Authorization 헤더에서 JWT 토큰을 추출 + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/back/src/main/java/com/back/global/config/JwtTokenProvider.java b/back/src/main/java/com/back/global/config/JwtTokenProvider.java new file mode 100644 index 0000000..d30ed00 --- /dev/null +++ b/back/src/main/java/com/back/global/config/JwtTokenProvider.java @@ -0,0 +1,108 @@ +package com.back.global.config; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.stream.Collectors; + +/** + * JWT(JSON Web Token)를 생성하고 검증하는 유틸리티 클래스. + * 사용자 인증 정보를 기반으로 Access Token과 Refresh Token을 발급하며, + * 토큰의 유효성을 검사하고 토큰에서 인증 정보를 추출하는 기능을 제공합니다. + */ +@Slf4j +@Component +public class JwtTokenProvider { + + private final Key key; + private final CustomUserDetailsService customUserDetailsService; + + public JwtTokenProvider(@Value("${jwt.secret}") String secretKey, CustomUserDetailsService customUserDetailsService) { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + this.customUserDetailsService = customUserDetailsService; + } + + public TokenInfo generateToken(Authentication authentication) { + // 유저 정보를 가지고 AccessToken과 RefreshToken을 생성 + String authorities = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + long now = (new Date()).getTime(); + Date accessTokenExpiresIn = new Date(now + 86400000); // 24시간 + String accessToken = Jwts.builder() + .setSubject(authentication.getName()) + .claim("auth", authorities) + .setExpiration(accessTokenExpiresIn) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + String refreshToken = Jwts.builder() + .setExpiration(new Date(now + 86400000 * 7)) // 7일 + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + return TokenInfo.builder() + .grantType("Bearer") + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + public Authentication getAuthentication(String accessToken) { + // JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내 인증 객체를 생성 + Claims claims = parseClaims(accessToken); + + if (claims.get("auth") == null) { + throw new RuntimeException("권한 정보가 없는 토큰입니다."); + } + + Collection authorities = + Arrays.stream(claims.get("auth").toString().split(",")) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + UserDetails principal = customUserDetailsService.loadUserByUsername(claims.getSubject()); + return new UsernamePasswordAuthenticationToken(principal, "", authorities); + } + + public boolean validateToken(String token) { + // 토큰 정보를 검증 + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return true; + } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { + log.info("Invalid JWT Token", e); + } catch (ExpiredJwtException e) { + log.info("Expired JWT Token", e); + } catch (UnsupportedJwtException e) { + log.info("Unsupported JWT Token", e); + } catch (IllegalArgumentException e) { + log.info("JWT claims string is empty.", e); + } + return false; + } + + private Claims parseClaims(String accessToken) { + // Access Token에서 Claims를 파싱 + try { + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } +} diff --git a/back/src/main/java/com/back/global/config/OAuth2FailureHandler.java b/back/src/main/java/com/back/global/config/OAuth2FailureHandler.java new file mode 100644 index 0000000..d60501d --- /dev/null +++ b/back/src/main/java/com/back/global/config/OAuth2FailureHandler.java @@ -0,0 +1,33 @@ +package com.back.global.config; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; + +/** + * OAuth2 로그인 실패 시 호출되는 핸들러. + * 로그인 실패 정보를 클라이언트로 리다이렉트하여 전달합니다. + */ +@Slf4j +@Component +public class OAuth2FailureHandler extends SimpleUrlAuthenticationFailureHandler { + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { + // OAuth2 로그인 실패 시 에러 메시지를 포함하여 클라이언트로 리다이렉트 + log.error("OAuth2 Login Failure: {}", exception.getMessage()); + + String targetUrl = UriComponentsBuilder.fromUriString("http://localhost:3000/oauth2/redirect") // 클라이언트 리다이렉트 URL + .queryParam("error", exception.getMessage()) + .build().toUriString(); + + getRedirectStrategy().sendRedirect(request, response, targetUrl); + } +} diff --git a/back/src/main/java/com/back/global/config/OAuth2SuccessHandler.java b/back/src/main/java/com/back/global/config/OAuth2SuccessHandler.java new file mode 100644 index 0000000..97fa174 --- /dev/null +++ b/back/src/main/java/com/back/global/config/OAuth2SuccessHandler.java @@ -0,0 +1,41 @@ +package com.back.global.config; + +import com.back.global.config.JwtTokenProvider; +import com.back.global.config.TokenInfo; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.IOException; + +/** + * OAuth2 로그인 성공 시 호출되는 핸들러. + * 로그인 성공 후 JWT 토큰을 생성하고 클라이언트로 리다이렉트하여 전달합니다. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + // OAuth2 로그인 성공 시 JWT 토큰을 생성하고 클라이언트로 리다이렉트 + log.info("OAuth2 Login Success!"); + + TokenInfo tokenInfo = jwtTokenProvider.generateToken(authentication); + + String targetUrl = UriComponentsBuilder.fromUriString("http://localhost:3000/oauth2/redirect") // 클라이언트 리다이렉트 URL + .queryParam("token", tokenInfo.getAccessToken()) + .build().toUriString(); + + getRedirectStrategy().sendRedirect(request, response, targetUrl); + } +} diff --git a/back/src/main/java/com/back/global/config/OAuthAttributes.java b/back/src/main/java/com/back/global/config/OAuthAttributes.java new file mode 100644 index 0000000..b50cb21 --- /dev/null +++ b/back/src/main/java/com/back/global/config/OAuthAttributes.java @@ -0,0 +1,52 @@ +package com.back.global.config; + +import lombok.Builder; +import lombok.Getter; + +import java.util.Map; + +/** + * OAuth2 공급자로부터 받은 사용자 정보를 담는 DTO 클래스. + * 각 공급자별로 다른 속성 이름을 통일하여 처리할 수 있도록 돕습니다. + */ +@Getter +@Builder +public class OAuthAttributes { + private Map attributes; + private String nameAttributeKey; + private String name; + private String email; + private String picture; + + public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map attributes) { + // OAuth2 공급자(registrationId)에 따라 적절한 OAuthAttributes 객체를 생성 + if ("github".equals(registrationId)) { + return ofGithub(userNameAttributeName, attributes); + } else if ("google".equals(registrationId)) { + return ofGoogle(userNameAttributeName, attributes); + } + return null; // 또는 예외 처리 + } + + private static OAuthAttributes ofGoogle(String userNameAttributeName, Map attributes) { + // Google OAuth2 사용자 속성을 OAuthAttributes 객체로 변환 + return OAuthAttributes.builder() + .name((String) attributes.get("name")) + .email((String) attributes.get("email")) + .picture((String) attributes.get("picture")) + .attributes(attributes) + .nameAttributeKey(userNameAttributeName) + .build(); + } + + private static OAuthAttributes ofGithub(String userNameAttributeName, Map attributes) { + // GitHub OAuth2 사용자 속성을 OAuthAttributes 객체로 변환 + return OAuthAttributes.builder() + .name((String) attributes.get("login")) // GitHub는 'login' 필드를 이름으로 사용 + .email((String) attributes.get("email")) // GitHub는 이메일이 없을 수 있음 + .picture((String) attributes.get("avatar_url")) + .attributes(attributes) + .nameAttributeKey(userNameAttributeName) + .build(); + } +} diff --git a/back/src/main/java/com/back/global/config/SecurityConfig.java b/back/src/main/java/com/back/global/config/SecurityConfig.java new file mode 100644 index 0000000..c062634 --- /dev/null +++ b/back/src/main/java/com/back/global/config/SecurityConfig.java @@ -0,0 +1,101 @@ +package com.back.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; // 추가 +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +/** + * Spring Security 설정을 정의하는 구성 클래스. + * 웹 보안, 세션 관리, 인증/인가 규칙, OAuth2 로그인 및 JWT 필터 등을 설정합니다. + */ +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtTokenProvider jwtTokenProvider; + private final CustomUserDetailsService customUserDetailsService; + private final CustomOAuth2UserService customOAuth2UserService; + private final OAuth2SuccessHandler oAuth2SuccessHandler; + private final OAuth2FailureHandler oAuth2FailureHandler; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // HTTP 보안 필터 체인을 구성 + http + .httpBasic().disable() + .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) + .and() + .formLogin() + .loginPage("/users-auth/login") + .loginProcessingUrl("/users-auth/login") + .defaultSuccessUrl("/") + .failureUrl("/users-auth/login?error=true") + .permitAll() + .and() + .logout() + .logoutUrl("/users-auth/logout") + .logoutSuccessUrl("/users-auth/login?logout=true") + .invalidateHttpSession(true) + .deleteCookies("JSESSIONID") + .permitAll() + .and() + .oauth2Login() + .userInfoEndpoint() + .userService(customOAuth2UserService) + .and() + .successHandler(oAuth2SuccessHandler) + .failureHandler(oAuth2FailureHandler) + .and() + .authorizeHttpRequests() + .requestMatchers("/users-auth/**").permitAll() // Swagger 경로를 WebSecurityCustomizer로 제외했으므로 여기서 제거 + .requestMatchers("/admin/**").hasRole("ADMIN") + .anyRequest().authenticated() + .and() + .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + // 비밀번호 인코더를 제공하는 Bean을 등록 + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + // AuthenticationManager를 Bean으로 등록 + return authenticationConfiguration.getAuthenticationManager(); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + // DaoAuthenticationProvider를 Bean으로 등록 + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(customUserDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean // WebSecurityCustomizer Bean 추가 + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().requestMatchers( + "/swagger-ui/**", + "/v3/api-docs/**", + "/swagger-resources/**", + "/webjars/**" + ); + } +} \ No newline at end of file diff --git a/back/src/main/java/com/back/global/config/TokenInfo.java b/back/src/main/java/com/back/global/config/TokenInfo.java new file mode 100644 index 0000000..43233a7 --- /dev/null +++ b/back/src/main/java/com/back/global/config/TokenInfo.java @@ -0,0 +1,18 @@ +package com.back.global.config; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +/** + * JWT 토큰 정보를 담는 DTO(Data Transfer Object) 클래스. + * Access Token과 Refresh Token, 그리고 토큰의 타입을 포함합니다. + */ +@Data +@Builder +@AllArgsConstructor +public class TokenInfo { + private String grantType; + private String accessToken; + private String refreshToken; +} diff --git a/back/src/main/java/com/back/global/dto/LoginRequest.java b/back/src/main/java/com/back/global/dto/LoginRequest.java new file mode 100644 index 0000000..207de2b --- /dev/null +++ b/back/src/main/java/com/back/global/dto/LoginRequest.java @@ -0,0 +1,15 @@ +package com.back.global.dto; + +import lombok.Getter; +import lombok.Setter; + +/** + * 사용자 로그인 요청 시 필요한 정보를 담는 DTO 클래스. + * 로그인 ID와 비밀번호를 포함합니다. + */ +@Getter +@Setter +public class LoginRequest { + private String loginId; + private String password; +} diff --git a/back/src/main/java/com/back/global/dto/SignupRequest.java b/back/src/main/java/com/back/global/dto/SignupRequest.java new file mode 100644 index 0000000..87044e8 --- /dev/null +++ b/back/src/main/java/com/back/global/dto/SignupRequest.java @@ -0,0 +1,52 @@ +package com.back.global.dto; + +import com.back.domain.user.entity.Gender; +import com.back.domain.user.entity.Mbti; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Past; +import jakarta.validation.constraints.Pattern; +import lombok.Getter; +import lombok.Setter; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +/** + * 사용자 회원가입 요청 시 필요한 정보를 담는 DTO 클래스. + * 로그인 ID, 이메일, 비밀번호, 닉네임, 생년월일, 성별, MBTI, 가치관 등을 포함합니다. + */ +@Getter +@Setter +public class SignupRequest { + + @NotBlank(message = "로그인 아이디는 필수 입력 값입니다.") + private String loginId; + + @NotBlank(message = "이메일은 필수 입력 값입니다.") + @Email(message = "이메일 형식에 맞지 않습니다.") + private String email; + + @NotBlank(message = "비밀번호는 필수 입력 값입니다.") + @Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}", + message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 8자 ~ 20자의 비밀번호여야 합니다.") + private String password; + + @NotBlank(message = "닉네임은 필수 입력 값입니다.") + private String nickname; + + @NotNull(message = "생년월일은 필수 입력 값입니다.") + @Past(message = "생년월일은 과거 날짜여야 합니다.") + @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime birthdayAt; + + @NotNull(message = "성별은 필수 입력 값입니다.") + private Gender gender; + + @NotNull(message = "MBTI는 필수 입력 값입니다.") + private Mbti mbti; + + @NotBlank(message = "가치관은 필수 입력 값입니다.") + private String beliefs; +} diff --git a/back/src/main/java/com/back/global/exception/ApiException.java b/back/src/main/java/com/back/global/exception/ApiException.java new file mode 100644 index 0000000..8f51bc3 --- /dev/null +++ b/back/src/main/java/com/back/global/exception/ApiException.java @@ -0,0 +1,22 @@ +package com.back.global.exception; + +import lombok.Getter; + +/** + * 애플리케이션에서 발생하는 비즈니스 로직 관련 예외를 정의하는 커스텀 예외 클래스. + * ErrorCode를 포함하여 구체적인 에러 정보와 HTTP 상태 코드를 제공합니다. + */ +@Getter +public class ApiException extends RuntimeException { + + private final ErrorCode errorCode; + + public ApiException(ErrorCode errorCode) { + this(errorCode, errorCode.getMessage()); + } + + public ApiException(ErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; + } +} diff --git a/back/src/main/java/com/back/global/exception/ErrorCode.java b/back/src/main/java/com/back/global/exception/ErrorCode.java new file mode 100644 index 0000000..974eeb3 --- /dev/null +++ b/back/src/main/java/com/back/global/exception/ErrorCode.java @@ -0,0 +1,62 @@ +package com.back.global.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +/** + * 애플리케이션에서 발생하는 다양한 에러 상황을 정의하는 열거형 클래스. + * 각 에러는 HTTP 상태 코드, 고유한 에러 코드, 그리고 사용자에게 표시될 메시지를 가집니다. + */ +@Getter +@RequiredArgsConstructor +public enum ErrorCode { + + // Common Errors + INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "C001", "Invalid Input Value"), + METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "C002", "Method Not Allowed"), + HANDLE_ACCESS_DENIED(HttpStatus.FORBIDDEN, "C003", "Access is Denied"), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "C004", "Server Error"), + INVALID_TYPE_VALUE(HttpStatus.BAD_REQUEST, "C005", "Invalid Type Value"), + ENTITY_NOT_FOUND(HttpStatus.BAD_REQUEST, "C006", "Entity Not Found"), + + // User Errors + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "U001", "User Not Found"), + EMAIL_DUPLICATION(HttpStatus.BAD_REQUEST, "U002", "Email Duplication"), + LOGIN_ID_DUPLICATION(HttpStatus.BAD_REQUEST, "U003", "Login ID Duplication"), + INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "U004", "Invalid Password"), + UNAUTHORIZED_USER(HttpStatus.UNAUTHORIZED, "U005", "Unauthorized User"), + + // Post Errors + POST_NOT_FOUND(HttpStatus.NOT_FOUND, "P001", "Post Not Found"), + + // Comment Errors + COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "CM001", "Comment Not Found"), + + // Session Errors + SESSION_NOT_FOUND(HttpStatus.NOT_FOUND, "S001", "Session Not Found"), + SESSION_EXPIRED(HttpStatus.UNAUTHORIZED, "S002", "Session Expired"), + SESSION_REVOKED(HttpStatus.UNAUTHORIZED, "S003", "Session Revoked"), + + // Node Errors + NODE_NOT_FOUND(HttpStatus.NOT_FOUND, "N001", "Node Not Found"), + BASE_LINE_NOT_FOUND(HttpStatus.NOT_FOUND, "N002", "BaseLine Not Found"), + DECISION_LINE_NOT_FOUND(HttpStatus.NOT_FOUND, "N003", "DecisionLine Not Found"), + + // Scenario Errors + SCENARIO_NOT_FOUND(HttpStatus.NOT_FOUND, "SC001", "Scenario Not Found"), + SCENARIO_REQUEST_NOT_FOUND(HttpStatus.NOT_FOUND, "SC002", "Scenario Request Not Found"), + SCENE_COMPARE_NOT_FOUND(HttpStatus.NOT_FOUND, "SC003", "Scene Compare Not Found"), + SCENE_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "SC004", "Scene Type Not Found"), + + // Like Errors + LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "L001", "Like Not Found"), + + // Poll Errors + POLL_VOTE_NOT_FOUND(HttpStatus.NOT_FOUND, "PV001", "Poll Vote Not Found"); + + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/back/src/main/java/com/back/global/exception/ErrorResponse.java b/back/src/main/java/com/back/global/exception/ErrorResponse.java new file mode 100644 index 0000000..c2ca2e4 --- /dev/null +++ b/back/src/main/java/com/back/global/exception/ErrorResponse.java @@ -0,0 +1,46 @@ +package com.back.global.exception; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +import java.time.LocalDateTime; + +/** + * API 에러 응답을 위한 DTO 클래스. + * RFC 7807 Problem Details for HTTP APIs 표준을 따르며, + * 에러 발생 시 클라이언트에게 상세한 에러 정보를 제공합니다. + */ +@Getter +@Builder +public class ErrorResponse { + + private final LocalDateTime timestamp = LocalDateTime.now(); + private final int status; + private final String error; + private final String code; + private final String message; + private final String path; + + public static ErrorResponse of(HttpStatus status, ErrorCode errorCode, String path) { + // ErrorCode와 경로를 사용하여 ErrorResponse 객체를 생성 + return ErrorResponse.builder() + .status(status.value()) + .error(status.getReasonPhrase()) + .code(errorCode.getCode()) + .message(errorCode.getMessage()) + .path(path) + .build(); + } + + public static ErrorResponse of(HttpStatus status, ErrorCode errorCode, String message, String path) { + // ErrorCode, 메시지, 경로를 사용하여 ErrorResponse 객체를 생성 + return ErrorResponse.builder() + .status(status.value()) + .error(status.getReasonPhrase()) + .code(errorCode.getCode()) + .message(message) + .path(path) + .build(); + } +} \ No newline at end of file diff --git a/back/src/main/java/com/back/global/exception/GlobalExceptionHandler.java b/back/src/main/java/com/back/global/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..9b88a33 --- /dev/null +++ b/back/src/main/java/com/back/global/exception/GlobalExceptionHandler.java @@ -0,0 +1,46 @@ +package com.back.global.exception; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 전역 예외 처리 핸들러. + * 애플리케이션 전반에서 발생하는 예외를 중앙에서 처리하여 + * 일관된 에러 응답을 제공합니다. + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(ApiException.class) + protected ResponseEntity handleApiException(ApiException e, HttpServletRequest request) { + // 커스텀 비즈니스 예외 처리 + log.error("ApiException: {}", e.getMessage()); + return ResponseEntity + .status(e.getErrorCode().getStatus()) + .body(ErrorResponse.of(e.getErrorCode().getStatus(), e.getErrorCode(), e.getMessage(), request.getRequestURI())); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) { + // @Valid 어노테이션을 사용한 유효성 검사 실패 예외 처리 + log.error("MethodArgumentNotValidException: {}", e.getMessage()); + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(ErrorResponse.of(HttpStatus.BAD_REQUEST, ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult().getAllErrors().get(0).getDefaultMessage(), request.getRequestURI())); + } + + @ExceptionHandler(Exception.class) + protected ResponseEntity handleException(Exception e, HttpServletRequest request) { + // 모든 예상치 못한 예외 처리 + log.error("Exception: {}", e.getMessage()); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR, ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage(), request.getRequestURI())); + } +} \ No newline at end of file diff --git a/back/src/main/java/com/back/global/initdata/InitData.java b/back/src/main/java/com/back/global/initdata/InitData.java new file mode 100644 index 0000000..cd5522c --- /dev/null +++ b/back/src/main/java/com/back/global/initdata/InitData.java @@ -0,0 +1,59 @@ +package com.back.global.initdata; + +import com.back.domain.user.entity.Gender; +import com.back.domain.user.entity.Mbti; +import com.back.domain.user.entity.Role; +import com.back.domain.user.entity.User; +import com.back.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * 애플리케이션 시작 시 초기 데이터를 생성하는 컴포넌트. + * 개발 환경에서 필요한 기본 사용자(관리자, 일반 사용자)를 데이터베이스에 저장합니다. + */ +@Component +@RequiredArgsConstructor +public class InitData implements CommandLineRunner { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + @Override + public void run(String... args) throws Exception { + // 애플리케이션 시작 시 초기 사용자 데이터 생성 + if (userRepository.findByLoginId("admin").isEmpty()) { + User admin = User.builder() + .loginId("admin") + .email("admin@example.com") + .password(passwordEncoder.encode("admin1234!")) + .role(Role.ADMIN) + .nickname("관리자") + .birthdayAt(LocalDateTime.of(1990, 1, 1, 0, 0)) + .gender(Gender.M) + .mbti(Mbti.INTJ) + .beliefs("합리주의") + .build(); + userRepository.save(admin); + } + + if (userRepository.findByLoginId("user1").isEmpty()) { + User user1 = User.builder() + .loginId("user1") + .email("user1@example.com") + .password(passwordEncoder.encode("user1234!")) + .role(Role.USER) + .nickname("사용자1") + .birthdayAt(LocalDateTime.of(1995, 5, 10, 0, 0)) + .gender(Gender.F) + .mbti(Mbti.ENFP) + .beliefs("개인주의") + .build(); + userRepository.save(user1); + } + } +} \ No newline at end of file diff --git a/back/src/main/resources/application-dev.yml b/back/src/main/resources/application-dev.yml new file mode 100644 index 0000000..31dea49 --- /dev/null +++ b/back/src/main/resources/application-dev.yml @@ -0,0 +1,6 @@ +spring: + datasource: + url: jdbc:h2:./db_dev;MODE=MySQL + username: sa + password: + driver-class-name: org.h2.Driver \ No newline at end of file diff --git a/back/src/main/resources/application-test.yml b/back/src/main/resources/application-test.yml new file mode 100644 index 0000000..4603eaf --- /dev/null +++ b/back/src/main/resources/application-test.yml @@ -0,0 +1,6 @@ +spring: + datasource: + url: jdbc:h2:mem:db_test;MODE=MySQL + username: sa + password: + driver-class-name: org.h2.Driver \ No newline at end of file diff --git a/back/src/main/resources/application.yml b/back/src/main/resources/application.yml new file mode 100644 index 0000000..49c8258 --- /dev/null +++ b/back/src/main/resources/application.yml @@ -0,0 +1,65 @@ +spring: + application: + name: back + profiles: + active: test + output: + ansi: + enabled: always + datasource: + hikari: + auto-commit: false + jpa: + show-sql: true + hibernate: + ddl-auto: update + properties: + hibernate: + use_sql_comments: true + format_sql: true + highlight_sql: true + security: # 여기에 추가 + oauth2: + client: + registration: + google: + client-id: your-google-client-id + client-secret: your-google-client-secret + scope: + - email + - profile + github: + client-id: your-github-client-id + client-secret: your-github-client-secret + scope: + - user:email + - read:user + provider: + google: + authorization-uri: https://accounts.google.com/o/oauth2/v2/auth + token-uri: https://www.googleapis.com/oauth2/v4/token + user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo + user-name-attribute: sub + github: + authorization-uri: https://github.com/login/oauth/authorize + token-uri: https://github.com/login/oauth/access_token + user-info-uri: https://api.github.com/user + user-name-attribute: id +logging: + level: + org.hibernate.orm.jdbc.bind: TRACE + org.hibernate.orm.jdbc.extract: TRACE + org.springframework.transaction.interceptor: TRACE + +jwt: + secret: c3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LXR1dG9yaWFsLWJhY2stZW5kLXNlY3JldC1rZXkK # Base64 encoded 256-bit key + +springdoc: + swagger-ui: + path: /swagger-ui.html + groups: + enabled: true + api-docs: + path: /v3/api-docs + + diff --git a/back/src/test/java/com/back/BackApplicationTests.java b/back/src/test/java/com/back/BackApplicationTests.java new file mode 100644 index 0000000..172642d --- /dev/null +++ b/back/src/test/java/com/back/BackApplicationTests.java @@ -0,0 +1,13 @@ +package com.back; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BackApplicationTests { + + @Test + void contextLoads() { + } + +}