diff --git a/fourthSeminar/.gitignore b/fourthSeminar/.gitignore new file mode 100644 index 0000000..733be1e --- /dev/null +++ b/fourthSeminar/.gitignore @@ -0,0 +1,39 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +application.yaml \ No newline at end of file diff --git a/fourthSeminar/build.gradle b/fourthSeminar/build.gradle new file mode 100644 index 0000000..92ba45c --- /dev/null +++ b/fourthSeminar/build.gradle @@ -0,0 +1,44 @@ +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' + +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/fourthSeminar/gradle/wrapper/gradle-wrapper.jar b/fourthSeminar/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/fourthSeminar/gradle/wrapper/gradle-wrapper.jar differ diff --git a/fourthSeminar/gradle/wrapper/gradle-wrapper.properties b/fourthSeminar/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..774fae8 --- /dev/null +++ b/fourthSeminar/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/fourthSeminar/gradlew b/fourthSeminar/gradlew new file mode 100644 index 0000000..a69d9cb --- /dev/null +++ b/fourthSeminar/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/fourthSeminar/gradlew.bat b/fourthSeminar/gradlew.bat new file mode 100644 index 0000000..53a6b23 --- /dev/null +++ b/fourthSeminar/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/fourthSeminar/settings.gradle b/fourthSeminar/settings.gradle new file mode 100644 index 0000000..0e45ef5 --- /dev/null +++ b/fourthSeminar/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'fourthSeminar' diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/FourthSeminarApplication.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/FourthSeminarApplication.java new file mode 100644 index 0000000..2ca0b29 --- /dev/null +++ b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/FourthSeminarApplication.java @@ -0,0 +1,15 @@ +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/fourthSeminar/src/main/java/sopt/org/fourthSeminar/common/advice/ControllerExceptionAdvice.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/common/advice/ControllerExceptionAdvice.java new file mode 100644 index 0000000..d8ad04c --- /dev/null +++ b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/common/advice/ControllerExceptionAdvice.java @@ -0,0 +1,47 @@ +package sopt.org.fourthSeminar.common.advice; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import sopt.org.fourthSeminar.common.dto.ApiResponse; +import sopt.org.fourthSeminar.exception.Error; +import sopt.org.fourthSeminar.exception.model.SoptException; + +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())); + } + + /** + * 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())); + } +} \ No newline at end of file diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/common/dto/ApiResponse.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/common/dto/ApiResponse.java new file mode 100644 index 0000000..125ac0d --- /dev/null +++ b/fourthSeminar/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.Success; +import sopt.org.fourthSeminar.exception.Error; + +@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); + } +} \ No newline at end of file diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/config/WebConfig.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/config/WebConfig.java new file mode 100644 index 0000000..baba9e2 --- /dev/null +++ b/fourthSeminar/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); + } +} diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/config/jwt/JwtService.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/config/jwt/JwtService.java new file mode 100644 index 0000000..5c5a398 --- /dev/null +++ b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/config/jwt/JwtService.java @@ -0,0 +1,80 @@ +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(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"); + } +} diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/config/resolver/UserId.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/config/resolver/UserId.java new file mode 100644 index 0000000..9938d25 --- /dev/null +++ b/fourthSeminar/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 { +} diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/config/resolver/UserIdResolver.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/config/resolver/UserIdResolver.java new file mode 100644 index 0000000..cbd57ee --- /dev/null +++ b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/config/resolver/UserIdResolver.java @@ -0,0 +1,44 @@ +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").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())); + } + } +} diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/config/swagger/SwaggerConfig.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/config/swagger/SwaggerConfig.java new file mode 100644 index 0000000..05bc5ea --- /dev/null +++ b/fourthSeminar/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/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/BoardController.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/BoardController.java new file mode 100644 index 0000000..06b37fe --- /dev/null +++ b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/BoardController.java @@ -0,0 +1,39 @@ +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.jwt.JwtService; +import sopt.org.fourthSeminar.config.resolver.UserId; +import sopt.org.fourthSeminar.controller.dto.request.BoardImageListRequestDto; +import sopt.org.fourthSeminar.controller.dto.request.BoardRequestDto; +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 BoardService boardService; + private final JwtService jwtService; + private final S3Service s3Service; + + @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/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/UserController.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/UserController.java new file mode 100644 index 0000000..4d077be --- /dev/null +++ b/fourthSeminar/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/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/dto/request/BoardImageListRequestDto.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/dto/request/BoardImageListRequestDto.java new file mode 100644 index 0000000..989ee7a --- /dev/null +++ b/fourthSeminar/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/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/dto/request/BoardRequestDto.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/dto/request/BoardRequestDto.java new file mode 100644 index 0000000..7034d6b --- /dev/null +++ b/fourthSeminar/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/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/dto/request/UserLoginRequestDto.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/dto/request/UserLoginRequestDto.java new file mode 100644 index 0000000..f019bce --- /dev/null +++ b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/dto/request/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; +} diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/dto/request/UserRequestDto.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/dto/request/UserRequestDto.java new file mode 100644 index 0000000..2ddb276 --- /dev/null +++ b/fourthSeminar/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; +} diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/dto/response/UserLoginResponseDto.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/dto/response/UserLoginResponseDto.java new file mode 100644 index 0000000..28f84c8 --- /dev/null +++ b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/dto/response/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); + } +} diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/dto/response/UserResponseDto.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/controller/dto/response/UserResponseDto.java new file mode 100644 index 0000000..c0ea83d --- /dev/null +++ b/fourthSeminar/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/fourthSeminar/src/main/java/sopt/org/fourthSeminar/domain/AuditingTimeEntity.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/domain/AuditingTimeEntity.java new file mode 100644 index 0000000..1c38228 --- /dev/null +++ b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/domain/AuditingTimeEntity.java @@ -0,0 +1,23 @@ +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; + +//상속받아서 쓰는 거고, 따로 이것만 객체생성할 필요가 없기 때문에 abstract +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class AuditingTimeEntity { + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; +} diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/domain/Board.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/domain/Board.java new file mode 100644 index 0000000..8088312 --- /dev/null +++ b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/domain/Board.java @@ -0,0 +1,45 @@ +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.thumbnail = thumbnail; + 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/fourthSeminar/src/main/java/sopt/org/fourthSeminar/domain/Image.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/domain/Image.java new file mode 100644 index 0000000..a2effa5 --- /dev/null +++ b/fourthSeminar/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/fourthSeminar/src/main/java/sopt/org/fourthSeminar/domain/User.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/domain/User.java new file mode 100644 index 0000000..9a1bfb4 --- /dev/null +++ b/fourthSeminar/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/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/Error.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/Error.java new file mode 100644 index 0000000..ef60a12 --- /dev/null +++ b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/Error.java @@ -0,0 +1,49 @@ +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, "잘못된 요청입니다"), + + /** + * 404 NOT FOUND + */ + NOT_FOUND_USER_EXCEPTION(HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다"), + NOT_FOUND_SAVE_IMAGE_EXCEPTION(HttpStatus.NOT_FOUND, "존재하지 않는 이미지입니다."), + NOT_FOUND_IMAGE_EXCEPTION(HttpStatus.NOT_FOUND, "이미지 어쩌구"), + INVALID_MULTIPART_EXTENSION_EXCEPTION(HttpStatus.MULTI_STATUS, "이미지 어쩌구"), + + /** + * 409 CONFLICT + */ + ALREADY_EXIST_USER_EXCEPTION(HttpStatus.CONFLICT, "이미 존재하는 유저입니다"), + + + /** + * 500 INTERNAL SERVER ERROR + */ + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 서버 에러가 발생했습니다"), + + /** + * + */ + TOKEN_TIME_EXPIRED_EXCEPTION(HttpStatus.UNAUTHORIZED, "권한 없어요"), + INVALID_PASSWORD_EXCEPTION(HttpStatus.UNAUTHORIZED, "권한 없어요") + ; + + private final HttpStatus httpStatus; + private final String message; + + public int getHttpStatusCode() { + return httpStatus.value(); + } +} diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/Success.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/Success.java new file mode 100644 index 0000000..bf6b366 --- /dev/null +++ b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/Success.java @@ -0,0 +1,27 @@ +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 { + + /** + * 201 CREATED + */ + SIGNUP_SUCCESS(HttpStatus.CREATED, "회원가입이 완료됐습니다."), + CREATE_BOARD_SUCCESS(HttpStatus.CREATED, "게시물 생성이 완료됐습니다."), + + LOGIN_SUCCESS(HttpStatus.OK, "로그인 성공"), + ; + + private final HttpStatus httpStatus; + private final String message; + + public int getHttpStatusCode() { + return httpStatus.value(); + } +} diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/model/BadRequestException.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/model/BadRequestException.java new file mode 100644 index 0000000..ffdc94e --- /dev/null +++ b/fourthSeminar/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); + } +} diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/model/ConflictException.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/model/ConflictException.java new file mode 100644 index 0000000..d4fe6f7 --- /dev/null +++ b/fourthSeminar/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); + } +} \ No newline at end of file diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/model/NotFoundException.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/model/NotFoundException.java new file mode 100644 index 0000000..ee123ba --- /dev/null +++ b/fourthSeminar/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); + } +} \ No newline at end of file diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/model/SoptException.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/model/SoptException.java new file mode 100644 index 0000000..b685566 --- /dev/null +++ b/fourthSeminar/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/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/model/UnauthorizedException.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/model/UnauthorizedException.java new file mode 100644 index 0000000..c27cab4 --- /dev/null +++ b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/exception/model/UnauthorizedException.java @@ -0,0 +1,9 @@ +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); + } +} diff --git a/fourthSeminar/src/main/java/sopt/org/fourthSeminar/external/client/aws/S3Service.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/external/client/aws/S3Service.java new file mode 100644 index 0000000..e625f5b --- /dev/null +++ b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/external/client/aws/S3Service.java @@ -0,0 +1,95 @@ +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; +import java.util.stream.Collectors; + +@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) { + return multipartFileList.stream().map(multipartFile -> uploadImage(multipartFile, folder)).collect(Collectors.toList()); + } + + // 파일명 (중복 방지) + 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/fourthSeminar/src/main/java/sopt/org/fourthSeminar/infrastructure/BoardRepository.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/infrastructure/BoardRepository.java new file mode 100644 index 0000000..383190f --- /dev/null +++ b/fourthSeminar/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/fourthSeminar/src/main/java/sopt/org/fourthSeminar/infrastructure/ImageRepository.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/infrastructure/ImageRepository.java new file mode 100644 index 0000000..b3fc1fa --- /dev/null +++ b/fourthSeminar/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/fourthSeminar/src/main/java/sopt/org/fourthSeminar/infrastructure/UserRepository.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/infrastructure/UserRepository.java new file mode 100644 index 0000000..1116270 --- /dev/null +++ b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/infrastructure/UserRepository.java @@ -0,0 +1,21 @@ +package sopt.org.fourthSeminar.infrastructure; + +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/fourthSeminar/src/main/java/sopt/org/fourthSeminar/service/BoardService.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/service/BoardService.java new file mode 100644 index 0000000..036556c --- /dev/null +++ b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/service/BoardService.java @@ -0,0 +1,63 @@ +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, String boardThumbnailImageUrl, BoardRequestDto 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, +// boardThumbnailImageUrl, +// request.getTitle(), +// request.getContent(), +// request.getIsPublic() +// ); +// +// boardRepository.save(newBoard); +// } + + @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/fourthSeminar/src/main/java/sopt/org/fourthSeminar/service/UserService.java b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/service/UserService.java new file mode 100644 index 0000000..c1a124b --- /dev/null +++ b/fourthSeminar/src/main/java/sopt/org/fourthSeminar/service/UserService.java @@ -0,0 +1,51 @@ +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.BadRequestException; +import sopt.org.fourthSeminar.exception.model.ConflictException; +import sopt.org.fourthSeminar.exception.model.NotFoundException; +import sopt.org.fourthSeminar.infrastructure.UserRepository; + +@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(); + } + +} \ No newline at end of file diff --git a/fourthSeminar/src/test/java/sopt/org/fourthSeminar/FourthSeminarApplicationTests.java b/fourthSeminar/src/test/java/sopt/org/fourthSeminar/FourthSeminarApplicationTests.java new file mode 100644 index 0000000..81ef130 --- /dev/null +++ b/fourthSeminar/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() { + } + +}