From 1b9a103e5d4b2755e8b17f6df3cd838737da0dd7 Mon Sep 17 00:00:00 2001 From: Hong Jun Date: Wed, 14 Jun 2023 20:26:27 +0900 Subject: [PATCH 1/3] Week#6 --- build.gradle | 48 ++++ gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 240 ++++++++++++++++++ gradlew.bat | 91 +++++++ settings.gradle | 1 + .../FourthSeminarApplication.java | 13 + .../advice/ControllerExceptionAdvice.java | 69 +++++ .../fourthSeminar/common/dto/ApiResponse.java | 34 +++ .../org/fourthSeminar/config/WebConfig.java | 21 ++ .../fourthSeminar/config/jwt/JwtService.java | 79 ++++++ .../fourthSeminar/config/resolver/UserId.java | 11 + .../config/resolver/UserIdResolver.java | 45 ++++ .../config/swagger/SwaggerConfig.java | 31 +++ .../controller/BoardController.java | 36 +++ .../controller/EmotionController.java | 43 ++++ .../controller/PostController.java | 31 +++ .../controller/ServerProfileController.java | 22 ++ .../controller/UserController.java | 43 ++++ .../controller/dto/BoardRequestDto.java | 27 ++ .../dto/request/BoardImageListRequestDto.java | 23 ++ .../dto/request/BoardRequestDto.java | 27 ++ .../dto/request/EmotionRequestDto.java | 40 +++ .../dto/request/UserRequestDto.java | 35 +++ .../response/EmotionCalendarResponseDto.java | 24 ++ .../dto/response/EmotionResponseDto.java | 32 +++ .../dto/response/PostResponseDto.java | 22 ++ .../dto/response/UserResponseDto.java | 19 ++ .../request/dto/UserLoginRequestDto.java | 26 ++ .../request/dto/UserLoginResponseDto.java | 18 ++ .../domain/AuditingTimeEntity.java | 22 ++ .../sopt/org/fourthSeminar/domain/Board.java | 44 ++++ .../org/fourthSeminar/domain/Emotion.java | 53 ++++ .../sopt/org/fourthSeminar/domain/Image.java | 33 +++ .../sopt/org/fourthSeminar/domain/Post.java | 40 +++ .../sopt/org/fourthSeminar/domain/User.java | 36 +++ .../org/fourthSeminar/exception/Error.java | 54 ++++ .../org/fourthSeminar/exception/Success.java | 35 +++ .../exception/model/BadRequestException.java | 9 + .../exception/model/ConflictException.java | 9 + .../exception/model/NotFoundException.java | 9 + .../exception/model/SoptException.java | 19 ++ .../model/UnauthorizedException.java | 10 + .../external/client/aws/S3Service.java | 109 ++++++++ .../infrastructure/BoardRepository.java | 16 ++ .../infrastructure/EmotionRepository.java | 23 ++ .../infrastructure/ImageRepository.java | 9 + .../infrastructure/PostRepository.java | 13 + .../infrastructure/UserRepository.java | 23 ++ .../fourthSeminar/service/BoardService.java | 47 ++++ .../fourthSeminar/service/EmotionService.java | 70 +++++ .../fourthSeminar/service/PostService.java | 42 +++ .../fourthSeminar/service/UserService.java | 53 ++++ .../dto/request/EmotionServiceDto.java | 32 +++ src/main/resources/application.yaml | 50 ++++ .../FourthSeminarApplicationTests.java | 13 + 55 files changed, 2029 insertions(+) create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/sopt/org/fourthSeminar/FourthSeminarApplication.java create mode 100644 src/main/java/sopt/org/fourthSeminar/common/advice/ControllerExceptionAdvice.java create mode 100644 src/main/java/sopt/org/fourthSeminar/common/dto/ApiResponse.java create mode 100644 src/main/java/sopt/org/fourthSeminar/config/WebConfig.java create mode 100644 src/main/java/sopt/org/fourthSeminar/config/jwt/JwtService.java create mode 100644 src/main/java/sopt/org/fourthSeminar/config/resolver/UserId.java create mode 100644 src/main/java/sopt/org/fourthSeminar/config/resolver/UserIdResolver.java create mode 100644 src/main/java/sopt/org/fourthSeminar/config/swagger/SwaggerConfig.java create mode 100644 src/main/java/sopt/org/fourthSeminar/controller/BoardController.java create mode 100644 src/main/java/sopt/org/fourthSeminar/controller/EmotionController.java create mode 100644 src/main/java/sopt/org/fourthSeminar/controller/PostController.java create mode 100644 src/main/java/sopt/org/fourthSeminar/controller/ServerProfileController.java create mode 100644 src/main/java/sopt/org/fourthSeminar/controller/UserController.java create mode 100644 src/main/java/sopt/org/fourthSeminar/controller/dto/BoardRequestDto.java create mode 100644 src/main/java/sopt/org/fourthSeminar/controller/dto/request/BoardImageListRequestDto.java create mode 100644 src/main/java/sopt/org/fourthSeminar/controller/dto/request/BoardRequestDto.java create mode 100644 src/main/java/sopt/org/fourthSeminar/controller/dto/request/EmotionRequestDto.java create mode 100644 src/main/java/sopt/org/fourthSeminar/controller/dto/request/UserRequestDto.java create mode 100644 src/main/java/sopt/org/fourthSeminar/controller/dto/response/EmotionCalendarResponseDto.java create mode 100644 src/main/java/sopt/org/fourthSeminar/controller/dto/response/EmotionResponseDto.java create mode 100644 src/main/java/sopt/org/fourthSeminar/controller/dto/response/PostResponseDto.java create mode 100644 src/main/java/sopt/org/fourthSeminar/controller/dto/response/UserResponseDto.java create mode 100644 src/main/java/sopt/org/fourthSeminar/controller/request/dto/UserLoginRequestDto.java create mode 100644 src/main/java/sopt/org/fourthSeminar/controller/request/dto/UserLoginResponseDto.java create mode 100644 src/main/java/sopt/org/fourthSeminar/domain/AuditingTimeEntity.java create mode 100644 src/main/java/sopt/org/fourthSeminar/domain/Board.java create mode 100644 src/main/java/sopt/org/fourthSeminar/domain/Emotion.java create mode 100644 src/main/java/sopt/org/fourthSeminar/domain/Image.java create mode 100644 src/main/java/sopt/org/fourthSeminar/domain/Post.java create mode 100644 src/main/java/sopt/org/fourthSeminar/domain/User.java create mode 100644 src/main/java/sopt/org/fourthSeminar/exception/Error.java create mode 100644 src/main/java/sopt/org/fourthSeminar/exception/Success.java create mode 100644 src/main/java/sopt/org/fourthSeminar/exception/model/BadRequestException.java create mode 100644 src/main/java/sopt/org/fourthSeminar/exception/model/ConflictException.java create mode 100644 src/main/java/sopt/org/fourthSeminar/exception/model/NotFoundException.java create mode 100644 src/main/java/sopt/org/fourthSeminar/exception/model/SoptException.java create mode 100644 src/main/java/sopt/org/fourthSeminar/exception/model/UnauthorizedException.java create mode 100644 src/main/java/sopt/org/fourthSeminar/external/client/aws/S3Service.java create mode 100644 src/main/java/sopt/org/fourthSeminar/infrastructure/BoardRepository.java create mode 100644 src/main/java/sopt/org/fourthSeminar/infrastructure/EmotionRepository.java create mode 100644 src/main/java/sopt/org/fourthSeminar/infrastructure/ImageRepository.java create mode 100644 src/main/java/sopt/org/fourthSeminar/infrastructure/PostRepository.java create mode 100644 src/main/java/sopt/org/fourthSeminar/infrastructure/UserRepository.java create mode 100644 src/main/java/sopt/org/fourthSeminar/service/BoardService.java create mode 100644 src/main/java/sopt/org/fourthSeminar/service/EmotionService.java create mode 100644 src/main/java/sopt/org/fourthSeminar/service/PostService.java create mode 100644 src/main/java/sopt/org/fourthSeminar/service/UserService.java create mode 100644 src/main/java/sopt/org/fourthSeminar/service/dto/request/EmotionServiceDto.java create mode 100644 src/main/resources/application.yaml create mode 100644 src/test/java/sopt/org/fourthSeminar/FourthSeminarApplicationTests.java diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..f8120ac --- /dev/null +++ b/build.gradle @@ -0,0 +1,48 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '2.7.11' + id 'io.spring.dependency-management' version '1.0.15.RELEASE' +} + +group = 'sopt.org' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = '11' + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + // Health Check + implementation 'org.springframework.boot:spring-boot-starter-actuator' + + // JPA & Database + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'mysql:mysql-connector-java:8.0.32' + + //JWT + implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2' + implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2' + implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2' + + // S3 AWS + implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-aws', version: '2.2.6.RELEASE' + + // swagger + implementation 'org.springdoc:springdoc-openapi-ui:1.7.0' +} +tasks.named('test') { + useJUnitPlatform() +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..774fae8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..a69d9cb --- /dev/null +++ b/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# 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"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + 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 + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..0e45ef5 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'fourthSeminar' diff --git a/src/main/java/sopt/org/fourthSeminar/FourthSeminarApplication.java b/src/main/java/sopt/org/fourthSeminar/FourthSeminarApplication.java new file mode 100644 index 0000000..5169ad2 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/FourthSeminarApplication.java @@ -0,0 +1,13 @@ +package sopt.org.fourthSeminar; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@EnableJpaAuditing +@SpringBootApplication +public class FourthSeminarApplication { + public static void main(String[] args) { + SpringApplication.run(FourthSeminarApplication.class, args); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/common/advice/ControllerExceptionAdvice.java b/src/main/java/sopt/org/fourthSeminar/common/advice/ControllerExceptionAdvice.java new file mode 100644 index 0000000..5433a37 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/common/advice/ControllerExceptionAdvice.java @@ -0,0 +1,69 @@ +package sopt.org.fourthSeminar.common.advice; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import sopt.org.fourthSeminar.common.dto.ApiResponse; +import sopt.org.fourthSeminar.exception.Error; +import sopt.org.fourthSeminar.exception.model.SoptException; + +import javax.validation.UnexpectedTypeException; +import java.util.Objects; + +@RestControllerAdvice +public class ControllerExceptionAdvice { + + /** + * 400 BAD_REQUEST + */ + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ApiResponse handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) { + FieldError fieldError = Objects.requireNonNull(e.getFieldError()); + return ApiResponse.error(Error.REQUEST_VALIDATION_EXCEPTION, String.format("%s. (%s)", fieldError.getDefaultMessage(), fieldError.getField())); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MissingServletRequestParameterException.class) + protected ApiResponse handleMissingServletRequestParameterException(final MissingServletRequestParameterException e) { + return ApiResponse.error(Error.NO_REQUEST_PARAMETER_EXCEPTION, String.format("%s. (%s)", Error.NO_REQUEST_PARAMETER_EXCEPTION.getMessage(), e.getParameterName())); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + protected ApiResponse handleMethodArgumentTypeMismatchException(final MethodArgumentTypeMismatchException e) { + return ApiResponse.error(Error.PARAMETER_TYPE_MISMATCH_EXCEPTION, String.format("%s. (%s)", Error.PARAMETER_TYPE_MISMATCH_EXCEPTION.getMessage(), e.getName())); + } + +// @ResponseStatus(HttpStatus.BAD_REQUEST) +// @ExceptionHandler(UnexpectedTypeException.class) +// protected ApiResponse handleUnexpectedTypeException(final UnexpectedTypeException e) { +// return ApiResponse.error(Error.VALIDATION_WRONG_TYPE_EXCEPTION); +// } +// +// /** +// * 500 Internal Server +// */ +// @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) +// @ExceptionHandler(Exception.class) +// protected ApiResponse handleException(final Exception e) { +// return ApiResponse.error(Error.INTERNAL_SERVER_ERROR); +// } + + /** + * Sopt custom error + */ + @ExceptionHandler(SoptException.class) + protected ResponseEntity handleSoptException(SoptException e) { + return ResponseEntity.status(e.getHttpStatus()) + .body(ApiResponse.error(e.getError(), e.getMessage())); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/common/dto/ApiResponse.java b/src/main/java/sopt/org/fourthSeminar/common/dto/ApiResponse.java new file mode 100644 index 0000000..b2af8a8 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/common/dto/ApiResponse.java @@ -0,0 +1,34 @@ +package sopt.org.fourthSeminar.common.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import sopt.org.fourthSeminar.exception.Error; +import sopt.org.fourthSeminar.exception.Success; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ApiResponse { + + private final int code; + private final String message; + private T data; + + public static ApiResponse success(Success success) { + return new ApiResponse<>(success.getHttpStatusCode(), success.getMessage()); + } + + public static ApiResponse success(Success success, T data) { + return new ApiResponse(success.getHttpStatusCode(), success.getMessage(), data); + } + + public static ApiResponse error(Error error) { + return new ApiResponse<>(error.getHttpStatusCode(), error.getMessage()); + } + + public static ApiResponse error(Error error, String message) { + return new ApiResponse<>(error.getHttpStatusCode(), message); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/config/WebConfig.java b/src/main/java/sopt/org/fourthSeminar/config/WebConfig.java new file mode 100644 index 0000000..9d78cd4 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/config/WebConfig.java @@ -0,0 +1,21 @@ +package sopt.org.fourthSeminar.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import sopt.org.fourthSeminar.config.resolver.UserIdResolver; + +import java.util.List; + +@RequiredArgsConstructor +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final UserIdResolver userIdResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(userIdResolver); + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/config/jwt/JwtService.java b/src/main/java/sopt/org/fourthSeminar/config/jwt/JwtService.java new file mode 100644 index 0000000..5a6c0dd --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/config/jwt/JwtService.java @@ -0,0 +1,79 @@ +package sopt.org.fourthSeminar.config.jwt; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import sopt.org.fourthSeminar.exception.Error; +import sopt.org.fourthSeminar.exception.model.UnauthorizedException; + +import javax.annotation.PostConstruct; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Base64; +import java.util.Date; + +@Service +public class JwtService { + + @Value("${jwt.secret}") + private String jwtSecret; + + @PostConstruct + protected void init() { + jwtSecret = Base64.getEncoder() + .encodeToString(jwtSecret.getBytes(StandardCharsets.UTF_8)); + } + + // JWT 토큰 발급 + public String issuedToken(String userId) { + final Date now = new Date(); + + // 클레임 생성 + final Claims claims = Jwts.claims() + .setSubject("access_token") + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + 120 * 60 * 1000L)); + + //private claim 등록 + claims.put("userId", userId); + + return Jwts.builder() + .setHeaderParam(Header.TYPE , Header.JWT_TYPE) + .setClaims(claims) + .signWith(getSigningKey()) + .compact(); + } + + private Key getSigningKey() { + final byte[] keyBytes = jwtSecret.getBytes(StandardCharsets.UTF_8); + return Keys.hmacShaKeyFor(keyBytes); + } + + // JWT 토큰 검증 + public boolean verifyToken(String token) { + try { + final Claims claims = getBody(token); + return true; + } catch (RuntimeException e) { + if (e instanceof ExpiredJwtException) { + throw new UnauthorizedException(Error.TOKEN_TIME_EXPIRED_EXCEPTION, Error.TOKEN_TIME_EXPIRED_EXCEPTION.getMessage()); + } + return false; + } + } + + private Claims getBody(final String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + // JWT 토큰 내용 확인 + public String getJwtContents(String token) { + final Claims claims = getBody(token); + return (String) claims.get("userId"); + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/config/resolver/UserId.java b/src/main/java/sopt/org/fourthSeminar/config/resolver/UserId.java new file mode 100644 index 0000000..b9f23f7 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/config/resolver/UserId.java @@ -0,0 +1,11 @@ +package sopt.org.fourthSeminar.config.resolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface UserId { +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/config/resolver/UserIdResolver.java b/src/main/java/sopt/org/fourthSeminar/config/resolver/UserIdResolver.java new file mode 100644 index 0000000..767dad0 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/config/resolver/UserIdResolver.java @@ -0,0 +1,45 @@ +package sopt.org.fourthSeminar.config.resolver; + +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import sopt.org.fourthSeminar.config.jwt.JwtService; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.constraints.NotNull; + +@RequiredArgsConstructor +@Component +public class UserIdResolver implements HandlerMethodArgumentResolver { + + private final JwtService jwtService; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(UserId.class) && Long.class.equals(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(@NotNull MethodParameter parameter, ModelAndViewContainer modelAndViewContainer, @NotNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + final HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + //final String token = request.getHeader("Authorization"); + final String token = request.getHeader("Authorization").split(" ")[1]; + + // 토큰 검증 + if (!jwtService.verifyToken(token)) { + throw new RuntimeException(String.format("USER_ID를 가져오지 못했습니다. (%s - %s)", parameter.getClass(), parameter.getMethod())); + } + + // 유저 아이디 반환 + final String tokenContents = jwtService.getJwtContents(token); + try { + return Long.parseLong(tokenContents); + } catch (NumberFormatException e) { + throw new RuntimeException(String.format("USER_ID를 가져오지 못했습니다. (%s - %s)", parameter.getClass(), parameter.getMethod())); + } + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/config/swagger/SwaggerConfig.java b/src/main/java/sopt/org/fourthSeminar/config/swagger/SwaggerConfig.java new file mode 100644 index 0000000..1cac380 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/config/swagger/SwaggerConfig.java @@ -0,0 +1,31 @@ +package sopt.org.fourthSeminar.config.swagger; + +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@SecurityScheme( + name = "JWT Auth", + type = SecuritySchemeType.HTTP, + bearerFormat = "JWT", + scheme = "bearer" +) +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + Info info = new Info() + .title("SOPT32nd Seminar project") + .description("SOPT32nd Seminar project API Document") + .version("1.0.0"); + + return new OpenAPI() + .components(new Components()) + .info(info); + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/controller/BoardController.java b/src/main/java/sopt/org/fourthSeminar/controller/BoardController.java new file mode 100644 index 0000000..416a882 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/controller/BoardController.java @@ -0,0 +1,36 @@ +package sopt.org.fourthSeminar.controller; + +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import sopt.org.fourthSeminar.common.dto.ApiResponse; +import sopt.org.fourthSeminar.config.resolver.UserId; +import sopt.org.fourthSeminar.controller.dto.request.BoardImageListRequestDto; +import sopt.org.fourthSeminar.exception.Success; +import sopt.org.fourthSeminar.external.client.aws.S3Service; +import sopt.org.fourthSeminar.service.BoardService; + +import javax.validation.Valid; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/board") +@SecurityRequirement(name = "JWT Auth") +public class BoardController { + + private final S3Service s3Service; + private final BoardService boardService; + + @PostMapping(value = "/create", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.CREATED) + public ApiResponse create( + @UserId Long userId, + @ModelAttribute @Valid final BoardImageListRequestDto request) { + List boardThumbnailImageUrlList = s3Service.uploadImages(request.getBoardImages(), "board"); + boardService.create(userId, boardThumbnailImageUrlList, request); + return ApiResponse.success(Success.CREATE_BOARD_SUCCESS); + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/controller/EmotionController.java b/src/main/java/sopt/org/fourthSeminar/controller/EmotionController.java new file mode 100644 index 0000000..2d61b1a --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/controller/EmotionController.java @@ -0,0 +1,43 @@ +package sopt.org.fourthSeminar.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import sopt.org.fourthSeminar.common.dto.ApiResponse; +import sopt.org.fourthSeminar.controller.dto.request.EmotionRequestDto; +import sopt.org.fourthSeminar.controller.dto.response.EmotionCalendarResponseDto; +import sopt.org.fourthSeminar.controller.dto.response.EmotionResponseDto; +import sopt.org.fourthSeminar.exception.Success; +import sopt.org.fourthSeminar.service.EmotionService; + +import javax.validation.Valid; +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/emotion") +public class EmotionController { + + private final EmotionService emotionService; + + @PostMapping("") + @ResponseStatus(HttpStatus.CREATED) + public ApiResponse create(@RequestBody @Valid final EmotionRequestDto request) { + emotionService.create(request.toServiceDto()); + return ApiResponse.success(Success.CREATE_EMOTION_SUCCESS); + } + + @GetMapping("") + @ResponseStatus(HttpStatus.OK) + public ApiResponse> getCalendar(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") final LocalDate date) { + return ApiResponse.success(Success.GET_EMOTION_CALENDAR_SUCCESS, emotionService.getCalendar(date)); + } + + @GetMapping("{emotionId}") + @ResponseStatus(HttpStatus.OK) + public ApiResponse getOne(@PathVariable final Long emotionId) { + return ApiResponse.success(Success.GET_EMOTION_SUCCESS, emotionService.getOne(emotionId)); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/controller/PostController.java b/src/main/java/sopt/org/fourthSeminar/controller/PostController.java new file mode 100644 index 0000000..7dd3be5 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/controller/PostController.java @@ -0,0 +1,31 @@ +package sopt.org.fourthSeminar.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import sopt.org.fourthSeminar.common.dto.ApiResponse; +import sopt.org.fourthSeminar.controller.dto.response.PostResponseDto; +import sopt.org.fourthSeminar.exception.Success; +import sopt.org.fourthSeminar.service.PostService; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/web-gallery") +public class PostController { + + private final PostService postService; + + @GetMapping("") + @ResponseStatus(HttpStatus.OK) + public ApiResponse> getList() { + return ApiResponse.success(Success.GET_POST_LIST_SUCCESS, postService.getList()); + } + + @GetMapping("{postId}") + @ResponseStatus(HttpStatus.OK) + public ApiResponse getOne(@PathVariable final Long postId) { + return ApiResponse.success(Success.GET_POST_SUCCESS, postService.getOne(postId)); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/controller/ServerProfileController.java b/src/main/java/sopt/org/fourthSeminar/controller/ServerProfileController.java new file mode 100644 index 0000000..ae6f094 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/controller/ServerProfileController.java @@ -0,0 +1,22 @@ +package sopt.org.fourthSeminar.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.core.env.Environment; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Arrays; + +@RestController +@RequiredArgsConstructor +public class ServerProfileController { + + private final Environment env; + + @GetMapping("/profile") + public String getProfile() { + return Arrays.stream(env.getActiveProfiles()) + .findFirst() + .orElse(""); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/controller/UserController.java b/src/main/java/sopt/org/fourthSeminar/controller/UserController.java new file mode 100644 index 0000000..7a9a48d --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/controller/UserController.java @@ -0,0 +1,43 @@ +package sopt.org.fourthSeminar.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import sopt.org.fourthSeminar.common.dto.ApiResponse; +import sopt.org.fourthSeminar.config.jwt.JwtService; +import sopt.org.fourthSeminar.controller.dto.request.UserLoginRequestDto; +import sopt.org.fourthSeminar.controller.dto.request.UserRequestDto; +import sopt.org.fourthSeminar.controller.dto.response.UserLoginResponseDto; +import sopt.org.fourthSeminar.controller.dto.response.UserResponseDto; +import sopt.org.fourthSeminar.exception.Success; +import sopt.org.fourthSeminar.service.UserService; + +import javax.validation.Valid; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/user") +@Tag(name = "User", description = "유저 API Document") +public class UserController { + + private final UserService userService; + private final JwtService jwtService; + + @PostMapping("/signup") + @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "유저 생성 API", description = "유저를 서버에 등록합니다.") + public ApiResponse create(@RequestBody @Valid final UserRequestDto request) { + return ApiResponse.success(Success.SIGNUP_SUCCESS, userService.create(request)); + } + + @PostMapping("/login") + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "유저 로그인 API", description = "유저가 서버에 로그인을 요청합니다.") + public ApiResponse login(@RequestBody @Valid final UserLoginRequestDto request) { + final Long userId = userService.login(request); + final String token = jwtService.issuedToken(String.valueOf(userId)); + return ApiResponse.success(Success.LOGIN_SUCCESS, UserLoginResponseDto.of(userId, token)); + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/controller/dto/BoardRequestDto.java b/src/main/java/sopt/org/fourthSeminar/controller/dto/BoardRequestDto.java new file mode 100644 index 0000000..fe8f7ea --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/controller/dto/BoardRequestDto.java @@ -0,0 +1,27 @@ +package sopt.org.fourthSeminar.controller.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor +public class BoardRequestDto { + @NotNull + private MultipartFile thumbnail; + + @NotBlank + private String title; + + @NotBlank + private String content; + + @NotNull + private Boolean isPublic; +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/controller/dto/request/BoardImageListRequestDto.java b/src/main/java/sopt/org/fourthSeminar/controller/dto/request/BoardImageListRequestDto.java new file mode 100644 index 0000000..21fbf1d --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/controller/dto/request/BoardImageListRequestDto.java @@ -0,0 +1,23 @@ +package sopt.org.fourthSeminar.controller.dto.request; + +import lombok.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Getter +@Setter // ? +public class BoardImageListRequestDto { + private List boardImages; + + @NotBlank + private String title; + + @NotBlank + private String content; + + @NotNull + private Boolean isPublic; +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/controller/dto/request/BoardRequestDto.java b/src/main/java/sopt/org/fourthSeminar/controller/dto/request/BoardRequestDto.java new file mode 100644 index 0000000..446a4a8 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/controller/dto/request/BoardRequestDto.java @@ -0,0 +1,27 @@ +package sopt.org.fourthSeminar.controller.dto.request; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor +public class BoardRequestDto { + @NotNull + private MultipartFile thumbnail; + + @NotBlank + private String title; + + @NotBlank + private String content; + + @NotNull + private Boolean isPublic; +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/controller/dto/request/EmotionRequestDto.java b/src/main/java/sopt/org/fourthSeminar/controller/dto/request/EmotionRequestDto.java new file mode 100644 index 0000000..0f7c0d4 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/controller/dto/request/EmotionRequestDto.java @@ -0,0 +1,40 @@ +package sopt.org.fourthSeminar.controller.dto.request; + +import lombok.*; +import org.springframework.format.annotation.DateTimeFormat; +import sopt.org.fourthSeminar.service.dto.request.EmotionServiceDto; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalDate; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class EmotionRequestDto { + + @NotNull + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate recordDate; + + @NotBlank + private String emotionContent; + + @NotNull + private int positive; + + @NotNull + private int negative; + + @NotNull + private int neutral; + + @NotBlank + private String analysis; + + @NotNull + private String emotionType; + + public EmotionServiceDto toServiceDto() { + return EmotionServiceDto.of(recordDate, emotionContent, positive, negative, neutral, analysis, emotionType); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/controller/dto/request/UserRequestDto.java b/src/main/java/sopt/org/fourthSeminar/controller/dto/request/UserRequestDto.java new file mode 100644 index 0000000..fbf50c0 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/controller/dto/request/UserRequestDto.java @@ -0,0 +1,35 @@ +package sopt.org.fourthSeminar.controller.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Schema(description = "유저 생성 DTO") +public class UserRequestDto { + + @NotBlank + @Pattern(regexp = "^[가-힣a-zA-Z]{2,10}$", message = "닉네임 형식에 맞지 않습니다") + @Schema(description = "유저 닉네임") + private String nickname; + + @Email(message = "이메일 형식에 맞지 않습니다") + @NotBlank + @Schema(description = "유저 이메일") + private String email; + + @NotNull + @Pattern( + regexp="(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}", + message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 8자 ~ 20자의 비밀번호여야 합니다" + ) + @Schema(description = "유저 비밀번호") + private String password; +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/controller/dto/response/EmotionCalendarResponseDto.java b/src/main/java/sopt/org/fourthSeminar/controller/dto/response/EmotionCalendarResponseDto.java new file mode 100644 index 0000000..2fb2894 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/controller/dto/response/EmotionCalendarResponseDto.java @@ -0,0 +1,24 @@ +package sopt.org.fourthSeminar.controller.dto.response; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class EmotionCalendarResponseDto { + + private Long emotionId; + + private LocalDate recordDate; + + private String emotionType; + + public static EmotionCalendarResponseDto of(Long emotionId, LocalDate recordDate, String emotionType) { + return new EmotionCalendarResponseDto(emotionId, recordDate, emotionType); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/controller/dto/response/EmotionResponseDto.java b/src/main/java/sopt/org/fourthSeminar/controller/dto/response/EmotionResponseDto.java new file mode 100644 index 0000000..5dc77a1 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/controller/dto/response/EmotionResponseDto.java @@ -0,0 +1,32 @@ +package sopt.org.fourthSeminar.controller.dto.response; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class EmotionResponseDto { + + private LocalDate recordDate; + + private String emotionContent; + + private int positive; + + private int negative; + + private int neutral; + + private String analysis; + + private String emotionType; + + public static EmotionResponseDto of(LocalDate recordDate, String emotionContent, int positive, int negative, int neutral, String analysis, String emotionType) { + return new EmotionResponseDto(recordDate, emotionContent, positive, negative, neutral, analysis, emotionType); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/controller/dto/response/PostResponseDto.java b/src/main/java/sopt/org/fourthSeminar/controller/dto/response/PostResponseDto.java new file mode 100644 index 0000000..1b1266a --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/controller/dto/response/PostResponseDto.java @@ -0,0 +1,22 @@ +package sopt.org.fourthSeminar.controller.dto.response; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class PostResponseDto { + + private Long id; + private String imgSrc; + private String name; + private String date; + private String content; + + public static PostResponseDto of(Long id, String imgSrc, String name, String date, String content) { + return new PostResponseDto(id, imgSrc, name, date, content); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/controller/dto/response/UserResponseDto.java b/src/main/java/sopt/org/fourthSeminar/controller/dto/response/UserResponseDto.java new file mode 100644 index 0000000..9497889 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/controller/dto/response/UserResponseDto.java @@ -0,0 +1,19 @@ +package sopt.org.fourthSeminar.controller.dto.response; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class UserResponseDto { + + private Long userId; + private String nickname; + + public static UserResponseDto of(Long userId, String nickname) { + return new UserResponseDto(userId, nickname); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/controller/request/dto/UserLoginRequestDto.java b/src/main/java/sopt/org/fourthSeminar/controller/request/dto/UserLoginRequestDto.java new file mode 100644 index 0000000..5e690a4 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/controller/request/dto/UserLoginRequestDto.java @@ -0,0 +1,26 @@ +package sopt.org.fourthSeminar.controller.dto.request; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class UserLoginRequestDto { + + @Email(message = "이메일 형식에 맞지 않습니다") + @NotBlank + private String email; + + @NotNull + @Pattern( + regexp="(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}", + message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 8자 ~ 20자의 비밀번호여야 합니다" + ) + private String password; +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/controller/request/dto/UserLoginResponseDto.java b/src/main/java/sopt/org/fourthSeminar/controller/request/dto/UserLoginResponseDto.java new file mode 100644 index 0000000..8af6e34 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/controller/request/dto/UserLoginResponseDto.java @@ -0,0 +1,18 @@ +package sopt.org.fourthSeminar.controller.dto.response; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class UserLoginResponseDto { + private Long userId; + private String accessToken; + + public static UserLoginResponseDto of(Long userId, String accessToken) { + return new UserLoginResponseDto(userId, accessToken); + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/domain/AuditingTimeEntity.java b/src/main/java/sopt/org/fourthSeminar/domain/AuditingTimeEntity.java new file mode 100644 index 0000000..e2e88f9 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/domain/AuditingTimeEntity.java @@ -0,0 +1,22 @@ +package sopt.org.fourthSeminar.domain; + +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class AuditingTimeEntity { + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/src/main/java/sopt/org/fourthSeminar/domain/Board.java b/src/main/java/sopt/org/fourthSeminar/domain/Board.java new file mode 100644 index 0000000..355a7ee --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/domain/Board.java @@ -0,0 +1,44 @@ +package sopt.org.fourthSeminar.domain; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Board extends AuditingTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(ConstraintMode.CONSTRAINT)) + private User user; + + @Column + private String thumbnail; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String content; + + @Column(nullable = false) + private Boolean isPublic; + + private Board(User user, String title, String content, Boolean isPublic) { + this.user = user; + this.title =title; + this.content = content; + this.isPublic = isPublic; + } + + public static Board newInstance(User user, String title, String content, Boolean isPublic) { + return new Board(user, title, content, isPublic); + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/domain/Emotion.java b/src/main/java/sopt/org/fourthSeminar/domain/Emotion.java new file mode 100644 index 0000000..00cdc41 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/domain/Emotion.java @@ -0,0 +1,53 @@ +package sopt.org.fourthSeminar.domain; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.time.LocalDate; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Emotion extends AuditingTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String emotionContent; + + @Column(nullable = false) + private LocalDate recordDate; + + @Column(nullable = false) + private int positive; + + @Column(nullable = false) + private int negative; + + @Column(nullable = false) + private int neutral; + + @Column(nullable = false) + private String analysis; + + @Column(nullable = false) + private String emotionType; + + private Emotion(String emotionContent, LocalDate recordDate, int positive, int negative, int neutral, String analysis, String emotionType) { + this.emotionContent = emotionContent; + this.recordDate = recordDate; + this.positive = positive; + this.negative = negative; + this.neutral = neutral; + this.analysis = analysis; + this.emotionType = emotionType; + } + + public static Emotion newInstance(String emotionContent, LocalDate recordDate, int positive, int negative, int neutral, String analysis, String emotionType) { + return new Emotion(emotionContent, recordDate, positive, negative, neutral, analysis, emotionType); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/domain/Image.java b/src/main/java/sopt/org/fourthSeminar/domain/Image.java new file mode 100644 index 0000000..2832487 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/domain/Image.java @@ -0,0 +1,33 @@ +package sopt.org.fourthSeminar.domain; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Image extends AuditingTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "board_id", nullable = false, foreignKey = @ForeignKey(ConstraintMode.CONSTRAINT)) + private Board board; + + @Column(nullable = false) + private String imageUrl; + + private Image(Board board, String imageUrl) { + this.board = board; + this.imageUrl = imageUrl; + } + + public static Image newInstance(Board board, String imageUrl) { + return new Image(board, imageUrl); + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/domain/Post.java b/src/main/java/sopt/org/fourthSeminar/domain/Post.java new file mode 100644 index 0000000..86f62f2 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/domain/Post.java @@ -0,0 +1,40 @@ +package sopt.org.fourthSeminar.domain; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Post extends AuditingTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String imgSrc; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String date; + + @Column(nullable = false) + private String content; + + private Post(String imgSrc, String name, String date, String content) { + this.imgSrc = imgSrc; + this.name = name; + this.date = date; + this.content = content; + } + + public static Post newInstance(String imgSrc, String name, String date, String content) { + return new Post(imgSrc, name, date, content); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/domain/User.java b/src/main/java/sopt/org/fourthSeminar/domain/User.java new file mode 100644 index 0000000..404d9ee --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/domain/User.java @@ -0,0 +1,36 @@ +package sopt.org.fourthSeminar.domain; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class User extends AuditingTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String nickname; + + @Column(nullable = false) + private String email; + + @Column(nullable = false) + private String password; + + private User(String nickname, String email, String password) { + this.nickname = nickname; + this.email = email; + this.password = password; + } + + public static User newInstance(String nickname, String email, String password) { + return new User(nickname, email, password); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/exception/Error.java b/src/main/java/sopt/org/fourthSeminar/exception/Error.java new file mode 100644 index 0000000..65c4717 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/exception/Error.java @@ -0,0 +1,54 @@ +package sopt.org.fourthSeminar.exception; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum Error { + + /** + * 400 BAD REQUEST + */ + REQUEST_VALIDATION_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 요청입니다"), + NO_REQUEST_PARAMETER_EXCEPTION(HttpStatus.BAD_REQUEST, "요청 파라미터 값이 없습니다"), + VALIDATION_WRONG_TYPE_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 타입이 입력되었습니다"), + PARAMETER_TYPE_MISMATCH_EXCEPTION(HttpStatus.BAD_REQUEST, "파라미터의 타입이 잘못됐습니다"), + INVALID_PASSWORD_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 비밀번호가 입력됐습니다."), + + /** + * 401 UNAUTHORIZED + */ + TOKEN_TIME_EXPIRED_EXCEPTION(HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."), + + /** + * 404 NOT FOUND + */ + NOT_FOUND_USER_EXCEPTION(HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다"), + NOT_FOUND_POST_EXCEPTION(HttpStatus.NOT_FOUND, "존재하지 않는 게시물입니다"), + NOT_FOUND_EMOTION_EXCEPTION(HttpStatus.NOT_FOUND, "존재하지 않는 감정 기록입니다"), + + /** + * 409 CONFLICT + */ + ALREADY_EXIST_USER_EXCEPTION(HttpStatus.CONFLICT, "이미 존재하는 유저입니다"), + ALREADY_EXIST_EMOTION_EXCEPTION(HttpStatus.CONFLICT, "이미 해당 날짜에 감정이 기록되었습니다"), + + + /** + * 500 INTERNAL SERVER ERROR + */ + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 서버 에러가 발생했습니다"), + NOT_FOUND_SAVE_IMAGE_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 서버 에러가 발생했습니다"), + NOT_FOUND_IMAGE_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 서버 에러가 발생했습니다"), + INVALID_MULTIPART_EXTENSION_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 서버 에러가 발생했습니다"); + + private final HttpStatus httpStatus; + private final String message; + + public int getHttpStatusCode() { + return httpStatus.value(); + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/exception/Success.java b/src/main/java/sopt/org/fourthSeminar/exception/Success.java new file mode 100644 index 0000000..404cbd3 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/exception/Success.java @@ -0,0 +1,35 @@ +package sopt.org.fourthSeminar.exception; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum Success { + + /** + * 200 OK + */ + LOGIN_SUCCESS(HttpStatus.OK, "로그인에 성공했습니다."), + GET_POST_LIST_SUCCESS(HttpStatus.OK, "게시물 리스트 조회에 성공했습니다."), + GET_POST_SUCCESS(HttpStatus.OK, "게시물 조회에 성공했습니다."), + GET_EMOTION_CALENDAR_SUCCESS(HttpStatus.OK, "감정 캘린더 조회에 성공했습니다."), + GET_EMOTION_SUCCESS(HttpStatus.OK, "감정 조회에 성공했습니다."), + + /** + * 201 CREATED + */ + SIGNUP_SUCCESS(HttpStatus.CREATED, "회원가입이 완료됐습니다."), + CREATE_BOARD_SUCCESS(HttpStatus.CREATED, "게시물 생성이 완료됐습니다."), + CREATE_EMOTION_SUCCESS(HttpStatus.CREATED, "감정 기록에 성공했습니다."), + ; + + private final HttpStatus httpStatus; + private final String message; + + public int getHttpStatusCode() { + return httpStatus.value(); + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/exception/model/BadRequestException.java b/src/main/java/sopt/org/fourthSeminar/exception/model/BadRequestException.java new file mode 100644 index 0000000..3cb5082 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/exception/model/BadRequestException.java @@ -0,0 +1,9 @@ +package sopt.org.fourthSeminar.exception.model; + +import sopt.org.fourthSeminar.exception.Error; + +public class BadRequestException extends SoptException{ + public BadRequestException(Error error, String message) { + super(error, message); + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/exception/model/ConflictException.java b/src/main/java/sopt/org/fourthSeminar/exception/model/ConflictException.java new file mode 100644 index 0000000..1800a13 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/exception/model/ConflictException.java @@ -0,0 +1,9 @@ +package sopt.org.fourthSeminar.exception.model; + +import sopt.org.fourthSeminar.exception.Error; + +public class ConflictException extends SoptException { + public ConflictException(Error error, String message) { + super(error, message); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/exception/model/NotFoundException.java b/src/main/java/sopt/org/fourthSeminar/exception/model/NotFoundException.java new file mode 100644 index 0000000..1e039ab --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/exception/model/NotFoundException.java @@ -0,0 +1,9 @@ +package sopt.org.fourthSeminar.exception.model; + +import sopt.org.fourthSeminar.exception.Error; + +public class NotFoundException extends SoptException { + public NotFoundException(Error error, String message) { + super(error, message); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/exception/model/SoptException.java b/src/main/java/sopt/org/fourthSeminar/exception/model/SoptException.java new file mode 100644 index 0000000..b284d9c --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/exception/model/SoptException.java @@ -0,0 +1,19 @@ +package sopt.org.fourthSeminar.exception.model; + +import lombok.Getter; +import sopt.org.fourthSeminar.exception.Error; + +@Getter +public class SoptException extends RuntimeException { + + private final Error error; + + public SoptException(Error error, String message) { + super(message); + this.error = error; + } + + public int getHttpStatus() { + return error.getHttpStatusCode(); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/exception/model/UnauthorizedException.java b/src/main/java/sopt/org/fourthSeminar/exception/model/UnauthorizedException.java new file mode 100644 index 0000000..2d3e0ef --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/exception/model/UnauthorizedException.java @@ -0,0 +1,10 @@ +package sopt.org.fourthSeminar.exception.model; + +import sopt.org.fourthSeminar.exception.Error; + +public class UnauthorizedException extends SoptException{ + public UnauthorizedException(Error error, String message) + { + super(error, message); + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/external/client/aws/S3Service.java b/src/main/java/sopt/org/fourthSeminar/external/client/aws/S3Service.java new file mode 100644 index 0000000..a04962c --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/external/client/aws/S3Service.java @@ -0,0 +1,109 @@ +package sopt.org.fourthSeminar.external.client.aws; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import sopt.org.fourthSeminar.exception.Error; +import sopt.org.fourthSeminar.exception.model.BadRequestException; +import sopt.org.fourthSeminar.exception.model.NotFoundException; + +import javax.annotation.PostConstruct; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class S3Service { + private final AmazonS3 amazonS3; + + @Value("${cloud.aws.credentials.accessKey}") + private String accessKey; + + @Value("${cloud.aws.credentials.secretKey}") + private String secretKey; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Value("${cloud.aws.region.static}") + private String region; + + @PostConstruct + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } + + public String uploadImage(MultipartFile multipartFile, String folder) { + String fileName = createFileName(multipartFile.getOriginalFilename()); + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentLength(multipartFile.getSize()); + objectMetadata.setContentType(multipartFile.getContentType()); + + try(InputStream inputStream = multipartFile.getInputStream()) { + amazonS3.putObject(new PutObjectRequest(bucket+"/"+ folder + "/image", fileName, inputStream, objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + return amazonS3.getUrl(bucket+"/"+ folder + "/image", fileName).toString(); + } catch(IOException e) { + throw new NotFoundException(Error.NOT_FOUND_SAVE_IMAGE_EXCEPTION, Error.NOT_FOUND_SAVE_IMAGE_EXCEPTION.getMessage()); + } + } + + public List uploadImages(List multipartFileList, String folder) { + List imageUrls = new ArrayList<>(); + for(MultipartFile multipartFile: multipartFileList){ + String fileName = createFileName(multipartFile.getOriginalFilename()); + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentLength(multipartFile.getSize()); + objectMetadata.setContentType(multipartFile.getContentType()); + + try(InputStream inputStream = multipartFile.getInputStream()) { + amazonS3.putObject(new PutObjectRequest(bucket+"/"+ folder + "/image", fileName, inputStream, objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + imageUrls.add(amazonS3.getUrl(bucket+"/"+ folder + "/image", fileName).toString()); + } catch(IOException e) { + throw new NotFoundException(Error.NOT_FOUND_SAVE_IMAGE_EXCEPTION, Error.NOT_FOUND_SAVE_IMAGE_EXCEPTION.getMessage()); + } + } + return imageUrls; + } + + // 파일명 (중복 방지) + private String createFileName(String fileName) { + return UUID.randomUUID().toString().concat(getFileExtension(fileName)); + } + + // 파일 유효성 검사 + private String getFileExtension(String fileName) { + if (fileName.length() == 0) { + throw new NotFoundException(Error.NOT_FOUND_IMAGE_EXCEPTION, Error.NOT_FOUND_USER_EXCEPTION.getMessage()); + } + ArrayList fileValidate = new ArrayList<>(); + fileValidate.add(".jpg"); + fileValidate.add(".jpeg"); + fileValidate.add(".png"); + fileValidate.add(".JPG"); + fileValidate.add(".JPEG"); + fileValidate.add(".PNG"); + String idxFileName = fileName.substring(fileName.lastIndexOf(".")); + if (!fileValidate.contains(idxFileName)) { + throw new BadRequestException(Error.INVALID_MULTIPART_EXTENSION_EXCEPTION, Error.INVALID_MULTIPART_EXTENSION_EXCEPTION.getMessage()); + } + return fileName.substring(fileName.lastIndexOf(".")); + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/infrastructure/BoardRepository.java b/src/main/java/sopt/org/fourthSeminar/infrastructure/BoardRepository.java new file mode 100644 index 0000000..1488ff9 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/infrastructure/BoardRepository.java @@ -0,0 +1,16 @@ +package sopt.org.fourthSeminar.infrastructure; + +import org.springframework.data.repository.Repository; +import sopt.org.fourthSeminar.domain.Board; + +public interface BoardRepository extends Repository { + + // CREATE + void save(Board board); + + // READ + + // UPDATE + + // DELETE +} diff --git a/src/main/java/sopt/org/fourthSeminar/infrastructure/EmotionRepository.java b/src/main/java/sopt/org/fourthSeminar/infrastructure/EmotionRepository.java new file mode 100644 index 0000000..dba6af8 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/infrastructure/EmotionRepository.java @@ -0,0 +1,23 @@ +package sopt.org.fourthSeminar.infrastructure; + +import org.springframework.data.repository.Repository; +import sopt.org.fourthSeminar.domain.Emotion; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +public interface EmotionRepository extends Repository { + + // CREATE + void save(Emotion emotion); + + // READ + List findByRecordDateBetween(LocalDate start, LocalDate end); + Optional findById(Long id); + boolean existsByRecordDate(LocalDate recordDate); + + // UPDATE + + // DELETE +} diff --git a/src/main/java/sopt/org/fourthSeminar/infrastructure/ImageRepository.java b/src/main/java/sopt/org/fourthSeminar/infrastructure/ImageRepository.java new file mode 100644 index 0000000..59a19d6 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/infrastructure/ImageRepository.java @@ -0,0 +1,9 @@ +package sopt.org.fourthSeminar.infrastructure; + +import org.springframework.data.repository.Repository; +import sopt.org.fourthSeminar.domain.Image; + +public interface ImageRepository extends Repository { + // CREATE + void save(Image image); +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/infrastructure/PostRepository.java b/src/main/java/sopt/org/fourthSeminar/infrastructure/PostRepository.java new file mode 100644 index 0000000..8facfec --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/infrastructure/PostRepository.java @@ -0,0 +1,13 @@ +package sopt.org.fourthSeminar.infrastructure; + +import org.springframework.data.repository.Repository; +import sopt.org.fourthSeminar.domain.Post; + +import java.util.List; +import java.util.Optional; + +public interface PostRepository extends Repository { + List findAll(); + + Optional findById(Long id); +} diff --git a/src/main/java/sopt/org/fourthSeminar/infrastructure/UserRepository.java b/src/main/java/sopt/org/fourthSeminar/infrastructure/UserRepository.java new file mode 100644 index 0000000..a78da6f --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/infrastructure/UserRepository.java @@ -0,0 +1,23 @@ +package sopt.org.fourthSeminar.infrastructure; + +import org.springframework.boot.context.properties.bind.BindResult; +import org.springframework.data.repository.Repository; +import sopt.org.fourthSeminar.domain.User; + +import java.util.Optional; + +public interface UserRepository extends Repository { + + // CREATE + void save(User user); + + // READ + Optional findByEmail(String email); + boolean existsByEmail(String email); + + Optional findById(Long userId); + + // UPDATE + + // DELETE +} diff --git a/src/main/java/sopt/org/fourthSeminar/service/BoardService.java b/src/main/java/sopt/org/fourthSeminar/service/BoardService.java new file mode 100644 index 0000000..a5320f4 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/service/BoardService.java @@ -0,0 +1,47 @@ +package sopt.org.fourthSeminar.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sopt.org.fourthSeminar.controller.dto.request.BoardImageListRequestDto; +import sopt.org.fourthSeminar.controller.dto.request.BoardRequestDto; +import sopt.org.fourthSeminar.domain.Board; +import sopt.org.fourthSeminar.domain.Image; +import sopt.org.fourthSeminar.domain.User; +import sopt.org.fourthSeminar.exception.Error; +import sopt.org.fourthSeminar.exception.model.NotFoundException; +import sopt.org.fourthSeminar.infrastructure.BoardRepository; +import sopt.org.fourthSeminar.infrastructure.ImageRepository; +import sopt.org.fourthSeminar.infrastructure.UserRepository; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class BoardService { + + private final UserRepository userRepository; + private final BoardRepository boardRepository; + private final ImageRepository imageRepository; + + @Transactional + public void create(Long userId, List boardImageUrlList, BoardImageListRequestDto request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(Error.NOT_FOUND_USER_EXCEPTION, Error.NOT_FOUND_USER_EXCEPTION.getMessage())); + + // 게시글 생성 + Board newBoard = Board.newInstance( + user, + request.getTitle(), + request.getContent(), + request.getIsPublic() + ); + + boardRepository.save(newBoard); + + // 이미지 생성 + for (String boardImageUrl: boardImageUrlList) { + imageRepository.save(Image.newInstance(newBoard, boardImageUrl)); + } + } +} \ No newline at end of file diff --git a/src/main/java/sopt/org/fourthSeminar/service/EmotionService.java b/src/main/java/sopt/org/fourthSeminar/service/EmotionService.java new file mode 100644 index 0000000..e339555 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/service/EmotionService.java @@ -0,0 +1,70 @@ +package sopt.org.fourthSeminar.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sopt.org.fourthSeminar.controller.dto.response.EmotionCalendarResponseDto; +import sopt.org.fourthSeminar.controller.dto.response.EmotionResponseDto; +import sopt.org.fourthSeminar.domain.Emotion; +import sopt.org.fourthSeminar.exception.Error; +import sopt.org.fourthSeminar.exception.model.ConflictException; +import sopt.org.fourthSeminar.exception.model.NotFoundException; +import sopt.org.fourthSeminar.infrastructure.EmotionRepository; +import sopt.org.fourthSeminar.service.dto.request.EmotionServiceDto; + +import java.time.LocalDate; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class EmotionService { + + private final EmotionRepository emotionRepository; + + @Transactional + public void create(EmotionServiceDto request) { + if (emotionRepository.existsByRecordDate(request.getRecordDate())) { + throw new ConflictException(Error.ALREADY_EXIST_EMOTION_EXCEPTION, Error.ALREADY_EXIST_EMOTION_EXCEPTION.getMessage()); + } + + Emotion newEmotion = Emotion.newInstance( + request.getEmotionContent(), + request.getRecordDate(), + request.getPositive(), + request.getNegative(), + request.getNeutral(), + request.getAnalysis(), + request.getEmotionType() + ); + + emotionRepository.save(newEmotion); + } + + @Transactional + public List getCalendar(LocalDate date) { + List emotions = emotionRepository.findByRecordDateBetween(date.withDayOfMonth(1), date.withDayOfMonth(date.lengthOfMonth())); + + return emotions.stream() + .map(emotion -> EmotionCalendarResponseDto.of(emotion.getId(), emotion.getRecordDate(), emotion.getEmotionType())) + .sorted(Comparator.comparing(EmotionCalendarResponseDto::getRecordDate)) + .collect(Collectors.toList()); + } + + @Transactional + public EmotionResponseDto getOne(Long emotionId) { + Emotion emotion = emotionRepository.findById(emotionId) + .orElseThrow(() -> new NotFoundException(Error.NOT_FOUND_EMOTION_EXCEPTION, Error.NOT_FOUND_EMOTION_EXCEPTION.getMessage())); + + return EmotionResponseDto.of( + emotion.getRecordDate(), + emotion.getEmotionContent(), + emotion.getPositive(), + emotion.getNegative(), + emotion.getNeutral(), + emotion.getAnalysis(), + emotion.getEmotionType() + ); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/service/PostService.java b/src/main/java/sopt/org/fourthSeminar/service/PostService.java new file mode 100644 index 0000000..fed68b9 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/service/PostService.java @@ -0,0 +1,42 @@ +package sopt.org.fourthSeminar.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sopt.org.fourthSeminar.controller.dto.response.PostResponseDto; +import sopt.org.fourthSeminar.domain.Post; +import sopt.org.fourthSeminar.exception.Error; +import sopt.org.fourthSeminar.exception.model.NotFoundException; +import sopt.org.fourthSeminar.infrastructure.PostRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PostService { + + private final PostRepository postRepository; + + @Transactional + public List getList() { + List postList = postRepository.findAll(); + + return postList.stream() + .map(post -> PostResponseDto.of( + post.getId(), + post.getImgSrc(), + post.getName(), + post.getDate(), + post.getContent())) + .collect(Collectors.toList()); + } + + @Transactional + public PostResponseDto getOne(Long id) { + Post post = postRepository.findById(id) + .orElseThrow(() -> new NotFoundException(Error.NOT_FOUND_POST_EXCEPTION, Error.NOT_FOUND_POST_EXCEPTION.getMessage())); + + return PostResponseDto.of(post.getId(), post.getImgSrc(), post.getName(), post.getDate(), post.getDate()); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/service/UserService.java b/src/main/java/sopt/org/fourthSeminar/service/UserService.java new file mode 100644 index 0000000..bef6053 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/service/UserService.java @@ -0,0 +1,53 @@ +package sopt.org.fourthSeminar.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sopt.org.fourthSeminar.controller.dto.request.UserLoginRequestDto; +import sopt.org.fourthSeminar.controller.dto.request.UserRequestDto; +import sopt.org.fourthSeminar.controller.dto.response.UserResponseDto; +import sopt.org.fourthSeminar.domain.User; +import sopt.org.fourthSeminar.exception.Error; +import sopt.org.fourthSeminar.exception.model.ConflictException; +import sopt.org.fourthSeminar.exception.model.NotFoundException; +import sopt.org.fourthSeminar.infrastructure.UserRepository; +import sopt.org.fourthSeminar.exception.model.BadRequestException; + + + + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + @Transactional + public UserResponseDto create(UserRequestDto request) { + if (userRepository.existsByEmail(request.getEmail())) { + throw new ConflictException(Error.ALREADY_EXIST_USER_EXCEPTION, Error.ALREADY_EXIST_USER_EXCEPTION.getMessage()); + } + + User newUser = User.newInstance( + request.getNickname(), + request.getEmail(), + request.getPassword() + ); + + userRepository.save(newUser); + + return UserResponseDto.of(newUser.getId(), newUser.getNickname()); + } + + @Transactional + public Long login(UserLoginRequestDto request) { + User user = userRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new NotFoundException(Error.NOT_FOUND_USER_EXCEPTION, Error.NOT_FOUND_USER_EXCEPTION.getMessage())); + + if (!user.getPassword().equals(request.getPassword())) { + throw new BadRequestException(Error.INVALID_PASSWORD_EXCEPTION, Error.INVALID_PASSWORD_EXCEPTION.getMessage()); + } + + return user.getId(); + } +} diff --git a/src/main/java/sopt/org/fourthSeminar/service/dto/request/EmotionServiceDto.java b/src/main/java/sopt/org/fourthSeminar/service/dto/request/EmotionServiceDto.java new file mode 100644 index 0000000..3bc2769 --- /dev/null +++ b/src/main/java/sopt/org/fourthSeminar/service/dto/request/EmotionServiceDto.java @@ -0,0 +1,32 @@ +package sopt.org.fourthSeminar.service.dto.request; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class EmotionServiceDto { + + private LocalDate recordDate; + + private String emotionContent; + + private int positive; + + private int negative; + + private int neutral; + + private String analysis; + + private String emotionType; + + public static EmotionServiceDto of(LocalDate recordDate, String emotionContent, int positive, int negative, int neutral, String analysis, String emotionType) { + return new EmotionServiceDto(recordDate, emotionContent, positive, negative, neutral, analysis, emotionType); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..4bbbe78 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,50 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/sopt_32?useSSL=true&useUnicode=true&serverTimezone=Asia/Seoul + username: sopt_server + password: Qwer1234! + + jpa: + show-sql: true + hibernate: + ddl-auto: create + properties: + hibernate: + format_sql: true + +jwt: + secret: youcantreavelthesecretkey1234012300040 + +cloud: + aws: + credentials: + accessKey: AKIASEBYH7CLU4PJKM66 + secretKey: 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 + region: + static: ap-northeast-2 + s3: + bucket: sopt32-seminar-hong + stack: + auto: false + +logging: + level: + org: + hibernate: + type: + descriptor: + sql: trace + +springdoc: + packages-to-scan: sopt.org.fourthSeminar + default-consumes-media-type: application/json;charset=UTF-8 + default-produces-media-type: application/json;charset=UTF-8 + swagger-ui: + tags-sorter: alpah + operations-sorter: alpha + api-docs: + path: /api-docs/json + groups: + enabled: true + cache: \ No newline at end of file diff --git a/src/test/java/sopt/org/fourthSeminar/FourthSeminarApplicationTests.java b/src/test/java/sopt/org/fourthSeminar/FourthSeminarApplicationTests.java new file mode 100644 index 0000000..81ef130 --- /dev/null +++ b/src/test/java/sopt/org/fourthSeminar/FourthSeminarApplicationTests.java @@ -0,0 +1,13 @@ +package sopt.org.fourthSeminar; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class FourthSeminarApplicationTests { + + @Test + void contextLoads() { + } + +} From e9fe384c9d635c82c377fb521c3549af608a227b Mon Sep 17 00:00:00 2001 From: Hong0329 <97835512+Hong0329@users.noreply.github.com> Date: Thu, 15 Jun 2023 00:52:59 +0900 Subject: [PATCH 2/3] Update application.yaml --- src/main/resources/application.yaml | 51 +---------------------------- 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 4bbbe78..63fc813 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,50 +1 @@ -spring: - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/sopt_32?useSSL=true&useUnicode=true&serverTimezone=Asia/Seoul - username: sopt_server - password: Qwer1234! - - jpa: - show-sql: true - hibernate: - ddl-auto: create - properties: - hibernate: - format_sql: true - -jwt: - secret: youcantreavelthesecretkey1234012300040 - -cloud: - aws: - credentials: - accessKey: AKIASEBYH7CLU4PJKM66 - secretKey: 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 - region: - static: ap-northeast-2 - s3: - bucket: sopt32-seminar-hong - stack: - auto: false - -logging: - level: - org: - hibernate: - type: - descriptor: - sql: trace - -springdoc: - packages-to-scan: sopt.org.fourthSeminar - default-consumes-media-type: application/json;charset=UTF-8 - default-produces-media-type: application/json;charset=UTF-8 - swagger-ui: - tags-sorter: alpah - operations-sorter: alpha - api-docs: - path: /api-docs/json - groups: - enabled: true - cache: \ No newline at end of file +xxxx From 138c13860766706ec0751fa6987259546e497f0c Mon Sep 17 00:00:00 2001 From: Hong0329 <97835512+Hong0329@users.noreply.github.com> Date: Thu, 15 Jun 2023 00:53:23 +0900 Subject: [PATCH 3/3] Delete src/main/resources directory --- src/main/resources/application.yaml | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/main/resources/application.yaml diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml deleted file mode 100644 index 63fc813..0000000 --- a/src/main/resources/application.yaml +++ /dev/null @@ -1 +0,0 @@ -xxxx