diff --git a/.gitignore b/.gitignore index 76c178677..e437bd997 100644 --- a/.gitignore +++ b/.gitignore @@ -20,13 +20,17 @@ bin/ upload/ ### IntelliJ IDEA ### +*.env .idea *.iws *.iml *.ipr out/ +.logs/ !**/src/main/**/out/ !**/src/test/**/out/ +*.yaml +.discodeit/ ### NetBeans ### /nbproject/private/ @@ -36,4 +40,4 @@ out/ /.nb-gradle/ ### VS Code ### -.vscode/ +.vscode/ \ No newline at end of file diff --git a/admin/.gitattributes b/admin/.gitattributes new file mode 100644 index 000000000..8af972cde --- /dev/null +++ b/admin/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/admin/.gitignore b/admin/.gitignore new file mode 100644 index 000000000..c2065bc26 --- /dev/null +++ b/admin/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/admin/build.gradle b/admin/build.gradle new file mode 100644 index 000000000..c6b7f29aa --- /dev/null +++ b/admin/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.5.3' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.sprint.mission' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +repositories { + mavenCentral() +} + +ext { + set('springBootAdminVersion', "3.5.0") +} + +dependencies { + implementation 'de.codecentric:spring-boot-admin-starter-server' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +dependencyManagement { + imports { + mavenBom "de.codecentric:spring-boot-admin-dependencies:${springBootAdminVersion}" + } +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/admin/gradle/wrapper/gradle-wrapper.jar b/admin/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..1b33c55ba Binary files /dev/null and b/admin/gradle/wrapper/gradle-wrapper.jar differ diff --git a/admin/gradle/wrapper/gradle-wrapper.properties b/admin/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..ff23a68d7 --- /dev/null +++ b/admin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/admin/gradlew b/admin/gradlew new file mode 100755 index 000000000..23d15a936 --- /dev/null +++ b/admin/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/admin/gradlew.bat b/admin/gradlew.bat new file mode 100644 index 000000000..db3a6ac20 --- /dev/null +++ b/admin/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/admin/settings.gradle b/admin/settings.gradle new file mode 100644 index 000000000..e83a19cde --- /dev/null +++ b/admin/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'admin' diff --git a/admin/src/main/java/com/sprint/mission/admin/AdminApplication.java b/admin/src/main/java/com/sprint/mission/admin/AdminApplication.java new file mode 100644 index 000000000..9245ccc8a --- /dev/null +++ b/admin/src/main/java/com/sprint/mission/admin/AdminApplication.java @@ -0,0 +1,15 @@ +package com.sprint.mission.admin; + +import de.codecentric.boot.admin.server.config.EnableAdminServer; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@EnableAdminServer +public class AdminApplication { + + public static void main(String[] args) { + SpringApplication.run(AdminApplication.class, args); + } + +} diff --git a/admin/src/main/resources/application.yaml b/admin/src/main/resources/application.yaml new file mode 100644 index 000000000..02abe4d0a --- /dev/null +++ b/admin/src/main/resources/application.yaml @@ -0,0 +1,5 @@ +spring: + application: + name: admin +server: + port: 9090 \ No newline at end of file diff --git a/admin/src/test/java/com/sprint/mission/admin/AdminApplicationTests.java b/admin/src/test/java/com/sprint/mission/admin/AdminApplicationTests.java new file mode 100644 index 000000000..490b85e74 --- /dev/null +++ b/admin/src/test/java/com/sprint/mission/admin/AdminApplicationTests.java @@ -0,0 +1,13 @@ +package com.sprint.mission.admin; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AdminApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/build.gradle b/build.gradle index bf2f694c5..9ff8b0d37 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.4.4' id 'io.spring.dependency-management' version '1.1.7' + id 'jacoco' } group = 'com.sprint.mission' @@ -13,6 +14,19 @@ java { } } +test { + finalizedBy jacocoTestReport + jvmArgs "-Duser.timezone=UTC" +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + html.required = true + } +} + configurations { compileOnly { extendsFrom annotationProcessor @@ -40,8 +54,14 @@ dependencies { // https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'io.micrometer:micrometer-registry-prometheus' + implementation 'org.springframework.boot:spring-boot-starter-validation' + testImplementation 'com.h2database:h2' + implementation 'de.codecentric:spring-boot-admin-starter-client:3.4.5' } tasks.named('test') { useJUnitPlatform() + finalizedBy jacocoTestReport } diff --git a/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java b/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java index 10e44ccd7..2ac54d9f3 100644 --- a/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java +++ b/src/main/java/com/sprint/mission/discodeit/DiscodeitApplication.java @@ -2,13 +2,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication -@EnableJpaAuditing public class DiscodeitApplication { - public static void main(String[] args) { - SpringApplication.run(DiscodeitApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(DiscodeitApplication.class, args); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/aop/LoggingAspect.java b/src/main/java/com/sprint/mission/discodeit/aop/LoggingAspect.java index 1466b0b76..fc6a95a44 100644 --- a/src/main/java/com/sprint/mission/discodeit/aop/LoggingAspect.java +++ b/src/main/java/com/sprint/mission/discodeit/aop/LoggingAspect.java @@ -1,8 +1,10 @@ package com.sprint.mission.discodeit.aop; import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.JoinPoint; -import org.aspectj.lang.annotation.*; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import java.util.Arrays; @@ -12,38 +14,54 @@ @Slf4j public class LoggingAspect { -// @Pointcut("execution(* com.sprint.mission.discodeit.controller..*(..))") -// public void controllerMethods() {} - - @Pointcut("execution(* com.sprint.mission.discodeit.service..*(..))") - public void serviceMethods() {} + @Pointcut("execution(* com.sprint.mission.discodeit.service..*Service.*(..))") + public void businessServicePointcut() { + } - @Pointcut("execution(* com.sprint.mission.discodeit.repository..*(..))") - public void repositoryMethods() {} + // 페이지네이션 메서드 전용 포인트컷 + @Pointcut("execution(* com.sprint.mission.discodeit.service.basic.BasicMessageService.findAllByChannelId(..))") + public void paginationServicePointcut() { + } - @Before("serviceMethods()") - public void logBefore(JoinPoint joinPoint) { - String method = joinPoint.getSignature().toShortString(); + @Around("businessServicePointcut()") + public Object logBusinessEvent(ProceedingJoinPoint joinPoint) throws Throwable { + String className = joinPoint.getTarget().getClass().getSimpleName(); + String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); - log.info(">> Start: {}", method); - log.info(">> Args: {}", Arrays.toString(args)); - } - @AfterReturning(pointcut = "serviceMethods()", returning = "result") - public void logAfter(JoinPoint joinPoint, Object result) { - String method = joinPoint.getSignature().toShortString(); - log.info("V End: {}", method); - log.info("V Returned: {}", result); + log.info("[{}] 비즈니스 이벤트 시작 - 작업: {}, 매개변수: {}", className, methodName, Arrays.toString(args)); + + try { + Object result = joinPoint.proceed(); + log.info("[{}] 비즈니스 이벤트 성공 - 작업: {}, 결과: {}", className, methodName, result); + return result; + } catch (Throwable throwable) { + log.error("[{}] 비즈니스 이벤트 실패 - 작업: {}, 예외: {}, 메시지: {}", + className, methodName, throwable.getClass().getSimpleName(), throwable.getMessage()); + throw throwable; + } } - @AfterThrowing(pointcut = "serviceMethods()", throwing = "ex") - public void logException(JoinPoint joinPoint, Throwable ex) { - String method = joinPoint.getSignature().toShortString(); - log.error("* Exception in {}: {}", method, ex.getMessage(), ex); + @Around("paginationServicePointcut()") + public Object logPaginationEvent(ProceedingJoinPoint joinPoint) throws Throwable { + Object[] args = joinPoint.getArgs(); + String channelId = args.length > 0 ? String.valueOf(args[0]) : "N/A"; + String cursor = args.length > 1 ? String.valueOf(args[1]) : "N/A"; + String pageSize = (args.length > 2 && args[2] instanceof org.springframework.data.domain.Pageable) + ? String.valueOf(((org.springframework.data.domain.Pageable) args[2]).getPageSize()) + : "N/A"; + + log.info("[Pagination] 채널ID={}, 커서={}, 페이지사이즈={}", channelId, cursor, pageSize); + + try { + Object result = joinPoint.proceed(); + log.info("[Pagination] 메시지 조회 성공"); + return result; + } catch (Throwable throwable) { + log.error("[Pagination] 메시지 조회 실패 - 예외: {}, 메시지: {}", + throwable.getClass().getSimpleName(), throwable.getMessage()); + throw throwable; + } } -// @Before("repositoryMethods()") -// public void logRepository(JoinPoint joinPoint) { -// System.out.println(joinPoint.getSignature().getName()); -// } } diff --git a/src/main/java/com/sprint/mission/discodeit/config/JpaAuditingConfig.java b/src/main/java/com/sprint/mission/discodeit/config/JpaAuditingConfig.java new file mode 100644 index 000000000..658dcf244 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/config/JpaAuditingConfig.java @@ -0,0 +1,11 @@ +package com.sprint.mission.discodeit.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@Profile("!test") // 테스트 프로필에서는 이 Bean이 생성되지 않음 +@EnableJpaAuditing +public class JpaAuditingConfig { +} diff --git a/src/main/java/com/sprint/mission/discodeit/config/MDCLoggingInterceptor.java b/src/main/java/com/sprint/mission/discodeit/config/MDCLoggingInterceptor.java new file mode 100644 index 000000000..4d4a782e2 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/config/MDCLoggingInterceptor.java @@ -0,0 +1,35 @@ +package com.sprint.mission.discodeit.config; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.UUID; + + +@Component +public class MDCLoggingInterceptor implements Filter { + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + try { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + String requestId = UUID.randomUUID().toString(); + + MDC.put("requestId", requestId); + MDC.put("requestMethod", request.getMethod()); + MDC.put("requestURI", request.getRequestURI()); + + response.setHeader("Discodeit-Request_ID", requestId); + + filterChain.doFilter(servletRequest, servletResponse); + } finally { + MDC.clear(); + } + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/config/WebMvcConfig.java b/src/main/java/com/sprint/mission/discodeit/config/WebMvcConfig.java new file mode 100644 index 000000000..a5db5893d --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/config/WebMvcConfig.java @@ -0,0 +1,19 @@ +package com.sprint.mission.discodeit.config; + +import jakarta.servlet.Filter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class WebMvcConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new MDCLoggingInterceptor()); + registrationBean.addUrlPatterns("/*"); + registrationBean.setOrder(1); + return registrationBean; + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/controller/AuthController.java b/src/main/java/com/sprint/mission/discodeit/controller/AuthController.java index 43af1c85b..40a621203 100644 --- a/src/main/java/com/sprint/mission/discodeit/controller/AuthController.java +++ b/src/main/java/com/sprint/mission/discodeit/controller/AuthController.java @@ -1,8 +1,5 @@ package com.sprint.mission.discodeit.controller; -import com.sprint.mission.discodeit.dto.CodeMessageResponseDto; -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; import com.sprint.mission.discodeit.dto.data.UserDto; import com.sprint.mission.discodeit.dto.request.LoginRequest; import com.sprint.mission.discodeit.entity.User; @@ -14,6 +11,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -22,8 +20,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.NoSuchElementException; - @Tag( name = "Auth", description = "Auth API" @@ -33,43 +29,43 @@ @RequestMapping("/api/auth") public class AuthController { - private final AuthService authService; + private final AuthService authService; - @Operation( - summary = "로그인", - operationId = "login" - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "로그인 성공", - content = @Content( - mediaType = "*/*", - schema = @Schema(implementation = User.class) - ) - ), - @ApiResponse( - responseCode = "404", - description = "사용자를 찾을 수 없음", - content = @Content( - mediaType = "*/*", - examples = @ExampleObject(value = "User with username {username} not found") - ) - ), - @ApiResponse( - responseCode = "400", - description = "비밀번호가 일치하지 않음", - content = @Content( - mediaType = "*/*", - examples = @ExampleObject(value = "Wrong password") - ) - ) - }) - @PostMapping("/login") - public ResponseEntity login(@RequestBody LoginRequest loginRequest) { - UserDto user = authService.login(loginRequest); - return ResponseEntity - .status(HttpStatus.OK) - .body(user); - } + @Operation( + summary = "로그인", + operationId = "login" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "로그인 성공", + content = @Content( + mediaType = "*/*", + schema = @Schema(implementation = User.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "사용자를 찾을 수 없음", + content = @Content( + mediaType = "*/*", + examples = @ExampleObject(value = "User with username {username} not found") + ) + ), + @ApiResponse( + responseCode = "400", + description = "비밀번호가 일치하지 않음", + content = @Content( + mediaType = "*/*", + examples = @ExampleObject(value = "Wrong password") + ) + ) + }) + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest loginRequest) { + UserDto user = authService.login(loginRequest); + return ResponseEntity + .status(HttpStatus.OK) + .body(user); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/controller/BinaryContentController.java b/src/main/java/com/sprint/mission/discodeit/controller/BinaryContentController.java index 9fc01da9a..c340e034f 100644 --- a/src/main/java/com/sprint/mission/discodeit/controller/BinaryContentController.java +++ b/src/main/java/com/sprint/mission/discodeit/controller/BinaryContentController.java @@ -1,8 +1,5 @@ package com.sprint.mission.discodeit.controller; -import com.sprint.mission.discodeit.dto.CodeMessageResponseDto; -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; import com.sprint.mission.discodeit.dto.data.BinaryContentDto; import com.sprint.mission.discodeit.entity.BinaryContent; import com.sprint.mission.discodeit.service.BinaryContentService; @@ -23,7 +20,6 @@ import org.springframework.web.bind.annotation.*; import java.util.List; -import java.util.NoSuchElementException; import java.util.UUID; @Tag( @@ -35,82 +31,82 @@ @RequiredArgsConstructor public class BinaryContentController { - private final BinaryContentService binaryContentService; - private final BinaryContentStorage binaryContentStorage; + private final BinaryContentService binaryContentService; + private final BinaryContentStorage binaryContentStorage; - @Operation( - summary = "첨부 파일 조회", - operationId = "find" - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "첨부 파일 조회 성공", - content = @Content( - mediaType = "*/*", - schema = @Schema(implementation = BinaryContent.class) - ) - ), - @ApiResponse( - responseCode = "404", - description = "첨부 파일을 찾을 수 없음", - content = @Content( - mediaType = "*/*", - examples = @ExampleObject(value = "BinaryContent with id {binaryContentId} not found") - ) - ) - }) - @Parameter( - name = "binaryContentId", - description = "조회할 첨부 파일 ID", - required = true, - in = ParameterIn.PATH, - schema = @Schema(type = "string", format = "uuid") - ) - @GetMapping("/{binaryContentId}") - public ResponseEntity find(@PathVariable UUID binaryContentId) { - BinaryContentDto binaryContent = binaryContentService.find(binaryContentId); - return ResponseEntity - .status(HttpStatus.OK) - .body(binaryContent); - } + @Operation( + summary = "첨부 파일 조회", + operationId = "find" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "첨부 파일 조회 성공", + content = @Content( + mediaType = "*/*", + schema = @Schema(implementation = BinaryContent.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "첨부 파일을 찾을 수 없음", + content = @Content( + mediaType = "*/*", + examples = @ExampleObject(value = "BinaryContent with id {binaryContentId} not found") + ) + ) + }) + @Parameter( + name = "binaryContentId", + description = "조회할 첨부 파일 ID", + required = true, + in = ParameterIn.PATH, + schema = @Schema(type = "string", format = "uuid") + ) + @GetMapping("/{binaryContentId}") + public ResponseEntity find(@PathVariable UUID binaryContentId) { + BinaryContentDto binaryContent = binaryContentService.find(binaryContentId); + return ResponseEntity + .status(HttpStatus.OK) + .body(binaryContent); + } - @Operation( - summary = "여러 첨부 파일 조회", - operationId = "findAllByIdIn" - ) - @ApiResponse( - responseCode = "200", - description = "첨부 파일 목록 조회 성공", - content = @Content( - mediaType = "*/*", - array = @ArraySchema( - schema = @Schema(implementation = BinaryContent.class) - ) - ) - ) - @Parameter( - name = "binaryContentId", - description = "조회할 첨부 파일 ID 목록", - required = true, - in = ParameterIn.QUERY, - array = @ArraySchema(schema = @Schema(type = "string", format = "uuid")) - ) - @GetMapping("") - public ResponseEntity> findAllByIdIn( - @RequestParam("binaryContentIds") List binaryContentIds) { - List binaryContents = binaryContentService.findAllByIdIn(binaryContentIds); - return ResponseEntity - .status(HttpStatus.OK) - .body(binaryContents); + @Operation( + summary = "여러 첨부 파일 조회", + operationId = "findAllByIdIn" + ) + @ApiResponse( + responseCode = "200", + description = "첨부 파일 목록 조회 성공", + content = @Content( + mediaType = "*/*", + array = @ArraySchema( + schema = @Schema(implementation = BinaryContent.class) + ) + ) + ) + @Parameter( + name = "binaryContentId", + description = "조회할 첨부 파일 ID 목록", + required = true, + in = ParameterIn.QUERY, + array = @ArraySchema(schema = @Schema(type = "string", format = "uuid")) + ) + @GetMapping("") + public ResponseEntity> findAllByIdIn( + @RequestParam("binaryContentIds") List binaryContentIds) { + List binaryContents = binaryContentService.findAllByIdIn(binaryContentIds); + return ResponseEntity + .status(HttpStatus.OK) + .body(binaryContents); - } + } - @GetMapping("/{binaryContentId}/download") - public ResponseEntity fileDownload(@PathVariable UUID binaryContentId) { - BinaryContentDto binaryContentDto = binaryContentService.find(binaryContentId); - ResponseEntity response = binaryContentStorage.download(binaryContentDto); + @GetMapping("/{binaryContentId}/download") + public ResponseEntity fileDownload(@PathVariable UUID binaryContentId) { + BinaryContentDto binaryContentDto = binaryContentService.find(binaryContentId); + ResponseEntity response = binaryContentStorage.download(binaryContentDto); - return response; - } + return response; + } } diff --git a/src/main/java/com/sprint/mission/discodeit/controller/ChannelController.java b/src/main/java/com/sprint/mission/discodeit/controller/ChannelController.java index 97e5701e3..e9e23d499 100644 --- a/src/main/java/com/sprint/mission/discodeit/controller/ChannelController.java +++ b/src/main/java/com/sprint/mission/discodeit/controller/ChannelController.java @@ -1,8 +1,5 @@ package com.sprint.mission.discodeit.controller; -import com.sprint.mission.discodeit.dto.CodeMessageResponseDto; -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; import com.sprint.mission.discodeit.dto.data.ChannelDto; import com.sprint.mission.discodeit.dto.request.PrivateChannelCreateRequest; import com.sprint.mission.discodeit.dto.request.PublicChannelCreateRequest; @@ -19,13 +16,13 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; -import java.util.NoSuchElementException; import java.util.UUID; @Tag( @@ -37,162 +34,162 @@ @RequiredArgsConstructor public class ChannelController { - private final ChannelService channelService; + private final ChannelService channelService; - @Operation( - summary = "Public Channel 생성", - operationId = "create_3" - ) - @ApiResponse( - responseCode = "201", - description = "Public Channel이 성공적으로 생성됨", - content = @Content( - mediaType = "*/*", - schema = @Schema(implementation = Channel.class) - ) - ) - @PostMapping("/public") - public ResponseEntity create(@RequestBody PublicChannelCreateRequest request) { + @Operation( + summary = "Public Channel 생성", + operationId = "create_3" + ) + @ApiResponse( + responseCode = "201", + description = "Public Channel이 성공적으로 생성됨", + content = @Content( + mediaType = "*/*", + schema = @Schema(implementation = Channel.class) + ) + ) + @PostMapping("/public") + public ResponseEntity create(@Valid @RequestBody PublicChannelCreateRequest request) { - ChannelDto createdChannel = channelService.create(request); - return ResponseEntity - .status(HttpStatus.CREATED) - .body(createdChannel); + ChannelDto createdChannel = channelService.create(request); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(createdChannel); - } + } - @Operation( - summary = "Private Channel 생성", - operationId = "create_4" - ) - @ApiResponse( - responseCode = "201", - description = "Private Channel이 성공적으로 생성됨", - content = @Content( - mediaType = "*/*", - schema = @Schema(implementation = Channel.class) - ) - ) - @PostMapping("/private") - public ResponseEntity create(@RequestBody PrivateChannelCreateRequest request) { - ChannelDto createdChannel = channelService.create(request); - return ResponseEntity - .status(HttpStatus.CREATED) - .body(createdChannel); - } + @Operation( + summary = "Private Channel 생성", + operationId = "create_4" + ) + @ApiResponse( + responseCode = "201", + description = "Private Channel이 성공적으로 생성됨", + content = @Content( + mediaType = "*/*", + schema = @Schema(implementation = Channel.class) + ) + ) + @PostMapping("/private") + public ResponseEntity create(@RequestBody PrivateChannelCreateRequest request) { + ChannelDto createdChannel = channelService.create(request); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(createdChannel); + } - @Operation( - summary = "Public Channel 수정", - description = "Public Channel의 정보를 수정합니다", - tags = {"Channel"} - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "Channel 수정 성공", - content = @Content( - mediaType = "*/*", - examples = @ExampleObject(value = "Success") - ) - ), - @ApiResponse( - responseCode = "400", - description = "Channel 수정 실패", - content = @Content( - mediaType = "*/*", - examples = @ExampleObject(value = "Fail") - ) - ), - @ApiResponse( - responseCode = "404", - description = "Channel을 찾을 수 없거나 관리자가 아님", - content = @Content( - mediaType = "*/*", - examples = @ExampleObject(value = "Channel not found or not an admin") - ) - ) - }) - @Parameter( - required = true, - content = @Content( - schema = @Schema(implementation = PublicChannelUpdateRequest.class) - ) - ) - @Parameter( - name = "channelId", - description = "수정할 채널 ID", - required = true, - schema = @Schema(type = "string", format = "uuid") - ) - @PutMapping("/{channelId}") - public ResponseEntity update(@PathVariable("channelId") UUID channelId, - @RequestBody PublicChannelUpdateRequest request) { + @Operation( + summary = "Public Channel 수정", + description = "Public Channel의 정보를 수정합니다", + tags = {"Channel"} + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Channel 수정 성공", + content = @Content( + mediaType = "*/*", + examples = @ExampleObject(value = "Success") + ) + ), + @ApiResponse( + responseCode = "400", + description = "Channel 수정 실패", + content = @Content( + mediaType = "*/*", + examples = @ExampleObject(value = "Fail") + ) + ), + @ApiResponse( + responseCode = "404", + description = "Channel을 찾을 수 없거나 관리자가 아님", + content = @Content( + mediaType = "*/*", + examples = @ExampleObject(value = "Channel not found or not an admin") + ) + ) + }) + @Parameter( + required = true, + content = @Content( + schema = @Schema(implementation = PublicChannelUpdateRequest.class) + ) + ) + @Parameter( + name = "channelId", + description = "수정할 채널 ID", + required = true, + schema = @Schema(type = "string", format = "uuid") + ) + @PutMapping("/{channelId}") + public ResponseEntity update(@PathVariable("channelId") UUID channelId, + @RequestBody PublicChannelUpdateRequest request) { - ChannelDto updatedChannel = channelService.update(channelId, request); - return ResponseEntity - .status(HttpStatus.OK) - .body(updatedChannel); + ChannelDto updatedChannel = channelService.update(channelId, request); + return ResponseEntity + .status(HttpStatus.OK) + .body(updatedChannel); - } + } - @Operation( - summary = "Channel 삭제", - operationId = "delete_2" - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "204", - description = "Channel이 성공적으로 삭제됨" - ), - @ApiResponse( - responseCode = "404", - description = "Channel을 찾을 수 없음", - content = @Content( - mediaType = "*/*", - examples = @ExampleObject(value = "Channel with id {channelId} not found") - ) - ) - }) - @Parameter( - name = "channelId", - description = "삭제할 Channel ID", - required = true, - schema = @Schema(type = "string", format = "uuid") - ) - @DeleteMapping("/{channelId}") - public ResponseEntity delete(@PathVariable("channelId") UUID channelId) { - channelService.delete(channelId); - return ResponseEntity - .status(HttpStatus.NO_CONTENT) - .body("Channel이 성공적으로 삭제됨"); - } + @Operation( + summary = "Channel 삭제", + operationId = "delete_2" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "204", + description = "Channel이 성공적으로 삭제됨" + ), + @ApiResponse( + responseCode = "404", + description = "Channel을 찾을 수 없음", + content = @Content( + mediaType = "*/*", + examples = @ExampleObject(value = "Channel with id {channelId} not found") + ) + ) + }) + @Parameter( + name = "channelId", + description = "삭제할 Channel ID", + required = true, + schema = @Schema(type = "string", format = "uuid") + ) + @DeleteMapping("/{channelId}") + public ResponseEntity delete(@PathVariable("channelId") UUID channelId) { + channelService.delete(channelId); + return ResponseEntity + .status(HttpStatus.NO_CONTENT) + .body("Channel이 성공적으로 삭제됨"); + } - @Operation( - summary = "User가 참여 중인 Channel 목록 조회", - operationId = "findAll_1" - ) - @ApiResponse( - responseCode = "200", - description = "Channel 목록 조회 성공", - content = @Content( - mediaType = "*/*", - array = @ArraySchema( - schema = @Schema(implementation = ChannelDto.class) - ) - ) - ) - @Parameter( - name = "userId", - description = "조회할 User ID", - required = true, - in = ParameterIn.QUERY, - schema = @Schema(type = "string", format = "uuid") - ) - @GetMapping("") - public ResponseEntity> findAll(@RequestParam("userId") UUID userId) { - List channels = channelService.findAllByUserId(userId); - return ResponseEntity - .status(HttpStatus.OK) - .body(channels); - } + @Operation( + summary = "User가 참여 중인 Channel 목록 조회", + operationId = "findAll_1" + ) + @ApiResponse( + responseCode = "200", + description = "Channel 목록 조회 성공", + content = @Content( + mediaType = "*/*", + array = @ArraySchema( + schema = @Schema(implementation = ChannelDto.class) + ) + ) + ) + @Parameter( + name = "userId", + description = "조회할 User ID", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(type = "string", format = "uuid") + ) + @GetMapping("") + public ResponseEntity> findAll(@RequestParam("userId") UUID userId) { + List channels = channelService.findAllByUserId(userId); + return ResponseEntity + .status(HttpStatus.OK) + .body(channels); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/controller/MainController.java b/src/main/java/com/sprint/mission/discodeit/controller/MainController.java index 2a38434fb..6de2b5dd0 100644 --- a/src/main/java/com/sprint/mission/discodeit/controller/MainController.java +++ b/src/main/java/com/sprint/mission/discodeit/controller/MainController.java @@ -1,16 +1,10 @@ package com.sprint.mission.discodeit.controller; -import com.sprint.mission.discodeit.dto.data.UserDto; -import com.sprint.mission.discodeit.entity.UserStatus; import com.sprint.mission.discodeit.service.UserService; import com.sprint.mission.discodeit.service.UserStatusService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.ResponseBody; - -import java.util.UUID; @Controller @RequiredArgsConstructor diff --git a/src/main/java/com/sprint/mission/discodeit/controller/MessageController.java b/src/main/java/com/sprint/mission/discodeit/controller/MessageController.java index 4db6b5e6b..551e3da9a 100644 --- a/src/main/java/com/sprint/mission/discodeit/controller/MessageController.java +++ b/src/main/java/com/sprint/mission/discodeit/controller/MessageController.java @@ -1,8 +1,5 @@ package com.sprint.mission.discodeit.controller; -import com.sprint.mission.discodeit.dto.CodeMessageResponseDto; -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; import com.sprint.mission.discodeit.dto.data.MessageDto; import com.sprint.mission.discodeit.dto.request.BinaryContentCreateRequest; import com.sprint.mission.discodeit.dto.request.MessageCreateRequest; @@ -21,10 +18,9 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -34,7 +30,10 @@ import java.io.IOException; import java.time.Instant; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; @Tag( name = "Message", @@ -45,190 +44,190 @@ @RequiredArgsConstructor public class MessageController { - private final MessageService messageService; + private final MessageService messageService; - @Operation( - summary = "Message 생성", - operationId = "create_2", - tags = {"Message"} - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "201", - description = "Message가 성공적으로 생성됨", - content = @Content( - mediaType = "*/*", - schema = @Schema(implementation = Message.class) - ) - ), - @ApiResponse( - responseCode = "404", - description = "Channel 또는 User를 찾을 수 없음", - content = @Content( - mediaType = "*/*", - examples = @ExampleObject(value = "Channel | Author with id {channelId | authorId} not found") - ) - ) - }) - @Parameter( - name = "messageCreateRequest", - description = "새로운 메시지 생성 정보를 포함한 객체", - required = true, - schema = @Schema(implementation = MessageCreateRequest.class) - ) - @Parameter( - name = "attachments", - description = "Message 첨부 파일들", - content = @Content( - mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, - array = @ArraySchema( - schema = @Schema(type = "string", format = "binary") - ) - ) - ) - @PostMapping(value = "", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity create( - @RequestPart("messageCreateRequest") MessageCreateRequest messageCreateRequest, - @RequestPart(value = "attachments", required = false) List attachments - ) { - List attachmentRequests = Optional.ofNullable(attachments) - .map(files -> files.stream() - .map(file -> { - try { - return new BinaryContentCreateRequest( - file.getOriginalFilename(), - file.getContentType(), - file.getBytes() - ); - } catch (IOException e) { - throw new RuntimeException(e); - } - }) - .toList()) - .orElse(new ArrayList<>()); - MessageDto createdMessage = messageService.create(messageCreateRequest, attachmentRequests); - return ResponseEntity - .status(HttpStatus.CREATED) - .body(createdMessage); - } + @Operation( + summary = "Message 생성", + operationId = "create_2", + tags = {"Message"} + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "Message가 성공적으로 생성됨", + content = @Content( + mediaType = "*/*", + schema = @Schema(implementation = Message.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "Channel 또는 User를 찾을 수 없음", + content = @Content( + mediaType = "*/*", + examples = @ExampleObject(value = "Channel | Author with id {channelId | authorId} not found") + ) + ) + }) + @Parameter( + name = "messageCreateRequest", + description = "새로운 메시지 생성 정보를 포함한 객체", + required = true, + schema = @Schema(implementation = MessageCreateRequest.class) + ) + @Parameter( + name = "attachments", + description = "Message 첨부 파일들", + content = @Content( + mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, + array = @ArraySchema( + schema = @Schema(type = "string", format = "binary") + ) + ) + ) + @PostMapping(value = "", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity create( + @Valid @RequestPart("messageCreateRequest") MessageCreateRequest messageCreateRequest, + @RequestPart(value = "attachments", required = false) List attachments + ) { + List attachmentRequests = Optional.ofNullable(attachments) + .map(files -> files.stream() + .map(file -> { + try { + return new BinaryContentCreateRequest( + file.getOriginalFilename(), + file.getContentType(), + file.getBytes() + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .toList()) + .orElse(new ArrayList<>()); + MessageDto createdMessage = messageService.create(messageCreateRequest, attachmentRequests); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(createdMessage); + } - @Operation( - summary = "Message 내용 수정", - operationId = "update_2", - tags = {"Message"} - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "Message가 성공적으로 수정됨", - content = @Content( - mediaType = "*/*", - schema = @Schema(implementation = Message.class) - ) - ), - @ApiResponse( - responseCode = "404", - description = "Message를 찾을 수 없음", - content = @Content( - mediaType = "*/*", - examples = @ExampleObject(value = "Message with id {messageId} not found") - ) - ) - }) - @Parameter( - name = "messageId", - in = ParameterIn.PATH, - description = "수정할 Message ID", - required = true, - schema = @Schema(type = "string", format = "uuid") - ) - @Parameter( - name = "messageUpdateRequestDto", - description = "수정할 메시지 정보", - required = true, - schema = @Schema(implementation = Message.class) - ) - @PatchMapping("/{messageId}") - public ResponseEntity update(@PathVariable("messageId") UUID messageId, - @RequestBody MessageUpdateRequest request) { + @Operation( + summary = "Message 내용 수정", + operationId = "update_2", + tags = {"Message"} + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Message가 성공적으로 수정됨", + content = @Content( + mediaType = "*/*", + schema = @Schema(implementation = Message.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "Message를 찾을 수 없음", + content = @Content( + mediaType = "*/*", + examples = @ExampleObject(value = "Message with id {messageId} not found") + ) + ) + }) + @Parameter( + name = "messageId", + in = ParameterIn.PATH, + description = "수정할 Message ID", + required = true, + schema = @Schema(type = "string", format = "uuid") + ) + @Parameter( + name = "messageUpdateRequestDto", + description = "수정할 메시지 정보", + required = true, + schema = @Schema(implementation = Message.class) + ) + @PatchMapping("/{messageId}") + public ResponseEntity update(@PathVariable("messageId") UUID messageId, + @RequestBody MessageUpdateRequest request) { - MessageDto updatedMessage = messageService.update(messageId, request); - return ResponseEntity - .status(HttpStatus.OK) - .body(updatedMessage); - } + MessageDto updatedMessage = messageService.update(messageId, request); + return ResponseEntity + .status(HttpStatus.OK) + .body(updatedMessage); + } - @Operation( - summary = "Message 삭제", - operationId = "delete_1", - tags = {"Message"} - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "204", - description = "Message가 성공적으로 삭제됨", - content = @Content( - mediaType = "*/*", - examples = @ExampleObject(value = "Success") - ) - ), - @ApiResponse( - responseCode = "404", - description = "Message를 찾을 수 없음", - content = @Content( - mediaType = "*/*", - examples = @ExampleObject(value = "Message with id {messageId} not found") - ) - ) - }) - @Parameters({ - @Parameter( - name = "messageId", - description = "삭제할 Message ID", - required = true, - in = ParameterIn.PATH, - schema = @Schema(type = "string", format = "uuid") - ) - }) - @DeleteMapping("/{messageId}") - public ResponseEntity delete(@PathVariable("messageId") UUID messageId) { - messageService.delete(messageId); - return ResponseEntity - .status(HttpStatus.NO_CONTENT) - .body("Message가 성공적으로 삭제됨"); - } + @Operation( + summary = "Message 삭제", + operationId = "delete_1", + tags = {"Message"} + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "204", + description = "Message가 성공적으로 삭제됨", + content = @Content( + mediaType = "*/*", + examples = @ExampleObject(value = "Success") + ) + ), + @ApiResponse( + responseCode = "404", + description = "Message를 찾을 수 없음", + content = @Content( + mediaType = "*/*", + examples = @ExampleObject(value = "Message with id {messageId} not found") + ) + ) + }) + @Parameters({ + @Parameter( + name = "messageId", + description = "삭제할 Message ID", + required = true, + in = ParameterIn.PATH, + schema = @Schema(type = "string", format = "uuid") + ) + }) + @DeleteMapping("/{messageId}") + public ResponseEntity delete(@PathVariable("messageId") UUID messageId) { + messageService.delete(messageId); + return ResponseEntity + .status(HttpStatus.NO_CONTENT) + .body("Message가 성공적으로 삭제됨"); + } - @Operation( - summary = "Channel의 Message 목록 조회", - operationId = "findAllByChannelId", - tags = {"Message"} - ) - @ApiResponse( - responseCode = "200", - description = "Message 목록 조회 성공", - content = @Content( - mediaType = "*/*", - array = @ArraySchema( - schema = @Schema(implementation = Message.class) - ) - ) - ) - @Parameter( - name = "channelId", - description = "조회할 Channel ID", - required = true, - in = ParameterIn.QUERY, - schema = @Schema(type = "string", format = "uuid") - ) - @GetMapping("") - public ResponseEntity> findAllByChannelId( - @RequestParam("channelId") UUID channelId, - @RequestParam(name = "cursor", required = false) Instant cursor, - @PageableDefault Pageable pageable) { + @Operation( + summary = "Channel의 Message 목록 조회", + operationId = "findAllByChannelId", + tags = {"Message"} + ) + @ApiResponse( + responseCode = "200", + description = "Message 목록 조회 성공", + content = @Content( + mediaType = "*/*", + array = @ArraySchema( + schema = @Schema(implementation = Message.class) + ) + ) + ) + @Parameter( + name = "channelId", + description = "조회할 Channel ID", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(type = "string", format = "uuid") + ) + @GetMapping("") + public ResponseEntity> findAllByChannelId( + @RequestParam("channelId") UUID channelId, + @RequestParam(name = "cursor", required = false) Instant cursor, + @PageableDefault Pageable pageable) { - PageResponse messages = messageService.findAllByChannelId(channelId, cursor, pageable); - return ResponseEntity - .status(HttpStatus.OK) - .body(messages); - } + PageResponse messages = messageService.findAllByChannelId(channelId, cursor, pageable); + return ResponseEntity + .status(HttpStatus.OK) + .body(messages); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/controller/ReadStatusController.java b/src/main/java/com/sprint/mission/discodeit/controller/ReadStatusController.java index 582c31ec7..22e2f5bcb 100644 --- a/src/main/java/com/sprint/mission/discodeit/controller/ReadStatusController.java +++ b/src/main/java/com/sprint/mission/discodeit/controller/ReadStatusController.java @@ -1,8 +1,5 @@ package com.sprint.mission.discodeit.controller; -import com.sprint.mission.discodeit.dto.CodeMessageResponseDto; -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; import com.sprint.mission.discodeit.dto.data.ReadStatusDto; import com.sprint.mission.discodeit.dto.request.ReadStatusCreateRequest; import com.sprint.mission.discodeit.dto.request.ReadStatusUpdateRequest; @@ -18,13 +15,13 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; -import java.util.NoSuchElementException; import java.util.UUID; @Tag( @@ -36,126 +33,126 @@ @RequiredArgsConstructor public class ReadStatusController { - private final ReadStatusService readStatusService; + private final ReadStatusService readStatusService; - @Operation( - summary = "Message 읽음 상태 생성", - operationId = "create_1", - tags = {"ReadStatus"} - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "201", - description = "Message 읽음 상태가 성공적으로 생성됨", - content = @Content( - mediaType = "*/*", - schema = @Schema(implementation = ReadStatus.class) - ) - ), - @ApiResponse( - responseCode = "404", - description = "Channel 또는 User를 찾을 수 없음", - content = @Content( - mediaType = "*/*", - examples = @ExampleObject(value = "Channel | User with id {channelId | userId} not found") - ) - ), - @ApiResponse( - responseCode = "400", - description = "이미 읽음 상태가 존재함", - content = @Content( - mediaType = "*/*", - examples = @ExampleObject(value = "ReadStatus with userId {userId} and channelId {channelId} already exists") - ) - ) - }) - @Parameter( - name = "readStatusCreateRequestDto", - description = "생성할 읽음 상태 정보", - required = true, - schema = @Schema(implementation = ReadStatusCreateRequest.class) - ) - @PostMapping("") - public ResponseEntity create(@RequestBody ReadStatusCreateRequest request) { + @Operation( + summary = "Message 읽음 상태 생성", + operationId = "create_1", + tags = {"ReadStatus"} + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "201", + description = "Message 읽음 상태가 성공적으로 생성됨", + content = @Content( + mediaType = "*/*", + schema = @Schema(implementation = ReadStatus.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "Channel 또는 User를 찾을 수 없음", + content = @Content( + mediaType = "*/*", + examples = @ExampleObject(value = "Channel | User with id {channelId | userId} not found") + ) + ), + @ApiResponse( + responseCode = "400", + description = "이미 읽음 상태가 존재함", + content = @Content( + mediaType = "*/*", + examples = @ExampleObject(value = "ReadStatus with userId {userId} and channelId {channelId} already exists") + ) + ) + }) + @Parameter( + name = "readStatusCreateRequestDto", + description = "생성할 읽음 상태 정보", + required = true, + schema = @Schema(implementation = ReadStatusCreateRequest.class) + ) + @PostMapping("") + public ResponseEntity create(@Valid @RequestBody ReadStatusCreateRequest request) { - ReadStatusDto createdReadStatus = readStatusService.create(request); - return ResponseEntity - .status(HttpStatus.CREATED) - .body(createdReadStatus); - } + ReadStatusDto createdReadStatus = readStatusService.create(request); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(createdReadStatus); + } - @Operation( - summary = "Message 읽음 상태 수정", - operationId = "update_1" - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "Message 읽음 상태가 성공적으로 수정됨", - content = @Content( - mediaType = "*/*", - schema = @Schema(implementation = ReadStatus.class) - ) - ), - @ApiResponse( - responseCode = "404", - description = "Message 읽음 상태를 찾을 수 없음", - content = @Content( - mediaType = "*/*", - examples = @ExampleObject(value = "ReadStatus with id {readStatusId} not found") - ) - ) - }) - @Parameter( - name = "readStatusId", - description = "수정할 읽음 상태 ID", - required = true, - in = ParameterIn.PATH, - schema = @Schema(type = "string", format = "uuid") - ) - @Parameter( - name = "readStatusUpdateRequest", - description = "수정할 읽음 상태 정보", - required = true, - schema = @Schema(implementation = ReadStatusUpdateRequest.class) - ) - @PatchMapping("/{readStatusId}") - public ResponseEntity update(@PathVariable("readStatusId") UUID readStatusId, - @RequestBody ReadStatusUpdateRequest request) { + @Operation( + summary = "Message 읽음 상태 수정", + operationId = "update_1" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Message 읽음 상태가 성공적으로 수정됨", + content = @Content( + mediaType = "*/*", + schema = @Schema(implementation = ReadStatus.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "Message 읽음 상태를 찾을 수 없음", + content = @Content( + mediaType = "*/*", + examples = @ExampleObject(value = "ReadStatus with id {readStatusId} not found") + ) + ) + }) + @Parameter( + name = "readStatusId", + description = "수정할 읽음 상태 ID", + required = true, + in = ParameterIn.PATH, + schema = @Schema(type = "string", format = "uuid") + ) + @Parameter( + name = "readStatusUpdateRequest", + description = "수정할 읽음 상태 정보", + required = true, + schema = @Schema(implementation = ReadStatusUpdateRequest.class) + ) + @PatchMapping("/{readStatusId}") + public ResponseEntity update(@PathVariable("readStatusId") UUID readStatusId, + @RequestBody ReadStatusUpdateRequest request) { - ReadStatusDto updatedReadStatus = readStatusService.update(readStatusId, request); - return ResponseEntity - .status(HttpStatus.OK) - .body(updatedReadStatus); - } + ReadStatusDto updatedReadStatus = readStatusService.update(readStatusId, request); + return ResponseEntity + .status(HttpStatus.OK) + .body(updatedReadStatus); + } - @Operation( - summary = "User의 Message 읽음 상태 목록 조회", - operationId = "findAllByUserId" - ) - @ApiResponse( - responseCode = "200", - description = "Message 읽음 상태 목록 조회 성공", - content = @Content( - mediaType = "*/*", - array = @ArraySchema( - schema = @Schema(implementation = ReadStatus.class) - ) - ) - ) - @Parameter( - name = "userId", - description = "조회할 User ID", - required = true, - in = ParameterIn.QUERY, - schema = @Schema(type = "string", format = "uuid") - ) - @GetMapping("") - public ResponseEntity> findAllByUserId(@RequestParam("userId") UUID userId) { + @Operation( + summary = "User의 Message 읽음 상태 목록 조회", + operationId = "findAllByUserId" + ) + @ApiResponse( + responseCode = "200", + description = "Message 읽음 상태 목록 조회 성공", + content = @Content( + mediaType = "*/*", + array = @ArraySchema( + schema = @Schema(implementation = ReadStatus.class) + ) + ) + ) + @Parameter( + name = "userId", + description = "조회할 User ID", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(type = "string", format = "uuid") + ) + @GetMapping("") + public ResponseEntity> findAllByUserId(@RequestParam("userId") UUID userId) { - List readStatuses = readStatusService.findAllByUserId(userId); - return ResponseEntity - .status(HttpStatus.OK) - .body(readStatuses); - } + List readStatuses = readStatusService.findAllByUserId(userId); + return ResponseEntity + .status(HttpStatus.OK) + .body(readStatuses); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/controller/UserController.java b/src/main/java/com/sprint/mission/discodeit/controller/UserController.java index 68fde6ad3..13d54d59b 100644 --- a/src/main/java/com/sprint/mission/discodeit/controller/UserController.java +++ b/src/main/java/com/sprint/mission/discodeit/controller/UserController.java @@ -1,8 +1,6 @@ package com.sprint.mission.discodeit.controller; -import com.sprint.mission.discodeit.dto.CodeMessageResponseDto; -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; +import com.sprint.mission.discodeit.dto.ErrorResponse; import com.sprint.mission.discodeit.dto.data.UserDto; import com.sprint.mission.discodeit.dto.data.UserStatusDto; import com.sprint.mission.discodeit.dto.request.BinaryContentCreateRequest; @@ -23,18 +21,17 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.ErrorResponse; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.List; -import java.util.NoSuchElementException; import java.util.Optional; import java.util.UUID; @@ -48,220 +45,194 @@ @RequiredArgsConstructor public class UserController { - private final UserService userService; - private final UserStatusService userStatusService; - - @Operation( - summary = "User 등록", - operationId = "create" - ) - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "요청이 성공적으로 처리됨", content = @Content(schema = @Schema(implementation = UserCreateRequest.class))), - @ApiResponse(responseCode = "400", description = "같은 email 또는 USERNAME를 사용하는 User가 이미 존재함", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) - }) - @Parameter( - name = "userCreateRequest", - description = "새롭게 등록할 사용자의 정보를 포함한 객체", - schema = @Schema(implementation = UserCreateRequest.class) - ) - @Parameter( - name = "profile", - description = "사용자 프로필 이미지", - content = @Content( - mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, - schema = @Schema(type = "string", format = "binary") - ) - ) - @PostMapping("") - public ResponseEntity create( - @RequestPart("userCreateRequest") UserCreateRequest userCreateRequest, - @RequestPart(value = "profile", required = false) MultipartFile profile - ) { - try { - Optional profileRequest = Optional.ofNullable(profile) - .flatMap(this::resolveProfileRequest); - UserDto createdUser = userService.create(userCreateRequest, profileRequest); - - return ResponseEntity - .status(HttpStatus.CREATED) - .body(createdUser); - - } catch (IllegalArgumentException e) { - return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body(CodeMessageResponseDto.error( - ResponseCode.DUPLICATE_USER, - ResponseMessage.DUPLICATE_USER - )); - - } catch (RuntimeException e) { - return ResponseEntity - .status(HttpStatus.BAD_REQUEST) - .body(CodeMessageResponseDto.error( - ResponseCode.FILE_PROCESSING_ERROR, - ResponseMessage.FILE_PROCESSING_ERROR - )); - - } catch (Exception e) { - return ResponseEntity - .status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(CodeMessageResponseDto.error( - ResponseCode.INTERNAL_ERROR, - ResponseMessage.INTERNAL_SERVER_ERROR - )); + private final UserService userService; + private final UserStatusService userStatusService; + + @Operation( + summary = "User 등록", + operationId = "create" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "요청이 성공적으로 처리됨", content = @Content(schema = @Schema(implementation = UserCreateRequest.class))), + @ApiResponse(responseCode = "400", description = "같은 email 또는 USERNAME를 사용하는 User가 이미 존재함", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @Parameter( + name = "userCreateRequest", + description = "새롭게 등록할 사용자의 정보를 포함한 객체", + schema = @Schema(implementation = UserCreateRequest.class) + ) + @Parameter( + name = "profile", + description = "사용자 프로필 이미지", + content = @Content( + mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, + schema = @Schema(type = "string", format = "binary") + ) + ) + @PostMapping("") + public ResponseEntity create( + @Valid @RequestPart("userCreateRequest") UserCreateRequest userCreateRequest, + @RequestPart(value = "profile", required = false) MultipartFile profile + ) { + Optional profileRequest = Optional.ofNullable(profile) + .flatMap(this::resolveProfileRequest); + UserDto createdUser = userService.create(userCreateRequest, profileRequest); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(createdUser); } - } - - @Operation( - summary = "User 정보 수정", - operationId = "update" - ) - @ApiResponses( - value = { - @ApiResponse(responseCode = "200", description = "User 정보가 성공적으로 수정됨", content = @Content(schema = @Schema(implementation = User.class))), - @ApiResponse( - responseCode = "400", - description = "같은 email 또는 username를 사용하는 User가 이미 존재함", - content = @Content(mediaType = "*/*", - examples = @ExampleObject(value = "user with email {newEmail} already exists")) - ), - @ApiResponse( - responseCode = "404", - description = "User를 찾을 수 없음", - content = @Content(mediaType = "*/*", - examples = @ExampleObject(value = "User with id {userId} not found")) - ) - } - ) - @Parameter( - name = "userUpdateRequestDto", - schema = @Schema(implementation = UserCreateRequest.class) - ) - @Parameter( - name = "profile", - description = "수정할 User 프로필 이미지", - content = @Content( - mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, - schema = @Schema(type = "string", format = "binary") - ) - ) - @PatchMapping("/{userId}") - public ResponseEntity update( - @PathVariable("userId") UUID userId, - @RequestPart("userUpdateRequest") UserUpdateRequest userUpdateRequest, - @RequestPart(value = "profile", required = false) MultipartFile profile - ) { - Optional profileRequest = Optional.ofNullable(profile) - .flatMap(this::resolveProfileRequest); - UserDto updatedUser = userService.update(userId, userUpdateRequest, profileRequest); - return ResponseEntity - .status(HttpStatus.OK) - .body(updatedUser); - } - - @Operation( - summary = "User 삭제", - operationId = "delete" - ) - @ApiResponses( - value = { - @ApiResponse(responseCode = "204", description = "User가 성공적으로 삭제됨"), - @ApiResponse(responseCode = "404", description = "User를 찾을 수 없음", content = @Content(mediaType = "*/*", examples = @ExampleObject(value = "User with id {id} not found"))) - } - ) - @Parameter( - name = "userId", - in = ParameterIn.PATH, - description = "삭제할 User ID", - required = true, - schema = @Schema(type = "string", format = "uuid") - ) - @DeleteMapping("/{userId}") - public ResponseEntity delete(@PathVariable("userId") UUID userId) { - userService.delete(userId); - return ResponseEntity - .status(HttpStatus.NO_CONTENT) - .body("User가 성공적으로 삭제됨"); - } - - @Operation( - summary = "전체 User 목록 조회", - operationId = "findAll" - ) - @ApiResponse( - responseCode = "200", - description = "User 목록 조회 성공", - content = @Content( - mediaType = "*/*", - array = @ArraySchema( - schema = @Schema(implementation = UserDto.class) - ) - ) - ) - @GetMapping("") - public ResponseEntity> findAll() { + @Operation( + summary = "User 정보 수정", + operationId = "update" + ) + @ApiResponses( + value = { + @ApiResponse(responseCode = "200", description = "User 정보가 성공적으로 수정됨", content = @Content(schema = @Schema(implementation = User.class))), + @ApiResponse( + responseCode = "400", + description = "같은 email 또는 username를 사용하는 User가 이미 존재함", + content = @Content(mediaType = "*/*", + examples = @ExampleObject(value = "user with email {newEmail} already exists")) + ), + @ApiResponse( + responseCode = "404", + description = "User를 찾을 수 없음", + content = @Content(mediaType = "*/*", + examples = @ExampleObject(value = "User with id {userId} not found")) + ) + } + ) + @Parameter( + name = "userUpdateRequestDto", + schema = @Schema(implementation = UserCreateRequest.class) + ) + @Parameter( + name = "profile", + description = "수정할 User 프로필 이미지", + content = @Content( + mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, + schema = @Schema(type = "string", format = "binary") + ) + ) + @PatchMapping("/{userId}") + public ResponseEntity update( + @PathVariable("userId") UUID userId, + @RequestPart("userUpdateRequest") UserUpdateRequest userUpdateRequest, + @RequestPart(value = "profile", required = false) MultipartFile profile + ) { + + Optional profileRequest = Optional.ofNullable(profile) + .flatMap(this::resolveProfileRequest); + UserDto updatedUser = userService.update(userId, userUpdateRequest, profileRequest); + return ResponseEntity + .status(HttpStatus.OK) + .body(updatedUser); + } - List users = userService.findAll(); - return ResponseEntity - .status(HttpStatus.OK) - .body(users); - } + @Operation( + summary = "User 삭제", + operationId = "delete" + ) + @ApiResponses( + value = { + @ApiResponse(responseCode = "204", description = "User가 성공적으로 삭제됨"), + @ApiResponse(responseCode = "404", description = "User를 찾을 수 없음", content = @Content(mediaType = "*/*", examples = @ExampleObject(value = "User with id {id} not found"))) + } + ) + @Parameter( + name = "userId", + in = ParameterIn.PATH, + description = "삭제할 User ID", + required = true, + schema = @Schema(type = "string", format = "uuid") + ) + @DeleteMapping("/{userId}") + public ResponseEntity delete(@PathVariable("userId") UUID userId) { + userService.delete(userId); + return ResponseEntity + .status(HttpStatus.NO_CONTENT) + .body("User가 성공적으로 삭제됨"); + } - @Operation( - summary = "User 온라인 상태 업데이트", - operationId = "updateUserStatusByUserId" - ) - @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "User 온라인 상태가 성공적으로 업데이트됨", - content = @Content( - mediaType = "*/*", - schema = @Schema(implementation = UserStatus.class) - ) - ), - @ApiResponse( - responseCode = "404", - description = "해당 User의 UserStatus를 찾을 수 없음", - content = @Content( - mediaType = "*/*", - examples = @ExampleObject(value = "UserStatus with userId {userId} not found") - ) - ) - }) - @Parameter( - name = "userId", - description = "상태를 변경할 User ID", - required = true, - schema = @Schema(type = "string", format = "uuid") - ) - @PatchMapping("/{userId}/userStatus") - public ResponseEntity updateUserStatusByUserId(@PathVariable("userId") UUID userId, - @RequestBody UserStatusUpdateRequest request) { + @Operation( + summary = "전체 User 목록 조회", + operationId = "findAll" + ) + @ApiResponse( + responseCode = "200", + description = "User 목록 조회 성공", + content = @Content( + mediaType = "*/*", + array = @ArraySchema( + schema = @Schema(implementation = UserDto.class) + ) + ) + ) + @GetMapping("") + public ResponseEntity> findAll() { + + List users = userService.findAll(); + return ResponseEntity + .status(HttpStatus.OK) + .body(users); + } - UserStatusDto updatedUserStatus = userStatusService.updateByUserId(userId, request); - return ResponseEntity - .status(HttpStatus.OK) - .body(updatedUserStatus); + @Operation( + summary = "User 온라인 상태 업데이트", + operationId = "updateUserStatusByUserId" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "User 온라인 상태가 성공적으로 업데이트됨", + content = @Content( + mediaType = "*/*", + schema = @Schema(implementation = UserStatus.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "해당 User의 UserStatus를 찾을 수 없음", + content = @Content( + mediaType = "*/*", + examples = @ExampleObject(value = "UserStatus with userId {userId} not found") + ) + ) + }) + @Parameter( + name = "userId", + description = "상태를 변경할 User ID", + required = true, + schema = @Schema(type = "string", format = "uuid") + ) + @PatchMapping("/{userId}/userStatus") + public ResponseEntity updateUserStatusByUserId(@PathVariable("userId") UUID userId, + @RequestBody UserStatusUpdateRequest request) { + + UserStatusDto updatedUserStatus = userStatusService.updateByUserId(userId, request); + return ResponseEntity + .status(HttpStatus.OK) + .body(updatedUserStatus); - } + } - private Optional resolveProfileRequest(MultipartFile profileFile) { - if (profileFile.isEmpty()) { - return Optional.empty(); - } else { - try { - BinaryContentCreateRequest binaryContentCreateRequest = new BinaryContentCreateRequest( - profileFile.getOriginalFilename(), - profileFile.getContentType(), - profileFile.getBytes() - ); - return Optional.of(binaryContentCreateRequest); - } catch (IOException e) { - throw new RuntimeException(e); - } + private Optional resolveProfileRequest(MultipartFile profileFile) { + if (profileFile.isEmpty()) { + return Optional.empty(); + } else { + try { + BinaryContentCreateRequest binaryContentCreateRequest = new BinaryContentCreateRequest( + profileFile.getOriginalFilename(), + profileFile.getContentType(), + profileFile.getBytes() + ); + return Optional.of(binaryContentCreateRequest); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } - } } diff --git a/src/main/java/com/sprint/mission/discodeit/dto/CodeMessageResponseDto.java b/src/main/java/com/sprint/mission/discodeit/dto/CodeMessageResponseDto.java deleted file mode 100644 index b82604644..000000000 --- a/src/main/java/com/sprint/mission/discodeit/dto/CodeMessageResponseDto.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sprint.mission.discodeit.dto; - -import lombok.AllArgsConstructor; -import lombok.Getter; - - -@Getter -@AllArgsConstructor -public class CodeMessageResponseDto { - private String code; - private String message; - private T data; - - public static CodeMessageResponseDto of(String code, String message, T data) { - return new CodeMessageResponseDto<>(code, message, data); - } - - public static CodeMessageResponseDto success(T data) { - return new CodeMessageResponseDto<>(ResponseCode.SUCCESS, ResponseMessage.SUCCESS, data); - } - - public static CodeMessageResponseDto error(String code, String message) { - return new CodeMessageResponseDto<>(code, message, null); - } - -} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/ErrorCode.java b/src/main/java/com/sprint/mission/discodeit/dto/ErrorCode.java new file mode 100644 index 000000000..7347bbe55 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/ErrorCode.java @@ -0,0 +1,45 @@ +package com.sprint.mission.discodeit.dto; + +import org.springframework.http.HttpStatus; + +public enum ErrorCode { + SUCCESS("SUCCESS", HttpStatus.OK, "요청이 성공적으로 처리되었습니다."), + REQUEST_FAIL("REQUEST_FAIL", HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), + USER_NOT_FOUND("USER_NOT_FOUND", HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."), + CHANNEL_NOT_FOUND("CHANNEL_NOT_FOUND", HttpStatus.NOT_FOUND, "채널을 찾을 수 없습니다."), + MESSAGE_NOT_FOUND("MESSAGE_NOT_FOUND", HttpStatus.NOT_FOUND, "메시지를 찾을 수 없습니다."), + READ_STATUS_NOT_FOUND("READ_STATUS_NOT_FOUND", HttpStatus.NOT_FOUND, "읽음 상태를 찾을 수 없습니다."), + BINARY_NOT_FOUND("BINARY_NOT_FOUND", HttpStatus.NOT_FOUND, "바이너리 파일을 찾을 수 없습니다."), + ID_OR_PASSWORD_VALID("ID_OR_PASSWORD_VALID", HttpStatus.UNAUTHORIZED, "아이디 또는 비밀번호가 유효하지 않습니다."), + USER_OR_CHANNEL_NOT_FOUND("USER_OR_CHANNEL_NOT_FOUND", HttpStatus.NOT_FOUND, "사용자 또는 채널이 존재하지 않습니다."), + DUPLICATE_USER("DUPLICATE_USER", HttpStatus.CONFLICT, "이미 존재하는 사용자입니다."), + DUPLICATE_READ_STATUS("DUPLICATE_READ_STATUS", HttpStatus.CONFLICT, "중복된 읽음 상태입니다."), + FILE_PROCESSING_ERROR("FILE_PROCESSING_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "파일 처리 중 오류가 발생했습니다."), + INTERNAL_ERROR("INTERNAL_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "내부 서버 오류가 발생했습니다."), + DUPLICATE_USER_STATUS("DUPLICATE_USER_STATUS", HttpStatus.CONFLICT, "중복된 사용자 상태입니다."), + USER_STATUS_NOT_FOUND("USER_STATUS_NOT_FOUND", HttpStatus.NOT_FOUND, "사용자 상태 정보를 찾을 수 없습니다."), + PRIVATE_CHANNEL_UPDATE_DENIED("PRIVATE_CHANNEL_UPDATE_DENIED", HttpStatus.FORBIDDEN, "PRIVATE 채널은 수정할 수 없습니다."); + + private final String code; + private final HttpStatus status; + private final String message; + + ErrorCode(String code, HttpStatus status, String message) { + this.code = code; + this.status = status; + this.message = message; + } + + public String getCode() { + return code; + } + + public HttpStatus getStatus() { + return status; + } + + public String getMessage() { + return message; + } +} + diff --git a/src/main/java/com/sprint/mission/discodeit/dto/ErrorResponse.java b/src/main/java/com/sprint/mission/discodeit/dto/ErrorResponse.java new file mode 100644 index 000000000..ad8e04b2c --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/dto/ErrorResponse.java @@ -0,0 +1,19 @@ +package com.sprint.mission.discodeit.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.Instant; +import java.util.Map; + + +@Getter +@AllArgsConstructor +public class ErrorResponse { + private String code; + private String message; + private Map details; + private String exceptionType; + private int status; + private Instant timestamp; +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/ResponseCode.java b/src/main/java/com/sprint/mission/discodeit/dto/ResponseCode.java deleted file mode 100644 index 5bbd1b352..000000000 --- a/src/main/java/com/sprint/mission/discodeit/dto/ResponseCode.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.sprint.mission.discodeit.dto; - -public interface ResponseCode { - - String SUCCESS = "Success"; - String REQUEST_FAIL = "Request Fail"; - String USER_NOT_FOUND = "User Not Found"; - String CHANNEL_NOT_FOUND = "Channel Not Found"; - String MESSAGE_NOT_FOUND = "Message Not Found"; - String READ_STATUS_NOT_FOUND = "Read Status Not Found"; - String BINARY_NOT_FOUND = "Binary Not Found"; - String PASSWORD_VALID = "Password Not Valid"; - String USER_OR_CHANNEL_NOT_FOUND = "User Or Channel Not Found"; - String DUPLICATE_USER = "Duplicated User"; - String DUPLICATE_READ_STATUS = "Duplicated Read Status"; - String FILE_PROCESSING_ERROR = "File Processing Error"; - String INTERNAL_ERROR = "Internal Server Error"; - String DUPLICATE_USER_STATUS = "Duplicated User Status"; - String USER_STATUS_NOT_FOUND = "User Status Not Found"; -} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/ResponseMessage.java b/src/main/java/com/sprint/mission/discodeit/dto/ResponseMessage.java index 66754dc7a..a9e0fb9d0 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/ResponseMessage.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/ResponseMessage.java @@ -12,7 +12,7 @@ public interface ResponseMessage { String READ_STATUS_NOT_FOUND = "읽음 상태를 찾을 수 없음"; String USER_OR_CHANNEL_NOT_FOUND = "유저 또는 채널을 찾을 수 없음"; String DUPLICATE_USER = "이미 존재하는 사용자임"; - String PASSWORD_VALID = "비밀번호가 일치하지 않음"; + String ID_OR_PASSWORD_VALID = "아이디 또는 비밀번호가 일치하지 않음"; String DUPLICATE_READ_STATUS = "이미 존재하는 읽음 상태임"; String FILE_PROCESSING_ERROR = "파일 처리 중 오류가 발생했습니다."; String INTERNAL_SERVER_ERROR = "서버 내부 오류가 발생했습니다."; diff --git a/src/main/java/com/sprint/mission/discodeit/dto/data/ChannelDto.java b/src/main/java/com/sprint/mission/discodeit/dto/data/ChannelDto.java index 5251eb220..e638cc4ca 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/data/ChannelDto.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/data/ChannelDto.java @@ -2,9 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.sprint.mission.discodeit.entity.ChannelType; -import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.ToString; import java.time.Instant; import java.util.List; @@ -24,5 +22,6 @@ public record ChannelDto( @JsonFormat(shape = JsonFormat.Shape.STRING) Instant lastMessageAt -) {} +) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/data/UserDto.java b/src/main/java/com/sprint/mission/discodeit/dto/data/UserDto.java index e3af1ad4a..c2ec5a9b7 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/data/UserDto.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/data/UserDto.java @@ -2,7 +2,6 @@ import io.swagger.v3.oas.annotations.media.Schema; -import java.time.Instant; import java.util.UUID; @Schema( @@ -15,5 +14,6 @@ public record UserDto( String email, BinaryContentDto profile, Boolean online -) {} +) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/BinaryContentCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/BinaryContentCreateRequest.java index 773246dd5..055d70500 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/request/BinaryContentCreateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/BinaryContentCreateRequest.java @@ -12,5 +12,6 @@ public record BinaryContentCreateRequest( @Schema(description = "파일 바이트 데이터") byte[] bytes -) {} +) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/LoginRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/LoginRequest.java index 95de70d25..7098feee9 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/request/LoginRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/LoginRequest.java @@ -1,12 +1,16 @@ package com.sprint.mission.discodeit.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; @Schema(description = "로그인 요청 DTO") public record LoginRequest( @Schema(description = "사용자 이름", example = "john_doe") + @NotBlank(message = "아이디는 필수 입니다.") String username, @Schema(description = "비밀번호", example = "password123") + @NotBlank(message = "비밀번호는 필수 입니다.") String password -) {} +) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/MessageCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/MessageCreateRequest.java index 3a6b8ef7e..4b545e0b7 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/request/MessageCreateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/MessageCreateRequest.java @@ -1,18 +1,25 @@ package com.sprint.mission.discodeit.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import java.util.UUID; + @Schema(description = "메시지 생성 요청 DTO") public record MessageCreateRequest( @Schema(description = "메시지 내용", example = "안녕하세요!") + @NotBlank(message = "메시지 내용은 필수입니다.") String content, @Schema(description = "메시지가 속한 채널 ID", type = "string", format = "uuid", example = "123e4567-e89b-12d3-a456-426614174000") + @NotNull(message = "채널 ID는 null일 수 없습니다.") UUID channelId, @Schema(description = "메시지 작성자 ID", type = "string", format = "uuid", example = "123e4567-e89b-12d3-a456-426614174000") + @NotNull(message = "작성자 ID는 null일 수 없습니다.") UUID authorId -) {} +) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/MessageUpdateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/MessageUpdateRequest.java index b4daaa4d5..b5d8468b9 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/request/MessageUpdateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/MessageUpdateRequest.java @@ -6,5 +6,6 @@ public record MessageUpdateRequest( @Schema(description = "새로운 메시지 내용", example = "수정된 메시지입니다.") String newContent -) {} +) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/PrivateChannelCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/PrivateChannelCreateRequest.java index 8cc6f4c97..e15d7b6a7 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/request/PrivateChannelCreateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/PrivateChannelCreateRequest.java @@ -15,4 +15,5 @@ public record PrivateChannelCreateRequest( ) @ArraySchema(schema = @Schema(type = "string", format = "uuid")) List participantIds -) {} +) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/PublicChannelCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/PublicChannelCreateRequest.java index a925f30d7..09faa010d 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/request/PublicChannelCreateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/PublicChannelCreateRequest.java @@ -1,12 +1,17 @@ package com.sprint.mission.discodeit.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; @Schema(description = "공개 채널 생성 요청 DTO") public record PublicChannelCreateRequest( @Schema(description = "채널 이름", example = "일반-채팅", minLength = 1, maxLength = 100) + @NotBlank(message = "채널 이름은 필수 입니다.") + @Size(min = 1, max = 100) String name, @Schema(description = "채널 설명", example = "일반적인 대화를 나누는 채널입니다", nullable = true) String description -) {} +) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/PublicChannelUpdateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/PublicChannelUpdateRequest.java index 16296521f..cd3e77f41 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/request/PublicChannelUpdateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/PublicChannelUpdateRequest.java @@ -9,4 +9,5 @@ public record PublicChannelUpdateRequest( @Schema(description = "새로운 채널 설명", example = "채널 설명이 수정되었습니다", nullable = true) String newDescription -) {} +) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/ReadStatusCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/ReadStatusCreateRequest.java index 3499411fb..617adcac0 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/request/ReadStatusCreateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/ReadStatusCreateRequest.java @@ -1,6 +1,7 @@ package com.sprint.mission.discodeit.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; import java.time.Instant; import java.util.UUID; @@ -9,13 +10,16 @@ public record ReadStatusCreateRequest( @Schema(description = "사용자 ID", type = "string", format = "uuid", example = "123e4567-e89b-12d3-a456-426614174000") + @NotNull(message = "사용자 ID는 필수 입니다.") UUID userId, @Schema(description = "채널 ID", type = "string", format = "uuid", example = "123e4567-e89b-12d3-a456-426614174000") + @NotNull(message = "채널 ID는 필수 입니다.") UUID channelId, @Schema(description = "마지막 읽은 시간", type = "string", format = "date-time", example = "2024-03-20T09:12:28Z") Instant lastReadAt -) {} +) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/ReadStatusUpdateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/ReadStatusUpdateRequest.java index 3a3c5b301..aa75c1a2d 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/request/ReadStatusUpdateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/ReadStatusUpdateRequest.java @@ -1,6 +1,7 @@ package com.sprint.mission.discodeit.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import java.time.Instant; @@ -8,5 +9,7 @@ public record ReadStatusUpdateRequest( @Schema(description = "새로운 마지막 읽은 시간", type = "string", format = "date-time", example = "2024-03-20T09:12:28Z") + @NotBlank(message = "시간 정보는 필수 값 입니다.") Instant newLastReadAt -) {} +) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/UserCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/UserCreateRequest.java index a98e74a70..154758e4e 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/request/UserCreateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/UserCreateRequest.java @@ -1,15 +1,24 @@ package com.sprint.mission.discodeit.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; @Schema(description = "사용자 생성 요청 DTO") public record UserCreateRequest( @Schema(description = "사용자 이름", example = "john_doe", minLength = 3, maxLength = 50) + @NotBlank(message = "사용자 이름은 필수 입니다.") + @Size(min = 3, max = 50) String username, @Schema(description = "이메일 주소", example = "john.doe@example.com", format = "email") + @Email(message = "이메일은 필수 입니다.") String email, @Schema(description = "비밀번호", example = "password123", minLength = 8) + @NotBlank(message = "비밀번호는 필수 입니다.") + @Size(min = 8) String password -) {} +) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/UserStatusCreateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/UserStatusCreateRequest.java index bd4a08cbe..8221a96d2 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/request/UserStatusCreateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/UserStatusCreateRequest.java @@ -1,6 +1,7 @@ package com.sprint.mission.discodeit.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import java.time.Instant; import java.util.UUID; @@ -9,9 +10,12 @@ public record UserStatusCreateRequest( @Schema(description = "사용자 ID", type = "string", format = "uuid", example = "123e4567-e89b-12d3-a456-426614174000") + @NotBlank(message = "사용자 ID는 필수 입니다.") UUID userId, @Schema(description = "마지막 활동 시간", type = "string", format = "date-time", example = "2024-03-20T09:12:28Z") + @NotBlank(message = "시간 정보는 필수 입니다.") Instant lastActiveAt -) {} +) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/UserStatusUpdateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/UserStatusUpdateRequest.java index d8a0f55ff..adce7fa47 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/request/UserStatusUpdateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/UserStatusUpdateRequest.java @@ -1,6 +1,7 @@ package com.sprint.mission.discodeit.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import java.time.Instant; @@ -8,5 +9,7 @@ public record UserStatusUpdateRequest( @Schema(description = "새로운 마지막 활동 시간", type = "string", format = "date-time", example = "2024-03-20T09:12:28Z") + @NotBlank(message = "시간 정보는 필수 입니다.") Instant newLastActiveAt -) {} +) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/dto/request/UserUpdateRequest.java b/src/main/java/com/sprint/mission/discodeit/dto/request/UserUpdateRequest.java index 15366e516..59e482662 100644 --- a/src/main/java/com/sprint/mission/discodeit/dto/request/UserUpdateRequest.java +++ b/src/main/java/com/sprint/mission/discodeit/dto/request/UserUpdateRequest.java @@ -12,4 +12,5 @@ public record UserUpdateRequest( @Schema(description = "새로운 비밀번호", example = "newpassword123", nullable = true) String newPassword -) {} +) { +} diff --git a/src/main/java/com/sprint/mission/discodeit/entity/BinaryContent.java b/src/main/java/com/sprint/mission/discodeit/entity/BinaryContent.java index b38e5e8f6..a9684580d 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/BinaryContent.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/BinaryContent.java @@ -1,60 +1,55 @@ package com.sprint.mission.discodeit.entity; -import com.fasterxml.jackson.annotation.JsonFormat; import com.sprint.mission.discodeit.entity.base.BaseEntity; -import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; +import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import java.io.Serializable; -import java.time.Instant; -import java.util.UUID; - @Schema( name = "BinaryContent", description = "이진 컨텐츠 정보를 담고 있는 엔티티" ) @Entity @Table(name = "tbl_binary_content") -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Setter public class BinaryContent extends BaseEntity { - @Schema( - description = "파일 이름", - type = "string", - example = "profile.jpg" - ) - @Column(name = "file_name", nullable = false, length = 100) - private String fileName; - - @Schema( - description = "파일 크기 (바이트)", - type = "integer", - format = "int64", - example = "1024" - ) - @Column(name = "size", nullable = false) - private Long size; - - @Schema( - description = "컨텐츠 타입", - type = "string", - example = "image/jpeg" - ) - @Column(name = "content_type", nullable = false, length = 100) - private String contentType; - - public BinaryContent(String fileName, Long size, String contentType) { - this.fileName = fileName; - this.size = size; - this.contentType = contentType; - } + @Schema( + description = "파일 이름", + type = "string", + example = "profile.jpg" + ) + @Column(name = "file_name", nullable = false, length = 100) + private String fileName; + + @Schema( + description = "파일 크기 (바이트)", + type = "integer", + format = "int64", + example = "1024" + ) + @Column(name = "size", nullable = false) + private Long size; + + @Schema( + description = "컨텐츠 타입", + type = "string", + example = "image/jpeg" + ) + @Column(name = "content_type", nullable = false, length = 100) + private String contentType; + + public BinaryContent(String fileName, Long size, String contentType) { + this.fileName = fileName; + this.size = size; + this.contentType = contentType; + } } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/Channel.java b/src/main/java/com/sprint/mission/discodeit/entity/Channel.java index 0f706561a..1969f83cc 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/Channel.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/Channel.java @@ -1,70 +1,77 @@ package com.sprint.mission.discodeit.entity; -import com.fasterxml.jackson.annotation.JsonFormat; -import com.sprint.mission.discodeit.entity.base.BaseEntity; import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.annotations.JdbcType; -import org.hibernate.annotations.JdbcTypeCode; -import org.hibernate.type.SqlTypes; - -import java.io.Serializable; -import java.time.Instant; -import java.util.UUID; +import org.springframework.context.annotation.Profile; @Schema( name = "Channel", description = "채널 정보를 담고 있는 엔티티" ) @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "tbl_channel") @Entity public class Channel extends BaseUpdatableEntity { - @Schema( - description = "채널 타입(PUBLIC/PRIVATE)", - type = "string", - example = "PUBLIC" - ) - @Enumerated(EnumType.STRING) - @Column(name = "type", nullable = false, length = 10, columnDefinition = "discodeit.channel_type") - @JdbcTypeCode(SqlTypes.NAMED_ENUM) - private ChannelType type; + @Schema( + description = "채널 타입(PUBLIC/PRIVATE)", + type = "string", + example = "PUBLIC" + ) + @Enumerated(EnumType.STRING) + @Column(name = "type", nullable = false, length = 10) + private ChannelType type; - @Schema( - description = "채널 이름", - type = "string", - example = "일반", - minLength = 1, - maxLength = 100 - ) - @Column(name = "name", length = 100) - private String name; + @Schema( + description = "채널 이름", + type = "string", + example = "일반", + minLength = 1, + maxLength = 100 + ) + @Column(name = "name", length = 100) + private String name; - @Schema( - description = "채널 설명", - type = "string", - example = "일반적인 대화를 나누는 채널입니다" - ) - @Column(name = "description", length = 1000) - private String description; + @Schema( + description = "채널 설명", + type = "string", + example = "일반적인 대화를 나누는 채널입니다" + ) + @Column(name = "description", length = 1000) + private String description; - public Channel(ChannelType type, String name, String description) { - this.type = type; - this.name = name; - this.description = description; - } + public Channel(ChannelType type, String name, String description) { + this.type = type; + this.name = name; + this.description = description; + } - public void update(String newName, String newDescription) { - if (newName != null && !newName.equals(this.name)) { - this.name = newName; + public void update(String newName, String newDescription) { + if (newName != null && !newName.equals(this.name)) { + this.name = newName; + } + if (newDescription != null && !newDescription.equals(this.description)) { + this.description = newDescription; + } } - if (newDescription != null && !newDescription.equals(this.description)) { - this.description = newDescription; + + @Profile("!test") + @Converter(autoApply = true) + public class ChannelTypePostgresConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(ChannelType attribute) { + return attribute.name(); + } + + @Override + public ChannelType convertToEntityAttribute(String dbData) { + return ChannelType.valueOf(dbData); + } } - } } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/ChannelType.java b/src/main/java/com/sprint/mission/discodeit/entity/ChannelType.java index 4fca37721..9a2ff3f0f 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/ChannelType.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/ChannelType.java @@ -1,6 +1,6 @@ package com.sprint.mission.discodeit.entity; public enum ChannelType { - PUBLIC, - PRIVATE, + PUBLIC, + PRIVATE, } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/Message.java b/src/main/java/com/sprint/mission/discodeit/entity/Message.java index ae4f02956..dabe883b7 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/Message.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/Message.java @@ -1,68 +1,62 @@ package com.sprint.mission.discodeit.entity; -import com.fasterxml.jackson.annotation.JsonFormat; -import com.sprint.mission.discodeit.entity.base.BaseEntity; import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; -import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.ToString; -import java.io.Serializable; -import java.time.Instant; import java.util.ArrayList; import java.util.List; -import java.util.UUID; @Schema( name = "Message", description = "채널 내 메시지 정보를 담고 있는 엔티티" ) @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity @Table(name = "tbl_message") public class Message extends BaseUpdatableEntity { - @Schema( - description = "메시지 내용", - type = "string", - example = "안녕하세요! 반갑습니다." - ) - @Column(name = "content", nullable = false) - private String content; - - @ManyToOne - @JoinColumn(name = "channel_id") - private Channel channel; - - @ManyToOne - @JoinColumn(name = "author_id") - private User author; - - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) - @JoinTable( - name = "tbl_message_attachment", - joinColumns = @JoinColumn(name = "message_id"), - inverseJoinColumns = @JoinColumn(name = "attachment_id") - ) - private List attachments = new ArrayList<>(); - - public Message(String content, Channel channel, User author) { - this.content = content; - this.channel = channel; - this.author = author; - } + @Schema( + description = "메시지 내용", + type = "string", + example = "안녕하세요! 반갑습니다." + ) + @Column(name = "content", nullable = false) + private String content; + + @ManyToOne + @JoinColumn(name = "channel_id") + private Channel channel; + + @ManyToOne + @JoinColumn(name = "author_id") + private User author; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinTable( + name = "tbl_message_attachment", + joinColumns = @JoinColumn(name = "message_id"), + inverseJoinColumns = @JoinColumn(name = "attachment_id") + ) + private List attachments = new ArrayList<>(); + + public Message(String content, Channel channel, User author) { + this.content = content; + this.channel = channel; + this.author = author; + } - public void update(String newContent) { - if (newContent != null && !newContent.equals(this.content)) { - this.content = newContent; + public void update(String newContent) { + if (newContent != null && !newContent.equals(this.content)) { + this.content = newContent; + } } - } - public void addAttachment(BinaryContent attachment) { - attachments.add(attachment); - } + public void addAttachment(BinaryContent attachment) { + attachments.add(attachment); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java b/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java index 950140de1..9933f91c7 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/ReadStatus.java @@ -3,51 +3,53 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import java.io.Serializable; import java.time.Instant; -import java.util.UUID; @Schema( name = "ReadStatus", description = "사용자의 채널별 메시지 읽음 상태 정보를 담고 있는 엔티티" ) @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity @Table(name = "tbl_read_status") public class ReadStatus extends BaseUpdatableEntity { - @ManyToOne - @JoinColumn(name = "user_id") - private User user; - - @ManyToOne - @JoinColumn(name = "channel_id") - private Channel channel; - - @Schema( - description = "사용자가 채널의 메시지를 마지막으로 읽은 시간", - type = "string", - format = "date-time", - example = "2024-03-20T09:12:28Z" - ) - @JsonFormat(shape = JsonFormat.Shape.STRING) - private Instant lastReadAt; - - - public ReadStatus(User user, Channel channel, Instant lastReadAt) { - this.user = user; - this.channel = channel; - this.lastReadAt = lastReadAt; - } - - public void update(Instant newLastReadAt) { - if (newLastReadAt != null && !newLastReadAt.equals(this.lastReadAt)) { - this.lastReadAt = newLastReadAt; + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne + @JoinColumn(name = "channel_id") + private Channel channel; + + @Schema( + description = "사용자가 채널의 메시지를 마지막으로 읽은 시간", + type = "string", + format = "date-time", + example = "2024-03-20T09:12:28Z" + ) + @JsonFormat(shape = JsonFormat.Shape.STRING) + private Instant lastReadAt; + + + public ReadStatus(User user, Channel channel, Instant lastReadAt) { + this.user = user; + this.channel = channel; + this.lastReadAt = lastReadAt; + } + + public void update(Instant newLastReadAt) { + if (newLastReadAt != null && !newLastReadAt.equals(this.lastReadAt)) { + this.lastReadAt = newLastReadAt; + } } - } } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/User.java b/src/main/java/com/sprint/mission/discodeit/entity/User.java index 1ffa73633..0cf1f6144 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/User.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/User.java @@ -3,9 +3,9 @@ import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; @Schema( name = "User", @@ -13,74 +13,73 @@ ) @Entity @Table(name = "tbl_user") -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter -@Setter public class User extends BaseUpdatableEntity { - @Schema( - description = "사용자 이름", - type = "string", - example = "john_doe", - minLength = 3, - maxLength = 50 - ) - @Column(name = "username", nullable = false, unique = true, length = 50) - private String username; + @Schema( + description = "사용자 이름", + type = "string", + example = "john_doe", + minLength = 3, + maxLength = 50 + ) + @Column(name = "username", nullable = false, unique = true, length = 50) + private String username; - @Schema( - description = "사용자 이메일 주소", - type = "string", - format = "email", - example = "john.doe@example.com" - ) - @Column(name = "email", nullable = false, unique = true, length = 100) - private String email; + @Schema( + description = "사용자 이메일 주소", + type = "string", + format = "email", + example = "john.doe@example.com" + ) + @Column(name = "email", nullable = false, unique = true, length = 100) + private String email; - @Schema( - description = "사용자 비밀번호 (해시된 값)", - type = "string", - format = "password", - example = "********" - ) - @Column(name = "password", nullable = false, length = 100) - private String password; + @Schema( + description = "사용자 비밀번호 (해시된 값)", + type = "string", + format = "password", + example = "********" + ) + @Column(name = "password", nullable = false, length = 100) + private String password; - @OneToOne - @JoinColumn(name = "profile_id") - private BinaryContent profile; + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "profile_id") + private BinaryContent profile; - @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) - private UserStatus userStatus; + @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private UserStatus userStatus; - public User(String username, String email, String password, BinaryContent profile) { - this.username = username; - this.email = email; - this.password = password; - this.profile = profile; - } - - public void update(String newUsername, String newEmail, String newPassword, BinaryContent newProfile) { - if (newUsername != null && !newUsername.equals(this.username)) { - this.username = newUsername; + public User(String username, String email, String password, BinaryContent profile) { + this.username = username; + this.email = email; + this.password = password; + this.profile = profile; } - if (newEmail != null && !newEmail.equals(this.email)) { - this.email = newEmail; - } + public void update(String newUsername, String newEmail, String newPassword, BinaryContent newProfile) { + if (newUsername != null && !newUsername.equals(this.username)) { + this.username = newUsername; + } - if (newPassword != null && !newPassword.equals(this.password)) { - this.password = newPassword; - } + if (newEmail != null && !newEmail.equals(this.email)) { + this.email = newEmail; + } - if (newProfile != null && !newProfile.equals(this.profile)) { - this.profile = newProfile; + if (newPassword != null && !newPassword.equals(this.password)) { + this.password = newPassword; + } + + if (newProfile != null && !newProfile.equals(this.profile)) { + this.profile = newProfile; + } } - } - // 양방향 편의 메소드 - public void setUserStatus(UserStatus userStatus) { - this.userStatus = userStatus; - userStatus.setUser(this); - } + // 양방향 편의 메소드 + public void setUserStatus(UserStatus userStatus) { + this.userStatus = userStatus; + userStatus.setUser(this); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java b/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java index 986e48e81..e6eb0fe58 100644 --- a/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java +++ b/src/main/java/com/sprint/mission/discodeit/entity/UserStatus.java @@ -4,15 +4,10 @@ import com.sprint.mission.discodeit.entity.base.BaseUpdatableEntity; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import lombok.*; -import java.io.Serializable; import java.time.Duration; import java.time.Instant; -import java.util.UUID; @Schema( name = "UserStatus", @@ -20,40 +15,41 @@ ) @Entity @Table(name = "tbl_user_status") -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Setter @ToString(exclude = {"user"}) public class UserStatus extends BaseUpdatableEntity { - @OneToOne - @JoinColumn(name = "user_id") - private User user; + @OneToOne + @JoinColumn(name = "user_id") + private User user; - @Schema( - description = "사용자의 마지막 활동 시간", - type = "string", - format = "date-time", - example = "2024-03-20T09:12:28Z" - ) - @JsonFormat(shape = JsonFormat.Shape.STRING) - private Instant lastActiveAt; + @Schema( + description = "사용자의 마지막 활동 시간", + type = "string", + format = "date-time", + example = "2024-03-20T09:12:28Z" + ) + @JsonFormat(shape = JsonFormat.Shape.STRING) + @Column(columnDefinition = "timestamp with time zone", nullable = false) + private Instant lastActiveAt; - public UserStatus(User user, Instant lastActiveAt) { - this.user = user; - this.lastActiveAt = lastActiveAt; - } + public UserStatus(User user, Instant lastActiveAt) { + this.user = user; + this.lastActiveAt = lastActiveAt; + } - public void update(Instant lastActiveAt) { - if (lastActiveAt != null && !lastActiveAt.equals(this.lastActiveAt)) { - this.lastActiveAt = lastActiveAt; + public void update(Instant lastActiveAt) { + if (lastActiveAt != null && !lastActiveAt.equals(this.lastActiveAt)) { + this.lastActiveAt = lastActiveAt; + } } - } - public Boolean isOnline() { - Instant instantFiveMinutesAgo = Instant.now().minus(Duration.ofMinutes(5)); + public Boolean isOnline() { + Instant instantFiveMinutesAgo = Instant.now().minus(Duration.ofMinutes(5)); - return lastActiveAt.isAfter(instantFiveMinutesAgo); - } + return lastActiveAt.isAfter(instantFiveMinutesAgo); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/exception/BaseException.java b/src/main/java/com/sprint/mission/discodeit/exception/BaseException.java deleted file mode 100644 index c1d9ba7ae..000000000 --- a/src/main/java/com/sprint/mission/discodeit/exception/BaseException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.sprint.mission.discodeit.exception; - -import org.springframework.http.HttpStatus; - -public abstract class BaseException extends RuntimeException { - - public abstract String getCode(); - public abstract String getMessage(); - public abstract HttpStatus getHttpStatus(); -} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/BinaryContentNotFoundException.java b/src/main/java/com/sprint/mission/discodeit/exception/BinaryContentNotFoundException.java deleted file mode 100644 index e4d1f4af1..000000000 --- a/src/main/java/com/sprint/mission/discodeit/exception/BinaryContentNotFoundException.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sprint.mission.discodeit.exception; - -import com.sprint.mission.discodeit.dto.ResponseCode; -import org.springframework.http.HttpStatus; - -public class BinaryContentNotFoundException extends BaseException { - - @Override - public String getCode() { - return ResponseCode.BINARY_NOT_FOUND; - } - - @Override - public String getMessage() { - return ResponseCode.BINARY_NOT_FOUND; - } - - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.NOT_FOUND; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/ChannelNotFoundException.java b/src/main/java/com/sprint/mission/discodeit/exception/ChannelNotFoundException.java deleted file mode 100644 index 2a8160227..000000000 --- a/src/main/java/com/sprint/mission/discodeit/exception/ChannelNotFoundException.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sprint.mission.discodeit.exception; - -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; -import org.springframework.http.HttpStatus; - -public class ChannelNotFoundException extends BaseException { - - @Override - public String getCode() { - return ResponseCode.CHANNEL_NOT_FOUND; - } - - @Override - public String getMessage() { - return ResponseMessage.CHANNEL_NOT_FOUND; - } - - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.NOT_FOUND; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/DiscodeitException.java b/src/main/java/com/sprint/mission/discodeit/exception/DiscodeitException.java new file mode 100644 index 000000000..1d667b93c --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/DiscodeitException.java @@ -0,0 +1,25 @@ +package com.sprint.mission.discodeit.exception; + +import com.sprint.mission.discodeit.dto.ErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.Instant; +import java.util.Collections; +import java.util.Map; + +@Getter +public class DiscodeitException extends RuntimeException { + + final Instant timestamp; + final ErrorCode errorCode; + final Map details; + + public DiscodeitException(Instant timestamp, ErrorCode errorCode, Map details) { + this.timestamp = timestamp; + this.errorCode = errorCode; + this.details = details == null + ? Map.of() + : Map.copyOf(details); // 불변 Map으로 복사 + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/DuplicateReadStatusException.java b/src/main/java/com/sprint/mission/discodeit/exception/DuplicateReadStatusException.java deleted file mode 100644 index 5cb5b428a..000000000 --- a/src/main/java/com/sprint/mission/discodeit/exception/DuplicateReadStatusException.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sprint.mission.discodeit.exception; - -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; -import org.springframework.http.HttpStatus; - -public class DuplicateReadStatusException extends BaseException { - - @Override - public String getCode() { - return ResponseCode.DUPLICATE_READ_STATUS; - } - - @Override - public String getMessage() { - return ResponseMessage.DUPLICATE_READ_STATUS; - } - - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.CONFLICT; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/DuplicateUserException.java b/src/main/java/com/sprint/mission/discodeit/exception/DuplicateUserException.java deleted file mode 100644 index 83dcef3ff..000000000 --- a/src/main/java/com/sprint/mission/discodeit/exception/DuplicateUserException.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sprint.mission.discodeit.exception; - -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; -import org.springframework.http.HttpStatus; - -public class DuplicateUserException extends BaseException { - @Override - public String getCode() { - return ResponseCode.DUPLICATE_USER; - } - - @Override - public String getMessage() { - return ResponseMessage.DUPLICATE_USER; - } - - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.CONFLICT; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/DuplicateUserStatusException.java b/src/main/java/com/sprint/mission/discodeit/exception/DuplicateUserStatusException.java deleted file mode 100644 index f0accc65e..000000000 --- a/src/main/java/com/sprint/mission/discodeit/exception/DuplicateUserStatusException.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sprint.mission.discodeit.exception; - -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; -import org.springframework.http.HttpStatus; - -public class DuplicateUserStatusException extends BaseException { - - @Override - public String getCode() { - return ResponseCode.DUPLICATE_USER_STATUS; - } - - @Override - public String getMessage() { - return ResponseMessage.DUPLICATE_USER_STATUS; - } - - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.CONFLICT; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/FileProcessingException.java b/src/main/java/com/sprint/mission/discodeit/exception/FileProcessingException.java deleted file mode 100644 index df16e100c..000000000 --- a/src/main/java/com/sprint/mission/discodeit/exception/FileProcessingException.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sprint.mission.discodeit.exception; - -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; -import org.springframework.http.HttpStatus; - -public class FileProcessingException extends BaseException { - - @Override - public String getCode() { - return ResponseCode.FILE_PROCESSING_ERROR; - } - - @Override - public String getMessage() { - return ResponseMessage.FILE_PROCESSING_ERROR; - } - - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.INTERNAL_SERVER_ERROR; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/GlobalExceptionHandler.java b/src/main/java/com/sprint/mission/discodeit/exception/GlobalExceptionHandler.java index ecb76fff4..5ffc187e4 100644 --- a/src/main/java/com/sprint/mission/discodeit/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/sprint/mission/discodeit/exception/GlobalExceptionHandler.java @@ -1,30 +1,56 @@ package com.sprint.mission.discodeit.exception; -import com.sprint.mission.discodeit.dto.CodeMessageResponseDto; -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; +import com.sprint.mission.discodeit.dto.ErrorResponse; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestControllerAdvice; -import java.util.NoSuchElementException; +import java.time.Instant; +import java.util.Map; -@ControllerAdvice +@RestControllerAdvice @ResponseBody public class GlobalExceptionHandler { - @ExceptionHandler(BaseException.class) - public ResponseEntity> handleBaseException(BaseException ex) { - return ResponseEntity.status(ex.getHttpStatus()) - .body(CodeMessageResponseDto.error(ex.getCode(), ex.getMessage())); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity handleException(Exception e) { - return ResponseEntity - .status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(e.getMessage()); - } + @ExceptionHandler(DiscodeitException.class) + public ResponseEntity handleBaseException(DiscodeitException ex) { + + return ResponseEntity.status(ex.getErrorCode().getStatus()) + .body(new ErrorResponse( + ex.getErrorCode().toString(), + ex.getErrorCode().getMessage(), + ex.getDetails(), + ex.getClass().getSimpleName(), + ex.getErrorCode().getStatus().value(), + ex.getTimestamp() + )); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { + String message = ex.getBindingResult().getFieldErrors().stream() + .map(e -> e.getField() + ": " + e.getDefaultMessage()) + .findFirst() + .orElse("잘못된 요청입니다."); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ErrorResponse( + "BAD_REQUEST", + "잘못된 요청입니다.", + Map.of("message", message), + ex.getClass().getSimpleName(), + 400, + Instant.now() + )); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e) { + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(e.getMessage()); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/exception/IdOrPasswordValidException.java b/src/main/java/com/sprint/mission/discodeit/exception/IdOrPasswordValidException.java new file mode 100644 index 000000000..83d39ec7a --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/IdOrPasswordValidException.java @@ -0,0 +1,17 @@ +package com.sprint.mission.discodeit.exception; + +import com.sprint.mission.discodeit.dto.ErrorCode; +import com.sprint.mission.discodeit.dto.ResponseMessage; + +import java.time.Instant; +import java.util.Map; + +public class IdOrPasswordValidException extends DiscodeitException { + public IdOrPasswordValidException() { + super( + Instant.now(), + ErrorCode.ID_OR_PASSWORD_VALID, + Map.of("message", ResponseMessage.ID_OR_PASSWORD_VALID) + ); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/InternalErrorException.java b/src/main/java/com/sprint/mission/discodeit/exception/InternalErrorException.java index 0c07ae044..6af25bfbc 100644 --- a/src/main/java/com/sprint/mission/discodeit/exception/InternalErrorException.java +++ b/src/main/java/com/sprint/mission/discodeit/exception/InternalErrorException.java @@ -1,23 +1,17 @@ package com.sprint.mission.discodeit.exception; -import com.sprint.mission.discodeit.dto.ResponseCode; +import com.sprint.mission.discodeit.dto.ErrorCode; import com.sprint.mission.discodeit.dto.ResponseMessage; -import org.springframework.http.HttpStatus; -public class InternalErrorException extends BaseException { +import java.time.Instant; +import java.util.Map; - @Override - public String getCode() { - return ResponseCode.INTERNAL_ERROR; - } - - @Override - public String getMessage() { - return ResponseMessage.INTERNAL_SERVER_ERROR; - } - - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.INTERNAL_SERVER_ERROR; +public class InternalErrorException extends DiscodeitException { + public InternalErrorException() { + super( + Instant.now(), + ErrorCode.INTERNAL_ERROR, + Map.of("message", ResponseMessage.INTERNAL_SERVER_ERROR) + ); } } diff --git a/src/main/java/com/sprint/mission/discodeit/exception/MessageNotFoundException.java b/src/main/java/com/sprint/mission/discodeit/exception/MessageNotFoundException.java deleted file mode 100644 index ebe6e1a6a..000000000 --- a/src/main/java/com/sprint/mission/discodeit/exception/MessageNotFoundException.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sprint.mission.discodeit.exception; - -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; -import org.springframework.http.HttpStatus; - -public class MessageNotFoundException extends BaseException { - - @Override - public String getCode() { - return ResponseCode.MESSAGE_NOT_FOUND; - } - - @Override - public String getMessage() { - return ResponseMessage.MESSAGE_NOT_FOUND; - } - - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.NOT_FOUND; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/PasswordValidException.java b/src/main/java/com/sprint/mission/discodeit/exception/PasswordValidException.java deleted file mode 100644 index 838756235..000000000 --- a/src/main/java/com/sprint/mission/discodeit/exception/PasswordValidException.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sprint.mission.discodeit.exception; - -import com.sprint.mission.discodeit.dto.ResponseCode; -import org.springframework.http.HttpStatus; - -public class PasswordValidException extends BaseException { - - @Override - public String getCode() { - return ResponseCode.PASSWORD_VALID; - } - - @Override - public String getMessage() { - return ResponseCode.PASSWORD_VALID; - } - - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.BAD_REQUEST; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/ReadStatusNotFoundException.java b/src/main/java/com/sprint/mission/discodeit/exception/ReadStatusNotFoundException.java deleted file mode 100644 index 255db0611..000000000 --- a/src/main/java/com/sprint/mission/discodeit/exception/ReadStatusNotFoundException.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sprint.mission.discodeit.exception; - -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; -import org.springframework.http.HttpStatus; - -public class ReadStatusNotFoundException extends BaseException { - - @Override - public String getCode() { - return ResponseCode.READ_STATUS_NOT_FOUND; - } - - @Override - public String getMessage() { - return ResponseMessage.READ_STATUS_NOT_FOUND; - } - - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.NOT_FOUND; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/RequestFailException.java b/src/main/java/com/sprint/mission/discodeit/exception/RequestFailException.java deleted file mode 100644 index 306aa6d59..000000000 --- a/src/main/java/com/sprint/mission/discodeit/exception/RequestFailException.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sprint.mission.discodeit.exception; - -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; -import org.springframework.http.HttpStatus; - -public class RequestFailException extends BaseException { - - @Override - public String getCode() { - return ResponseCode.REQUEST_FAIL; - } - - @Override - public String getMessage() { - return ResponseMessage.REQUEST_FAIL; - } - - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.BAD_REQUEST; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/UserNotFoundException.java b/src/main/java/com/sprint/mission/discodeit/exception/UserNotFoundException.java deleted file mode 100644 index 1d253363b..000000000 --- a/src/main/java/com/sprint/mission/discodeit/exception/UserNotFoundException.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sprint.mission.discodeit.exception; - -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; -import org.springframework.http.HttpStatus; - -public class UserNotFoundException extends BaseException{ - @Override - public String getCode() { - return ResponseCode.USER_NOT_FOUND; - } - - @Override - public String getMessage() { - return ResponseMessage.USER_NOT_FOUND; - } - - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.NOT_FOUND; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/UserOrChannelNotFoundException.java b/src/main/java/com/sprint/mission/discodeit/exception/UserOrChannelNotFoundException.java deleted file mode 100644 index deb0e024d..000000000 --- a/src/main/java/com/sprint/mission/discodeit/exception/UserOrChannelNotFoundException.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sprint.mission.discodeit.exception; - -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; -import org.springframework.http.HttpStatus; - -public class UserOrChannelNotFoundException extends BaseException { - - @Override - public String getCode() { - return ResponseCode.USER_OR_CHANNEL_NOT_FOUND; - } - - @Override - public String getMessage() { - return ResponseMessage.USER_OR_CHANNEL_NOT_FOUND; - } - - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.NOT_FOUND; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/UserStatusNotFoundException.java b/src/main/java/com/sprint/mission/discodeit/exception/UserStatusNotFoundException.java deleted file mode 100644 index b2093d68c..000000000 --- a/src/main/java/com/sprint/mission/discodeit/exception/UserStatusNotFoundException.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sprint.mission.discodeit.exception; - -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; -import org.springframework.http.HttpStatus; - -public class UserStatusNotFoundException extends BaseException { - - @Override - public String getCode() { - return ResponseCode.USER_STATUS_NOT_FOUND; - } - - @Override - public String getMessage() { - return ResponseMessage.USER_STATUS_NOT_FOUND; - } - - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.NOT_FOUND; - } -} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/binaryContent/BinaryContentException.java b/src/main/java/com/sprint/mission/discodeit/exception/binaryContent/BinaryContentException.java new file mode 100644 index 000000000..1e5f8e09d --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/binaryContent/BinaryContentException.java @@ -0,0 +1,13 @@ +package com.sprint.mission.discodeit.exception.binaryContent; + +import com.sprint.mission.discodeit.dto.ErrorCode; +import com.sprint.mission.discodeit.exception.DiscodeitException; + +import java.time.Instant; +import java.util.Map; + +public class BinaryContentException extends DiscodeitException { + public BinaryContentException(Instant timestamp, ErrorCode errorCode, Map details) { + super(timestamp, errorCode, details); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/binaryContent/BinaryContentNotFoundException.java b/src/main/java/com/sprint/mission/discodeit/exception/binaryContent/BinaryContentNotFoundException.java new file mode 100644 index 000000000..652df8c4d --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/binaryContent/BinaryContentNotFoundException.java @@ -0,0 +1,17 @@ +package com.sprint.mission.discodeit.exception.binaryContent; + +import com.sprint.mission.discodeit.dto.ErrorCode; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +public class BinaryContentNotFoundException extends BinaryContentException { + public BinaryContentNotFoundException(UUID binaryContentId) { + super( + Instant.now(), + ErrorCode.BINARY_NOT_FOUND, + Map.of("binaryContentId", binaryContentId.toString()) + ); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/binaryContent/FileProcessingException.java b/src/main/java/com/sprint/mission/discodeit/exception/binaryContent/FileProcessingException.java new file mode 100644 index 000000000..4b07597c9 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/binaryContent/FileProcessingException.java @@ -0,0 +1,17 @@ +package com.sprint.mission.discodeit.exception.binaryContent; + +import com.sprint.mission.discodeit.dto.ErrorCode; +import com.sprint.mission.discodeit.dto.ResponseMessage; + +import java.time.Instant; +import java.util.Map; + +public class FileProcessingException extends BinaryContentException { + public FileProcessingException() { + super( + Instant.now(), + ErrorCode.FILE_PROCESSING_ERROR, + Map.of("message", ResponseMessage.FILE_PROCESSING_ERROR) + ); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/channel/ChannelException.java b/src/main/java/com/sprint/mission/discodeit/exception/channel/ChannelException.java new file mode 100644 index 000000000..5594de24e --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/channel/ChannelException.java @@ -0,0 +1,13 @@ +package com.sprint.mission.discodeit.exception.channel; + +import com.sprint.mission.discodeit.dto.ErrorCode; +import com.sprint.mission.discodeit.exception.DiscodeitException; + +import java.time.Instant; +import java.util.Map; + +public class ChannelException extends DiscodeitException { + public ChannelException(Instant timestamp, ErrorCode errorCode, Map details) { + super(timestamp, errorCode, details); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/channel/ChannelNotFoundException.java b/src/main/java/com/sprint/mission/discodeit/exception/channel/ChannelNotFoundException.java new file mode 100644 index 000000000..b6a161bb6 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/channel/ChannelNotFoundException.java @@ -0,0 +1,18 @@ +package com.sprint.mission.discodeit.exception.channel; + +import com.sprint.mission.discodeit.dto.ErrorCode; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +public class ChannelNotFoundException extends ChannelException { + + public ChannelNotFoundException(UUID channelId) { + super( + Instant.now(), + ErrorCode.CHANNEL_NOT_FOUND, + Map.of("channelId", channelId.toString()) + ); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/channel/PrivateChannelUpdateDeniedException.java b/src/main/java/com/sprint/mission/discodeit/exception/channel/PrivateChannelUpdateDeniedException.java new file mode 100644 index 000000000..28faa186f --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/channel/PrivateChannelUpdateDeniedException.java @@ -0,0 +1,18 @@ +package com.sprint.mission.discodeit.exception.channel; + +import com.sprint.mission.discodeit.dto.ErrorCode; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +public class PrivateChannelUpdateDeniedException extends ChannelException { + + public PrivateChannelUpdateDeniedException(UUID channelId) { + super( + Instant.now(), + ErrorCode.PRIVATE_CHANNEL_UPDATE_DENIED, + Map.of("channelId", channelId) + ); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/message/MessageException.java b/src/main/java/com/sprint/mission/discodeit/exception/message/MessageException.java new file mode 100644 index 000000000..6b98ce685 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/message/MessageException.java @@ -0,0 +1,13 @@ +package com.sprint.mission.discodeit.exception.message; + +import com.sprint.mission.discodeit.dto.ErrorCode; +import com.sprint.mission.discodeit.exception.DiscodeitException; + +import java.time.Instant; +import java.util.Map; + +public class MessageException extends DiscodeitException { + public MessageException(Instant timestamp, ErrorCode errorCode, Map details) { + super(timestamp, errorCode, details); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/message/MessageNotFoundException.java b/src/main/java/com/sprint/mission/discodeit/exception/message/MessageNotFoundException.java new file mode 100644 index 000000000..d765506b9 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/message/MessageNotFoundException.java @@ -0,0 +1,17 @@ +package com.sprint.mission.discodeit.exception.message; + +import com.sprint.mission.discodeit.dto.ErrorCode; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +public class MessageNotFoundException extends MessageException { + public MessageNotFoundException(UUID messageId) { + super( + Instant.now(), + ErrorCode.MESSAGE_NOT_FOUND, + Map.of("messageId", messageId) + ); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/readStatus/DuplicateReadStatusException.java b/src/main/java/com/sprint/mission/discodeit/exception/readStatus/DuplicateReadStatusException.java new file mode 100644 index 000000000..deb59ec70 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/readStatus/DuplicateReadStatusException.java @@ -0,0 +1,20 @@ +package com.sprint.mission.discodeit.exception.readStatus; + +import com.sprint.mission.discodeit.dto.ErrorCode; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +public class DuplicateReadStatusException extends ReadStatusException { + public DuplicateReadStatusException(UUID userId, UUID channelId) { + super( + Instant.now(), + ErrorCode.DUPLICATE_READ_STATUS, + Map.of( + "userId", userId, + "channelId", channelId + ) + ); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/readStatus/ReadStatusException.java b/src/main/java/com/sprint/mission/discodeit/exception/readStatus/ReadStatusException.java new file mode 100644 index 000000000..f36cdad79 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/readStatus/ReadStatusException.java @@ -0,0 +1,13 @@ +package com.sprint.mission.discodeit.exception.readStatus; + +import com.sprint.mission.discodeit.dto.ErrorCode; +import com.sprint.mission.discodeit.exception.DiscodeitException; + +import java.time.Instant; +import java.util.Map; + +public class ReadStatusException extends DiscodeitException { + public ReadStatusException(Instant timestamp, ErrorCode errorCode, Map details) { + super(timestamp, errorCode, details); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/readStatus/ReadStatusNotFoundException.java b/src/main/java/com/sprint/mission/discodeit/exception/readStatus/ReadStatusNotFoundException.java new file mode 100644 index 000000000..19a657015 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/readStatus/ReadStatusNotFoundException.java @@ -0,0 +1,17 @@ +package com.sprint.mission.discodeit.exception.readStatus; + +import com.sprint.mission.discodeit.dto.ErrorCode; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +public class ReadStatusNotFoundException extends ReadStatusException { + public ReadStatusNotFoundException(UUID readStatusId) { + super( + Instant.now(), + ErrorCode.READ_STATUS_NOT_FOUND, + Map.of("readStatusId", readStatusId) + ); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/user/DuplicateUserException.java b/src/main/java/com/sprint/mission/discodeit/exception/user/DuplicateUserException.java new file mode 100644 index 000000000..b7a72bfc7 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/user/DuplicateUserException.java @@ -0,0 +1,25 @@ +package com.sprint.mission.discodeit.exception.user; + +import com.sprint.mission.discodeit.dto.ErrorCode; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +public class DuplicateUserException extends UserException { + public DuplicateUserException(UUID userId) { + super( + Instant.now(), + ErrorCode.DUPLICATE_USER, + Map.of("userId", userId) + ); + } + + public DuplicateUserException(String username, String email) { + super( + Instant.now(), + ErrorCode.DUPLICATE_USER, + Map.of("username", username, "email", email) + ); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/user/UserException.java b/src/main/java/com/sprint/mission/discodeit/exception/user/UserException.java new file mode 100644 index 000000000..e23822645 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/user/UserException.java @@ -0,0 +1,14 @@ +package com.sprint.mission.discodeit.exception.user; + + +import com.sprint.mission.discodeit.dto.ErrorCode; +import com.sprint.mission.discodeit.exception.DiscodeitException; + +import java.time.Instant; +import java.util.Map; + +public class UserException extends DiscodeitException { + public UserException(Instant timestamp, ErrorCode errorCode, Map details) { + super(timestamp, errorCode, details); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/user/UserNotFoundException.java b/src/main/java/com/sprint/mission/discodeit/exception/user/UserNotFoundException.java new file mode 100644 index 000000000..1883d4cfa --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/user/UserNotFoundException.java @@ -0,0 +1,26 @@ +package com.sprint.mission.discodeit.exception.user; + +import com.sprint.mission.discodeit.dto.ErrorCode; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +public class UserNotFoundException extends UserException { + + public UserNotFoundException(UUID userId) { + super( + Instant.now(), + ErrorCode.USER_NOT_FOUND, + Map.of("userId", userId) + ); + } + + public UserNotFoundException(String username) { + super( + Instant.now(), + ErrorCode.USER_NOT_FOUND, + Map.of("username", username) + ); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/userStatus/DuplicateUserStatusException.java b/src/main/java/com/sprint/mission/discodeit/exception/userStatus/DuplicateUserStatusException.java new file mode 100644 index 000000000..171a20305 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/userStatus/DuplicateUserStatusException.java @@ -0,0 +1,17 @@ +package com.sprint.mission.discodeit.exception.userStatus; + +import com.sprint.mission.discodeit.dto.ErrorCode; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +public class DuplicateUserStatusException extends UserStatusException { + public DuplicateUserStatusException(UUID userId) { + super( + Instant.now(), + ErrorCode.DUPLICATE_USER_STATUS, + Map.of("userId", userId) + ); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/userStatus/UserStatusException.java b/src/main/java/com/sprint/mission/discodeit/exception/userStatus/UserStatusException.java new file mode 100644 index 000000000..c92f77536 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/userStatus/UserStatusException.java @@ -0,0 +1,13 @@ +package com.sprint.mission.discodeit.exception.userStatus; + +import com.sprint.mission.discodeit.dto.ErrorCode; +import com.sprint.mission.discodeit.exception.DiscodeitException; + +import java.time.Instant; +import java.util.Map; + +public class UserStatusException extends DiscodeitException { + public UserStatusException(Instant timestamp, ErrorCode errorCode, Map details) { + super(timestamp, errorCode, details); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/exception/userStatus/UserStatusNotFoundException.java b/src/main/java/com/sprint/mission/discodeit/exception/userStatus/UserStatusNotFoundException.java new file mode 100644 index 000000000..e7b2a4c15 --- /dev/null +++ b/src/main/java/com/sprint/mission/discodeit/exception/userStatus/UserStatusNotFoundException.java @@ -0,0 +1,18 @@ +package com.sprint.mission.discodeit.exception.userStatus; + +import com.sprint.mission.discodeit.dto.ErrorCode; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +public class UserStatusNotFoundException extends UserStatusException { + + public UserStatusNotFoundException(UUID userStatusId) { + super( + Instant.now(), + ErrorCode.USER_STATUS_NOT_FOUND, + Map.of("userStatusId", userStatusId) + ); + } +} diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/ChannelMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/ChannelMapper.java index c6be1cd97..a1796620c 100644 --- a/src/main/java/com/sprint/mission/discodeit/mapper/ChannelMapper.java +++ b/src/main/java/com/sprint/mission/discodeit/mapper/ChannelMapper.java @@ -8,11 +8,7 @@ import com.sprint.mission.discodeit.entity.ReadStatus; import com.sprint.mission.discodeit.repository.MessageRepository; import com.sprint.mission.discodeit.repository.ReadStatusRepository; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; -import org.mapstruct.Mapper; import org.springframework.stereotype.Component; import java.time.Instant; diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/MessageMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/MessageMapper.java index d96754046..645bb9ad4 100644 --- a/src/main/java/com/sprint/mission/discodeit/mapper/MessageMapper.java +++ b/src/main/java/com/sprint/mission/discodeit/mapper/MessageMapper.java @@ -2,10 +2,8 @@ import com.sprint.mission.discodeit.dto.data.MessageDto; import com.sprint.mission.discodeit.entity.Message; -import lombok.RequiredArgsConstructor; import org.mapstruct.Mapper; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; @Mapper(componentModel = "spring") public abstract class MessageMapper { diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/ReadStatusMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/ReadStatusMapper.java index 4e7764fd6..26b9fa5d1 100644 --- a/src/main/java/com/sprint/mission/discodeit/mapper/ReadStatusMapper.java +++ b/src/main/java/com/sprint/mission/discodeit/mapper/ReadStatusMapper.java @@ -4,7 +4,6 @@ import com.sprint.mission.discodeit.entity.ReadStatus; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.springframework.stereotype.Component; @Mapper(componentModel = "spring") public interface ReadStatusMapper { diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java index 9e5fa6ca0..fa1099af7 100644 --- a/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java +++ b/src/main/java/com/sprint/mission/discodeit/mapper/UserMapper.java @@ -2,11 +2,9 @@ import com.sprint.mission.discodeit.dto.data.UserDto; import com.sprint.mission.discodeit.entity.User; -import lombok.RequiredArgsConstructor; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; @Mapper(componentModel = "spring", uses = {BinaryContentMapper.class}) public abstract class UserMapper { diff --git a/src/main/java/com/sprint/mission/discodeit/mapper/UserStatusMapper.java b/src/main/java/com/sprint/mission/discodeit/mapper/UserStatusMapper.java index 9b5a506d5..fe8aa8435 100644 --- a/src/main/java/com/sprint/mission/discodeit/mapper/UserStatusMapper.java +++ b/src/main/java/com/sprint/mission/discodeit/mapper/UserStatusMapper.java @@ -4,7 +4,6 @@ import com.sprint.mission.discodeit.entity.UserStatus; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.springframework.stereotype.Component; @Mapper(componentModel = "spring") public interface UserStatusMapper { diff --git a/src/main/java/com/sprint/mission/discodeit/repository/BinaryContentRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/BinaryContentRepository.java index bc546a3c5..d2444ec3e 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/BinaryContentRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/BinaryContentRepository.java @@ -5,7 +5,6 @@ import org.springframework.stereotype.Repository; import java.util.List; -import java.util.Optional; import java.util.UUID; @Repository diff --git a/src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java index 3822a5811..7fe04ba5a 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/ChannelRepository.java @@ -4,7 +4,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import java.util.List; import java.util.UUID; @Repository diff --git a/src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java index 753b06763..8ca409f1b 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/MessageRepository.java @@ -1,30 +1,26 @@ package com.sprint.mission.discodeit.repository; import com.sprint.mission.discodeit.entity.Message; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.time.Instant; import java.util.List; -import java.util.Optional; import java.util.UUID; @Repository public interface MessageRepository extends JpaRepository { @EntityGraph(attributePaths = {"author", "author.userStatus", "author.profile", "attachments"}) public List findAllByChannelId(UUID channelId); - @EntityGraph(attributePaths = {"author", "author.userStatus", "author.profile", "attachments"}) - public Slice findAllByChannelId(UUID channelId, Pageable pageable); @EntityGraph(attributePaths = {"author", "author.userStatus", "author.profile", "attachments"}) public Slice findByChannelIdAndCreatedAtLessThanOrderByCreatedAtDesc(UUID channelId, Instant cursor, Pageable pageable); @EntityGraph(attributePaths = {"author", "author.userStatus", "author.profile", "attachments"}) public Slice findByChannelIdOrderByCreatedAtDesc(UUID channelId, Pageable pageable); + public void deleteAllByChannelId(UUID channelId); } diff --git a/src/main/java/com/sprint/mission/discodeit/repository/ReadStatusRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/ReadStatusRepository.java index 803945f3f..8317bfb1d 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/ReadStatusRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/ReadStatusRepository.java @@ -1,19 +1,19 @@ package com.sprint.mission.discodeit.repository; import com.sprint.mission.discodeit.entity.ReadStatus; -import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; -import java.util.Optional; import java.util.UUID; @Repository public interface ReadStatusRepository extends JpaRepository { -// @EntityGraph(attributePaths = {"user", "channel", "user.profile"}) + // @EntityGraph(attributePaths = {"user", "channel", "user.profile"}) List findAllByUserId(UUID userId); + void deleteAllByChannelId(UUID channelId); -// @EntityGraph(attributePaths = {"user", "channel", "user.profile"}) + + // @EntityGraph(attributePaths = {"user", "channel", "user.profile"}) List findAllByChannelId(UUID channelId); } diff --git a/src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java index b4ff470de..971081c40 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/UserRepository.java @@ -3,7 +3,6 @@ import com.sprint.mission.discodeit.entity.User; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.List; @@ -12,16 +11,17 @@ @Repository public interface UserRepository extends JpaRepository { -// @EntityGraph(attributePaths = {"userStatus", "profile"}) - @Query("SELECT u FROM User u JOIN FETCH u.userStatus JOIN FETCH u.profile") - @Override - List findAll(); + @EntityGraph(attributePaths = {"userStatus", "profile"}) + @Override + List findAll(); - @Query("SELECT u FROM User u JOIN FETCH u.userStatus JOIN FETCH u.profile") - @Override - Optional findById(UUID uuid); + @EntityGraph(attributePaths = {"userStatus", "profile"}) + @Override + Optional findById(UUID uuid); - public Optional findByUsername(String username); - public boolean existsByUsername(String username); - public boolean existsByEmail(String email); + public Optional findByUsername(String username); + + public boolean existsByUsername(String username); + + public boolean existsByEmail(String email); } diff --git a/src/main/java/com/sprint/mission/discodeit/repository/UserStatusRepository.java b/src/main/java/com/sprint/mission/discodeit/repository/UserStatusRepository.java index d15aac224..032dca9bc 100644 --- a/src/main/java/com/sprint/mission/discodeit/repository/UserStatusRepository.java +++ b/src/main/java/com/sprint/mission/discodeit/repository/UserStatusRepository.java @@ -5,7 +5,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import java.util.List; import java.util.Optional; import java.util.UUID; diff --git a/src/main/java/com/sprint/mission/discodeit/service/AuthService.java b/src/main/java/com/sprint/mission/discodeit/service/AuthService.java index 6eecdd0a9..80c15b333 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/AuthService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/AuthService.java @@ -2,9 +2,8 @@ import com.sprint.mission.discodeit.dto.data.UserDto; import com.sprint.mission.discodeit.dto.request.LoginRequest; -import com.sprint.mission.discodeit.entity.User; public interface AuthService { - UserDto login(LoginRequest loginRequest); + UserDto login(LoginRequest loginRequest); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/BinaryContentService.java b/src/main/java/com/sprint/mission/discodeit/service/BinaryContentService.java index c483db1c4..d64741362 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/BinaryContentService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/BinaryContentService.java @@ -2,18 +2,17 @@ import com.sprint.mission.discodeit.dto.data.BinaryContentDto; import com.sprint.mission.discodeit.dto.request.BinaryContentCreateRequest; -import com.sprint.mission.discodeit.entity.BinaryContent; import java.util.List; import java.util.UUID; public interface BinaryContentService { - BinaryContentDto create(BinaryContentCreateRequest request); + BinaryContentDto create(BinaryContentCreateRequest request); - BinaryContentDto find(UUID binaryContentId); + BinaryContentDto find(UUID binaryContentId); - List findAllByIdIn(List binaryContentIds); + List findAllByIdIn(List binaryContentIds); - void delete(UUID binaryContentId); + void delete(UUID binaryContentId); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/ChannelService.java b/src/main/java/com/sprint/mission/discodeit/service/ChannelService.java index 253f5f33c..1d26aa56d 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/ChannelService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/ChannelService.java @@ -4,22 +4,21 @@ import com.sprint.mission.discodeit.dto.request.PrivateChannelCreateRequest; import com.sprint.mission.discodeit.dto.request.PublicChannelCreateRequest; import com.sprint.mission.discodeit.dto.request.PublicChannelUpdateRequest; -import com.sprint.mission.discodeit.entity.Channel; import java.util.List; import java.util.UUID; public interface ChannelService { - ChannelDto create(PublicChannelCreateRequest request); + ChannelDto create(PublicChannelCreateRequest request); - ChannelDto create(PrivateChannelCreateRequest request); + ChannelDto create(PrivateChannelCreateRequest request); - ChannelDto find(UUID channelId); + ChannelDto find(UUID channelId); - List findAllByUserId(UUID userId); + List findAllByUserId(UUID userId); - ChannelDto update(UUID channelId, PublicChannelUpdateRequest request); + ChannelDto update(UUID channelId, PublicChannelUpdateRequest request); - void delete(UUID channelId); + void delete(UUID channelId); } \ No newline at end of file diff --git a/src/main/java/com/sprint/mission/discodeit/service/MessageService.java b/src/main/java/com/sprint/mission/discodeit/service/MessageService.java index c5d055dc2..0d2254410 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/MessageService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/MessageService.java @@ -5,10 +5,7 @@ import com.sprint.mission.discodeit.dto.request.MessageCreateRequest; import com.sprint.mission.discodeit.dto.request.MessageUpdateRequest; import com.sprint.mission.discodeit.dto.response.PageResponse; -import com.sprint.mission.discodeit.entity.Message; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; import java.time.Instant; import java.util.List; @@ -16,14 +13,14 @@ public interface MessageService { - MessageDto create(MessageCreateRequest messageCreateRequest, - List binaryContentCreateRequests); + MessageDto create(MessageCreateRequest messageCreateRequest, + List binaryContentCreateRequests); - MessageDto find(UUID messageId); + MessageDto find(UUID messageId); - PageResponse findAllByChannelId(UUID channelId, Instant cursor, Pageable pageable); + PageResponse findAllByChannelId(UUID channelId, Instant cursor, Pageable pageable); - MessageDto update(UUID messageId, MessageUpdateRequest request); + MessageDto update(UUID messageId, MessageUpdateRequest request); - void delete(UUID messageId); + void delete(UUID messageId); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/ReadStatusService.java b/src/main/java/com/sprint/mission/discodeit/service/ReadStatusService.java index 61c6224c2..09b1b5bec 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/ReadStatusService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/ReadStatusService.java @@ -3,20 +3,19 @@ import com.sprint.mission.discodeit.dto.data.ReadStatusDto; import com.sprint.mission.discodeit.dto.request.ReadStatusCreateRequest; import com.sprint.mission.discodeit.dto.request.ReadStatusUpdateRequest; -import com.sprint.mission.discodeit.entity.ReadStatus; import java.util.List; import java.util.UUID; public interface ReadStatusService { - ReadStatusDto create(ReadStatusCreateRequest request); + ReadStatusDto create(ReadStatusCreateRequest request); - ReadStatusDto find(UUID readStatusId); + ReadStatusDto find(UUID readStatusId); - List findAllByUserId(UUID userId); + List findAllByUserId(UUID userId); - ReadStatusDto update(UUID readStatusId, ReadStatusUpdateRequest request); + ReadStatusDto update(UUID readStatusId, ReadStatusUpdateRequest request); - void delete(UUID readStatusId); + void delete(UUID readStatusId); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/UserService.java b/src/main/java/com/sprint/mission/discodeit/service/UserService.java index 0bd6c5112..9d5714c9b 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/UserService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/UserService.java @@ -4,7 +4,6 @@ import com.sprint.mission.discodeit.dto.request.BinaryContentCreateRequest; import com.sprint.mission.discodeit.dto.request.UserCreateRequest; import com.sprint.mission.discodeit.dto.request.UserUpdateRequest; -import com.sprint.mission.discodeit.entity.User; import java.util.List; import java.util.Optional; @@ -12,15 +11,15 @@ public interface UserService { - UserDto create(UserCreateRequest userCreateRequest, - Optional profileCreateRequest); + UserDto create(UserCreateRequest userCreateRequest, + Optional profileCreateRequest); - UserDto find(UUID userId); + UserDto find(UUID userId); - List findAll(); + List findAll(); - UserDto update(UUID userId, UserUpdateRequest userUpdateRequest, - Optional profileCreateRequest); + UserDto update(UUID userId, UserUpdateRequest userUpdateRequest, + Optional profileCreateRequest); - void delete(UUID userId); + void delete(UUID userId); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/UserStatusService.java b/src/main/java/com/sprint/mission/discodeit/service/UserStatusService.java index c32b97f64..78f1d9733 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/UserStatusService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/UserStatusService.java @@ -10,15 +10,15 @@ public interface UserStatusService { - UserStatus create(UserStatusCreateRequest request); + UserStatus create(UserStatusCreateRequest request); - UserStatus find(UUID userStatusId); + UserStatus find(UUID userStatusId); - List findAll(); + List findAll(); - UserStatusDto update(UUID userStatusId, UserStatusUpdateRequest request); + UserStatusDto update(UUID userStatusId, UserStatusUpdateRequest request); - UserStatusDto updateByUserId(UUID userId, UserStatusUpdateRequest request); + UserStatusDto updateByUserId(UUID userId, UserStatusUpdateRequest request); - void delete(UUID userStatusId); + void delete(UUID userStatusId); } diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicAuthService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicAuthService.java index 506e0f6a5..36e5bd6e9 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicAuthService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicAuthService.java @@ -3,8 +3,8 @@ import com.sprint.mission.discodeit.dto.data.UserDto; import com.sprint.mission.discodeit.dto.request.LoginRequest; import com.sprint.mission.discodeit.entity.User; -import com.sprint.mission.discodeit.exception.PasswordValidException; -import com.sprint.mission.discodeit.exception.UserNotFoundException; +import com.sprint.mission.discodeit.exception.IdOrPasswordValidException; +import com.sprint.mission.discodeit.exception.user.UserNotFoundException; import com.sprint.mission.discodeit.mapper.UserMapper; import com.sprint.mission.discodeit.repository.UserRepository; import com.sprint.mission.discodeit.service.AuthService; @@ -12,28 +12,26 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.NoSuchElementException; - @RequiredArgsConstructor @Service public class BasicAuthService implements AuthService { - private final UserRepository userRepository; - private final UserMapper userMapper; + private final UserRepository userRepository; + private final UserMapper userMapper; - @Transactional(readOnly = true) - @Override - public UserDto login(LoginRequest loginRequest) { - String username = loginRequest.username(); - String password = loginRequest.password(); + @Transactional(readOnly = true) + @Override + public UserDto login(LoginRequest loginRequest) { + String username = loginRequest.username(); + String password = loginRequest.password(); - User user = userRepository.findByUsername(username) - .orElseThrow(UserNotFoundException::new); + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UserNotFoundException(username)); - if (!user.getPassword().equals(password)) { - throw new PasswordValidException(); - } + if (!user.getPassword().equals(password)) { + throw new IdOrPasswordValidException(); + } - return userMapper.toDto(user); - } + return userMapper.toDto(user); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicBinaryContentService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicBinaryContentService.java index afbe12bf3..97a795142 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicBinaryContentService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicBinaryContentService.java @@ -3,7 +3,7 @@ import com.sprint.mission.discodeit.dto.data.BinaryContentDto; import com.sprint.mission.discodeit.dto.request.BinaryContentCreateRequest; import com.sprint.mission.discodeit.entity.BinaryContent; -import com.sprint.mission.discodeit.exception.BinaryContentNotFoundException; +import com.sprint.mission.discodeit.exception.binaryContent.BinaryContentNotFoundException; import com.sprint.mission.discodeit.mapper.BinaryContentMapper; import com.sprint.mission.discodeit.repository.BinaryContentRepository; import com.sprint.mission.discodeit.service.BinaryContentService; @@ -13,51 +13,50 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.NoSuchElementException; import java.util.UUID; @RequiredArgsConstructor @Service public class BasicBinaryContentService implements BinaryContentService { - private final BinaryContentRepository binaryContentRepository; - private final BinaryContentStorage binaryContentStorage; - private final BinaryContentMapper binaryContentMapper; - - @Transactional - @Override - public BinaryContentDto create(BinaryContentCreateRequest request) { - String fileName = request.fileName(); - byte[] bytes = request.bytes(); - String contentType = request.contentType(); - BinaryContent binaryContent = new BinaryContent( - fileName, - (long) bytes.length, - contentType - ); - binaryContentStorage.put(binaryContent.getId(), bytes); - return binaryContentMapper.toDto(binaryContentRepository.save(binaryContent)); - } - - @Transactional(readOnly = true) - @Override - public BinaryContentDto find(UUID binaryContentId) { - return binaryContentMapper.toDto(binaryContentRepository.findById(binaryContentId) - .orElseThrow(BinaryContentNotFoundException::new)); - } - - @Transactional(readOnly = true) - @Override - public List findAllByIdIn(List binaryContentIds) { - return binaryContentRepository.findAllByIdIn(binaryContentIds).stream().map(binaryContentMapper::toDto).toList(); - } - - @Transactional - @Override - public void delete(UUID binaryContentId) { - if (!binaryContentRepository.existsById(binaryContentId)) { - throw new BinaryContentNotFoundException(); + private final BinaryContentRepository binaryContentRepository; + private final BinaryContentStorage binaryContentStorage; + private final BinaryContentMapper binaryContentMapper; + + @Transactional + @Override + public BinaryContentDto create(BinaryContentCreateRequest request) { + String fileName = request.fileName(); + byte[] bytes = request.bytes(); + String contentType = request.contentType(); + BinaryContent binaryContent = new BinaryContent( + fileName, + (long) bytes.length, + contentType + ); + binaryContentStorage.put(binaryContent.getId(), bytes); + return binaryContentMapper.toDto(binaryContentRepository.save(binaryContent)); + } + + @Transactional(readOnly = true) + @Override + public BinaryContentDto find(UUID binaryContentId) { + return binaryContentMapper.toDto(binaryContentRepository.findById(binaryContentId) + .orElseThrow(() -> new BinaryContentNotFoundException(binaryContentId))); + } + + @Transactional(readOnly = true) + @Override + public List findAllByIdIn(List binaryContentIds) { + return binaryContentRepository.findAllByIdIn(binaryContentIds).stream().map(binaryContentMapper::toDto).toList(); + } + + @Transactional + @Override + public void delete(UUID binaryContentId) { + if (!binaryContentRepository.existsById(binaryContentId)) { + throw new BinaryContentNotFoundException(binaryContentId); + } + binaryContentRepository.deleteById(binaryContentId); } - binaryContentRepository.deleteById(binaryContentId); - } } diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java index a344fb448..ed7020a95 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicChannelService.java @@ -4,10 +4,13 @@ import com.sprint.mission.discodeit.dto.request.PrivateChannelCreateRequest; import com.sprint.mission.discodeit.dto.request.PublicChannelCreateRequest; import com.sprint.mission.discodeit.dto.request.PublicChannelUpdateRequest; -import com.sprint.mission.discodeit.entity.*; -import com.sprint.mission.discodeit.exception.BaseException; -import com.sprint.mission.discodeit.exception.ChannelNotFoundException; -import com.sprint.mission.discodeit.exception.UserNotFoundException; +import com.sprint.mission.discodeit.entity.Channel; +import com.sprint.mission.discodeit.entity.ChannelType; +import com.sprint.mission.discodeit.entity.ReadStatus; +import com.sprint.mission.discodeit.entity.User; +import com.sprint.mission.discodeit.exception.channel.ChannelNotFoundException; +import com.sprint.mission.discodeit.exception.channel.PrivateChannelUpdateDeniedException; +import com.sprint.mission.discodeit.exception.user.UserNotFoundException; import com.sprint.mission.discodeit.mapper.ChannelMapper; import com.sprint.mission.discodeit.repository.ChannelRepository; import com.sprint.mission.discodeit.repository.MessageRepository; @@ -15,112 +18,98 @@ import com.sprint.mission.discodeit.repository.UserRepository; import com.sprint.mission.discodeit.service.ChannelService; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.Instant; -import java.util.*; +import java.util.List; +import java.util.UUID; @RequiredArgsConstructor @Service public class BasicChannelService implements ChannelService { - private final ChannelRepository channelRepository; - private final UserRepository userRepository; - private final ReadStatusRepository readStatusRepository; - private final MessageRepository messageRepository; - private final ChannelMapper channelMapper; - - @Transactional - @Override - public ChannelDto create(PublicChannelCreateRequest request) { - String name = request.name(); - String description = request.description(); - Channel channel = new Channel(ChannelType.PUBLIC, name, description); - - return channelMapper.toDto(channelRepository.save(channel)); - } - - @Transactional - @Override - public ChannelDto create(PrivateChannelCreateRequest request) { - Channel channel = new Channel(ChannelType.PRIVATE, null, null); - Channel createdChannel = channelRepository.save(channel); - - request.participantIds().stream() - .map(userId -> { - User user = userRepository.findById(userId).orElseThrow(UserNotFoundException::new); - return new ReadStatus(user, createdChannel, createdChannel.getCreatedAt()); - }) - .forEach(readStatusRepository::save); - - return channelMapper.toDto(createdChannel); - } - - @Transactional(readOnly = true) - @Override - public ChannelDto find(UUID channelId) { - return channelRepository.findById(channelId) - .map(channelMapper::toDto) - .orElseThrow(ChannelNotFoundException::new); - } - - @Transactional(readOnly = true) - @Override - public List findAllByUserId(UUID userId) { - List mySubscribedChannelIds = readStatusRepository.findAllByUserId(userId).stream() - .map(ReadStatus::getChannel) - .map(Channel::getId) - .toList(); - - return channelRepository.findAll().stream() - .filter(channel -> - channel.getType().equals(ChannelType.PUBLIC) - || mySubscribedChannelIds.contains(channel.getId()) - ) - .map(channelMapper::toDto) - .toList(); - } - - @Transactional - @Override - public ChannelDto update(UUID channelId, PublicChannelUpdateRequest request) { - String newName = request.newName(); - String newDescription = request.newDescription(); - Channel channel = channelRepository.findById(channelId) - .orElseThrow(ChannelNotFoundException::new); - if (channel.getType().equals(ChannelType.PRIVATE)) { - throw new BaseException() { - @Override - public String getCode() { - return "PUP"; - } + private final ChannelRepository channelRepository; + private final UserRepository userRepository; + private final ReadStatusRepository readStatusRepository; + private final MessageRepository messageRepository; + private final ChannelMapper channelMapper; - @Override - public String getMessage() { - return "Private channel cannot be updated: " + channelId; - } + @Transactional + @Override + public ChannelDto create(PublicChannelCreateRequest request) { + String name = request.name(); + String description = request.description(); + Channel channel = new Channel(ChannelType.PUBLIC, name, description); + + return channelMapper.toDto(channelRepository.save(channel)); + } - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.BAD_REQUEST; + @Transactional + @Override + public ChannelDto create(PrivateChannelCreateRequest request) { + Channel channel = new Channel(ChannelType.PRIVATE, null, null); + Channel createdChannel = channelRepository.save(channel); + + request.participantIds().stream() + .map(userId -> { + User user = userRepository.findById(userId).orElseThrow(() -> new UserNotFoundException(userId)); + return new ReadStatus(user, createdChannel, createdChannel.getCreatedAt()); + }) + .forEach(readStatusRepository::save); + + return channelMapper.toDto(createdChannel); + } + + @Transactional(readOnly = true) + @Override + public ChannelDto find(UUID channelId) { + return channelRepository.findById(channelId) + .map(channelMapper::toDto) + .orElseThrow(() -> new ChannelNotFoundException(channelId)); + } + + @Transactional(readOnly = true) + @Override + public List findAllByUserId(UUID userId) { + userRepository.findById(userId).orElseThrow(() -> new UserNotFoundException(userId)); + + List mySubscribedChannelIds = readStatusRepository.findAllByUserId(userId).stream() + .map(ReadStatus::getChannel) + .map(Channel::getId) + .toList(); + + return channelRepository.findAll().stream() + .filter(channel -> + channel.getType().equals(ChannelType.PUBLIC) + || mySubscribedChannelIds.contains(channel.getId()) + ) + .map(channelMapper::toDto) + .toList(); + } + + @Transactional + @Override + public ChannelDto update(UUID channelId, PublicChannelUpdateRequest request) { + String newName = request.newName(); + String newDescription = request.newDescription(); + Channel channel = channelRepository.findById(channelId) + .orElseThrow(() -> new ChannelNotFoundException(channelId)); + if (channel.getType().equals(ChannelType.PRIVATE)) { + throw new PrivateChannelUpdateDeniedException(channelId); } - }; + channel.update(newName, newDescription); + return channelMapper.toDto(channelRepository.save(channel)); } - channel.update(newName, newDescription); - return channelMapper.toDto(channelRepository.save(channel)); - } - @Transactional - @Override - public void delete(UUID channelId) { - Channel channel = channelRepository.findById(channelId).orElseThrow(ChannelNotFoundException::new); + @Transactional + @Override + public void delete(UUID channelId) { + Channel channel = channelRepository.findById(channelId).orElseThrow(() -> new ChannelNotFoundException(channelId)); - messageRepository.deleteAllByChannelId(channel.getId()); - readStatusRepository.deleteAllByChannelId(channel.getId()); + messageRepository.deleteAllByChannelId(channel.getId()); + readStatusRepository.deleteAllByChannelId(channel.getId()); - channelRepository.deleteById(channelId); - } + channelRepository.deleteById(channelId); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicMessageService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicMessageService.java index b715becf8..cd658dab7 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicMessageService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicMessageService.java @@ -5,10 +5,13 @@ import com.sprint.mission.discodeit.dto.request.MessageCreateRequest; import com.sprint.mission.discodeit.dto.request.MessageUpdateRequest; import com.sprint.mission.discodeit.dto.response.PageResponse; -import com.sprint.mission.discodeit.entity.*; -import com.sprint.mission.discodeit.exception.ChannelNotFoundException; -import com.sprint.mission.discodeit.exception.MessageNotFoundException; -import com.sprint.mission.discodeit.exception.UserNotFoundException; +import com.sprint.mission.discodeit.entity.BinaryContent; +import com.sprint.mission.discodeit.entity.Channel; +import com.sprint.mission.discodeit.entity.Message; +import com.sprint.mission.discodeit.entity.User; +import com.sprint.mission.discodeit.exception.channel.ChannelNotFoundException; +import com.sprint.mission.discodeit.exception.message.MessageNotFoundException; +import com.sprint.mission.discodeit.exception.user.UserNotFoundException; import com.sprint.mission.discodeit.mapper.MessageMapper; import com.sprint.mission.discodeit.mapper.PageResponseMapper; import com.sprint.mission.discodeit.repository.BinaryContentRepository; @@ -18,98 +21,100 @@ import com.sprint.mission.discodeit.service.MessageService; import com.sprint.mission.discodeit.storage.BinaryContentStorage; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.*; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.Instant; -import java.util.ArrayList; import java.util.List; -import java.util.NoSuchElementException; import java.util.UUID; @RequiredArgsConstructor @Service public class BasicMessageService implements MessageService { - private final MessageRepository messageRepository; - private final ChannelRepository channelRepository; - private final UserRepository userRepository; - private final BinaryContentRepository binaryContentRepository; - private final MessageMapper messageMapper; - private final PageResponseMapper pageResponseMapper; - private final BinaryContentStorage binaryContentStorage; - - @Transactional - @Override - public MessageDto create(MessageCreateRequest messageCreateRequest, - List binaryContentCreateRequests) { - Channel channel = channelRepository.findById(messageCreateRequest.channelId()).orElseThrow(ChannelNotFoundException::new); - - User author = userRepository.findById(messageCreateRequest.authorId()).orElseThrow(UserNotFoundException::new); - - Message message = new Message( - messageCreateRequest.content(), - channel, - author - ); - - for (BinaryContentCreateRequest req : binaryContentCreateRequests) { - BinaryContent binaryContent = new BinaryContent( - req.fileName(), - (long) req.bytes().length, - req.contentType() - ); - - message.addAttachment(binaryContent); - BinaryContent savedBinaryContent = binaryContentRepository.save(binaryContent); - binaryContentStorage.put(savedBinaryContent.getId(), req.bytes()); + private final MessageRepository messageRepository; + private final ChannelRepository channelRepository; + private final UserRepository userRepository; + private final BinaryContentRepository binaryContentRepository; + private final MessageMapper messageMapper; + private final PageResponseMapper pageResponseMapper; + private final BinaryContentStorage binaryContentStorage; + + @Transactional + @Override + public MessageDto create(MessageCreateRequest messageCreateRequest, + List binaryContentCreateRequests) { + Channel channel = channelRepository.findById(messageCreateRequest.channelId()).orElseThrow(() -> new ChannelNotFoundException(messageCreateRequest.channelId())); + + User author = userRepository.findById(messageCreateRequest.authorId()).orElseThrow(() -> new UserNotFoundException(messageCreateRequest.authorId())); + + Message message = new Message( + messageCreateRequest.content(), + channel, + author + ); + + for (BinaryContentCreateRequest req : binaryContentCreateRequests) { + BinaryContent binaryContent = new BinaryContent( + req.fileName(), + (long) req.bytes().length, + req.contentType() + ); + + message.addAttachment(binaryContent); + BinaryContent savedBinaryContent = binaryContentRepository.save(binaryContent); + binaryContentStorage.put(savedBinaryContent.getId(), req.bytes()); + } + + return messageMapper.toDto(messageRepository.save(message)); } - return messageMapper.toDto(messageRepository.save(message)); - } - - @Transactional(readOnly = true) - @Override - public MessageDto find(UUID messageId) { - return messageMapper.toDto(messageRepository.findById(messageId) - .orElseThrow(MessageNotFoundException::new) - ); - } - - @Transactional(readOnly = true) - @Override - public PageResponse findAllByChannelId(UUID channelId, Instant cursor, Pageable pageable) { - pageable = PageRequest.of(0, pageable.getPageSize(), Sort.by("createdAt").descending()); - - if (cursor != null) { - return pageResponseMapper.fromSlice(messageRepository.findByChannelIdAndCreatedAtLessThanOrderByCreatedAtDesc(channelId, cursor, pageable) - .map(messageMapper::toDto)); - } else { - return pageResponseMapper.fromSlice(messageRepository.findByChannelIdOrderByCreatedAtDesc(channelId, pageable) - .map(messageMapper::toDto)); + @Transactional(readOnly = true) + @Override + public MessageDto find(UUID messageId) { + return messageMapper.toDto(messageRepository.findById(messageId) + .orElseThrow(() -> new MessageNotFoundException(messageId)) + ); } - } - - @Transactional - @Override - public MessageDto update(UUID messageId, MessageUpdateRequest request) { - String newContent = request.newContent(); - Message message = messageRepository.findById(messageId).orElseThrow(MessageNotFoundException::new); - message.update(newContent); - return messageMapper.toDto(messageRepository.save(message)); - } - - @Transactional - @Override - public void delete(UUID messageId) { - Message message = messageRepository.findById(messageId).orElseThrow(MessageNotFoundException::new); - - message.getAttachments() - .forEach(binaryContent -> - binaryContentRepository.deleteById(binaryContent.getId()) - ); - messageRepository.deleteById(messageId); - } + @Transactional(readOnly = true) + @Override + public PageResponse findAllByChannelId(UUID channelId, Instant cursor, Pageable pageable) { + channelRepository.findById(channelId).orElseThrow(() -> new ChannelNotFoundException(channelId)); + + pageable = PageRequest.of(0, pageable.getPageSize(), Sort.by("createdAt").descending()); + + if (cursor != null) { + return pageResponseMapper.fromSlice(messageRepository.findByChannelIdAndCreatedAtLessThanOrderByCreatedAtDesc(channelId, cursor, pageable) + .map(messageMapper::toDto)); + } else { + return pageResponseMapper.fromSlice(messageRepository.findByChannelIdOrderByCreatedAtDesc(channelId, pageable) + .map(messageMapper::toDto)); + } + } + + @Transactional + @Override + public MessageDto update(UUID messageId, MessageUpdateRequest request) { + String newContent = request.newContent(); + Message message = messageRepository.findById(messageId).orElseThrow(() -> new MessageNotFoundException(messageId)); + message.update(newContent); + return messageMapper.toDto(messageRepository.save(message)); + } + + @Transactional + @Override + public void delete(UUID messageId) { + Message message = messageRepository.findById(messageId).orElseThrow(() -> new MessageNotFoundException(messageId)); + + message.getAttachments() + .forEach(binaryContent -> + binaryContentRepository.deleteById(binaryContent.getId()) + ); + + messageRepository.deleteById(messageId); + } } diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicReadStatusService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicReadStatusService.java index 5ed84b633..0fbb8f8f7 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicReadStatusService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicReadStatusService.java @@ -6,10 +6,10 @@ import com.sprint.mission.discodeit.entity.Channel; import com.sprint.mission.discodeit.entity.ReadStatus; import com.sprint.mission.discodeit.entity.User; -import com.sprint.mission.discodeit.exception.ChannelNotFoundException; -import com.sprint.mission.discodeit.exception.DuplicateReadStatusException; -import com.sprint.mission.discodeit.exception.ReadStatusNotFoundException; -import com.sprint.mission.discodeit.exception.UserNotFoundException; +import com.sprint.mission.discodeit.exception.channel.ChannelNotFoundException; +import com.sprint.mission.discodeit.exception.readStatus.DuplicateReadStatusException; +import com.sprint.mission.discodeit.exception.readStatus.ReadStatusNotFoundException; +import com.sprint.mission.discodeit.exception.user.UserNotFoundException; import com.sprint.mission.discodeit.mapper.ReadStatusMapper; import com.sprint.mission.discodeit.repository.ChannelRepository; import com.sprint.mission.discodeit.repository.ReadStatusRepository; @@ -21,68 +21,67 @@ import java.time.Instant; import java.util.List; -import java.util.NoSuchElementException; import java.util.UUID; @RequiredArgsConstructor @Service public class BasicReadStatusService implements ReadStatusService { - private final ReadStatusRepository readStatusRepository; - private final UserRepository userRepository; - private final ChannelRepository channelRepository; - private final ReadStatusMapper readStatusMapper; + private final ReadStatusRepository readStatusRepository; + private final UserRepository userRepository; + private final ChannelRepository channelRepository; + private final ReadStatusMapper readStatusMapper; - @Transactional - @Override - public ReadStatusDto create(ReadStatusCreateRequest request) { - User user = userRepository.findById(request.userId()).orElseThrow(UserNotFoundException::new); + @Transactional + @Override + public ReadStatusDto create(ReadStatusCreateRequest request) { + User user = userRepository.findById(request.userId()).orElseThrow(() -> new UserNotFoundException(request.userId())); - Channel channel = channelRepository.findById(request.channelId()).orElseThrow(ChannelNotFoundException::new); + Channel channel = channelRepository.findById(request.channelId()).orElseThrow(() -> new ChannelNotFoundException(request.channelId())); - if (readStatusRepository.findAllByUserId(user.getId()).stream() - .anyMatch(readStatus -> readStatus.getChannel().getId().equals(channel.getId()))) { - throw new DuplicateReadStatusException(); + if (readStatusRepository.findAllByUserId(user.getId()).stream() + .anyMatch(readStatus -> readStatus.getChannel().getId().equals(channel.getId()))) { + throw new DuplicateReadStatusException(user.getId(), channel.getId()); + } + + ReadStatus readStatus = new ReadStatus(user, channel, request.lastReadAt()); + return readStatusMapper.toDto(readStatusRepository.save(readStatus)); } - ReadStatus readStatus = new ReadStatus(user, channel, request.lastReadAt()); - return readStatusMapper.toDto(readStatusRepository.save(readStatus)); - } + @Transactional(readOnly = true) + @Override + public ReadStatusDto find(UUID readStatusId) { + return readStatusMapper.toDto(readStatusRepository.findById(readStatusId).orElseThrow(() -> new ReadStatusNotFoundException(readStatusId)) + ); + } - @Transactional(readOnly = true) - @Override - public ReadStatusDto find(UUID readStatusId) { - return readStatusMapper.toDto(readStatusRepository.findById(readStatusId).orElseThrow(ReadStatusNotFoundException::new) - ); - } + @Transactional(readOnly = true) + @Override + public List findAllByUserId(UUID userId) { + return readStatusRepository.findAllByUserId(userId).stream().map(readStatusMapper::toDto).toList(); + } - @Transactional(readOnly = true) - @Override - public List findAllByUserId(UUID userId) { - return readStatusRepository.findAllByUserId(userId).stream().map(readStatusMapper::toDto).toList(); - } + @Transactional + @Override + public ReadStatusDto update(UUID readStatusId, ReadStatusUpdateRequest request) { + Instant newLastReadAt = request.newLastReadAt(); - @Transactional - @Override - public ReadStatusDto update(UUID readStatusId, ReadStatusUpdateRequest request) { - Instant newLastReadAt = request.newLastReadAt(); + ReadStatus readStatus = readStatusRepository.findById(readStatusId).orElseThrow(() -> new ReadStatusNotFoundException(readStatusId)); - ReadStatus readStatus = readStatusRepository.findById(readStatusId).orElseThrow(ReadStatusNotFoundException::new); + readStatus.update(newLastReadAt); - readStatus.update(newLastReadAt); + return readStatusMapper.toDto(readStatus); + } - return readStatusMapper.toDto(readStatus); - } + @Transactional + @Override + public void delete(UUID readStatusId) { - @Transactional - @Override - public void delete(UUID readStatusId) { + if (!readStatusRepository.existsById(readStatusId)) { + throw new ReadStatusNotFoundException(readStatusId); + } - if (!readStatusRepository.existsById(readStatusId)) { - throw new ReadStatusNotFoundException(); + readStatusRepository.deleteById(readStatusId); } - - readStatusRepository.deleteById(readStatusId); - } } diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java index e46682769..5864fbd20 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserService.java @@ -7,8 +7,8 @@ import com.sprint.mission.discodeit.entity.BinaryContent; import com.sprint.mission.discodeit.entity.User; import com.sprint.mission.discodeit.entity.UserStatus; -import com.sprint.mission.discodeit.exception.BaseException; -import com.sprint.mission.discodeit.exception.UserNotFoundException; +import com.sprint.mission.discodeit.exception.user.DuplicateUserException; +import com.sprint.mission.discodeit.exception.user.UserNotFoundException; import com.sprint.mission.discodeit.mapper.UserMapper; import com.sprint.mission.discodeit.repository.BinaryContentRepository; import com.sprint.mission.discodeit.repository.UserRepository; @@ -16,13 +16,11 @@ import com.sprint.mission.discodeit.service.UserService; import com.sprint.mission.discodeit.storage.BinaryContentStorage; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.util.List; -import java.util.NoSuchElementException; import java.util.Optional; import java.util.UUID; @@ -30,153 +28,120 @@ @Service public class BasicUserService implements UserService { - private final UserRepository userRepository; - private final BinaryContentRepository binaryContentRepository; - private final UserStatusRepository userStatusRepository; - private final UserMapper userMapper; - private final BinaryContentStorage binaryContentStorage; - - @Transactional - @Override - public UserDto create(UserCreateRequest userCreateRequest, - Optional optionalProfileCreateRequest) { - String username = userCreateRequest.username(); - String email = userCreateRequest.email(); - - valid(username, email); - - BinaryContent profile = optionalProfileCreateRequest - .map(profileRequest -> { - String fileName = profileRequest.fileName(); - String contentType = profileRequest.contentType(); - byte[] bytes = profileRequest.bytes(); - BinaryContent binaryContent = new BinaryContent( - fileName, - (long) bytes.length, - contentType - ); - BinaryContent saveBinaryContent = binaryContentRepository.save(binaryContent); - binaryContentStorage.put(saveBinaryContent.getId(), bytes); - return saveBinaryContent; - }) - .orElse(null); - String password = userCreateRequest.password(); - - User user = new User(username, email, password, profile); - User createdUser = userRepository.save(user); - - Instant now = Instant.now(); - UserStatus userStatus = new UserStatus(createdUser, now); - - createdUser.setUserStatus(userStatus); - - return userMapper.toDto(createdUser); - } - - @Transactional(readOnly = true) - @Override - public UserDto find(UUID userId) { - return userRepository.findById(userId) - .map(userMapper::toDto) - .orElseThrow(UserNotFoundException::new); - } - - @Transactional(readOnly = true) - @Override - public List findAll() { - return userRepository.findAll() - .stream() - .map(userMapper::toDto) - .toList(); - } - - @Transactional - @Override - public UserDto update(UUID userId, UserUpdateRequest userUpdateRequest, - Optional optionalProfileCreateRequest) { - User user = userRepository.findById(userId).orElseThrow(UserNotFoundException::new); - - String newUsername = userUpdateRequest.newUsername(); - String newEmail = userUpdateRequest.newEmail(); - valid(newUsername, newEmail); - - BinaryContent userProfile = user.getProfile(); - - BinaryContent nullableProfile = optionalProfileCreateRequest.map(profileRequest -> { - Optional.ofNullable(userProfile) - .map(BinaryContent::getId) - .ifPresent(binaryContentRepository::deleteById); - - String fileName = profileRequest.fileName(); - String contentType = profileRequest.contentType(); - byte[] bytes = profileRequest.bytes(); - - BinaryContent binaryContent = new BinaryContent( - fileName, - (long) bytes.length, - contentType - ); - binaryContentRepository.save(binaryContent); - binaryContentStorage.put(binaryContent.getId(), bytes); - return binaryContent; - }).orElse(null); - - String newPassword = userUpdateRequest.newPassword(); - user.update(newUsername, newEmail, newPassword, nullableProfile); - - return userMapper.toDto(user); - } - - @Transactional - @Override - public void delete(UUID userId) { - User user = userRepository.findById(userId).orElseThrow(UserNotFoundException::new); - - Optional.ofNullable(user.getProfile()) - .ifPresent(profile -> { - binaryContentRepository.deleteById(profile.getId()); - binaryContentStorage.put(profile.getId(), null); - }); - - userRepository.deleteById(userId); - } - - private void valid(String newUsername, String newEmail) { - if (userRepository.existsByEmail(newEmail)) { - throw new BaseException() { - @Override - public String getCode() { - return "Duplicate Email"; - } + private final UserRepository userRepository; + private final BinaryContentRepository binaryContentRepository; + private final UserStatusRepository userStatusRepository; + private final UserMapper userMapper; + private final BinaryContentStorage binaryContentStorage; + + @Transactional + @Override + public UserDto create(UserCreateRequest userCreateRequest, + Optional optionalProfileCreateRequest) { + String username = userCreateRequest.username(); + String email = userCreateRequest.email(); + + valid(username, email); + + BinaryContent profile = optionalProfileCreateRequest + .map(profileRequest -> { + String fileName = profileRequest.fileName(); + String contentType = profileRequest.contentType(); + byte[] bytes = profileRequest.bytes(); + BinaryContent binaryContent = new BinaryContent( + fileName, + (long) bytes.length, + contentType + ); + BinaryContent saveBinaryContent = binaryContentRepository.save(binaryContent); + binaryContentStorage.put(saveBinaryContent.getId(), bytes); + return saveBinaryContent; + }) + .orElse(null); + String password = userCreateRequest.password(); + + User user = new User(username, email, password, profile); + User createdUser = userRepository.save(user); + + Instant now = Instant.now(); + UserStatus userStatus = new UserStatus(createdUser, now); + + createdUser.setUserStatus(userStatus); + + return userMapper.toDto(createdUser); + } - @Override - public String getMessage() { - return "Duplicate email: " + newEmail; - } + @Transactional(readOnly = true) + @Override + public UserDto find(UUID userId) { + return userRepository.findById(userId) + .map(userMapper::toDto) + .orElseThrow(() -> new UserNotFoundException(userId)); + } - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.CONFLICT; - } - }; + @Transactional(readOnly = true) + @Override + public List findAll() { + return userRepository.findAll() + .stream() + .map(userMapper::toDto) + .toList(); } - if (userRepository.existsByUsername(newUsername)) { - throw new BaseException() { - @Override - public String getCode() { - return "Duplicate Username"; - } - @Override - public String getMessage() { - return "Duplicate username: " + newUsername; - } + @Transactional + @Override + public UserDto update(UUID userId, UserUpdateRequest userUpdateRequest, + Optional optionalProfileCreateRequest) { + User user = userRepository.findById(userId).orElseThrow(() -> new UserNotFoundException(userId)); + + String newUsername = userUpdateRequest.newUsername(); + String newEmail = userUpdateRequest.newEmail(); + valid(newUsername, newEmail); + + BinaryContent userProfile = user.getProfile(); + + BinaryContent nullableProfile = optionalProfileCreateRequest.map(profileRequest -> { + Optional.ofNullable(userProfile) + .map(BinaryContent::getId) + .ifPresent(binaryContentRepository::deleteById); + + String fileName = profileRequest.fileName(); + String contentType = profileRequest.contentType(); + byte[] bytes = profileRequest.bytes(); + + BinaryContent binaryContent = new BinaryContent( + fileName, + (long) bytes.length, + contentType + ); + binaryContentRepository.save(binaryContent); + binaryContentStorage.put(binaryContent.getId(), bytes); + return binaryContent; + }).orElse(null); + + String newPassword = userUpdateRequest.newPassword(); + user.update(newUsername, newEmail, newPassword, nullableProfile); + + return userMapper.toDto(user); + } + + @Transactional + @Override + public void delete(UUID userId) { + User user = userRepository.findById(userId).orElseThrow(() -> new UserNotFoundException(userId)); + + Optional.ofNullable(user.getProfile()) + .ifPresent(profile -> { + binaryContentRepository.deleteById(profile.getId()); + binaryContentStorage.put(profile.getId(), null); + }); + + userRepository.deleteById(userId); + } - @Override - public HttpStatus getHttpStatus() { - return HttpStatus.CONFLICT; + private void valid(String newUsername, String newEmail) { + if (userRepository.existsByEmail(newEmail) || userRepository.existsByUsername(newUsername)) { + throw new DuplicateUserException(newUsername, newEmail); } - }; } - } } diff --git a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusService.java b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusService.java index a49b17542..d77ec3d6c 100644 --- a/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusService.java +++ b/src/main/java/com/sprint/mission/discodeit/service/basic/BasicUserStatusService.java @@ -5,9 +5,9 @@ import com.sprint.mission.discodeit.dto.request.UserStatusUpdateRequest; import com.sprint.mission.discodeit.entity.User; import com.sprint.mission.discodeit.entity.UserStatus; -import com.sprint.mission.discodeit.exception.DuplicateUserStatusException; -import com.sprint.mission.discodeit.exception.UserNotFoundException; -import com.sprint.mission.discodeit.exception.UserStatusNotFoundException; +import com.sprint.mission.discodeit.exception.user.UserNotFoundException; +import com.sprint.mission.discodeit.exception.userStatus.DuplicateUserStatusException; +import com.sprint.mission.discodeit.exception.userStatus.UserStatusNotFoundException; import com.sprint.mission.discodeit.mapper.UserStatusMapper; import com.sprint.mission.discodeit.repository.UserRepository; import com.sprint.mission.discodeit.repository.UserStatusRepository; @@ -18,70 +18,68 @@ import java.time.Instant; import java.util.List; -import java.util.NoSuchElementException; -import java.util.Optional; import java.util.UUID; @RequiredArgsConstructor @Service public class BasicUserStatusService implements UserStatusService { - private final UserStatusRepository userStatusRepository; - private final UserRepository userRepository; - private final UserStatusMapper userStatusMapper; + private final UserStatusRepository userStatusRepository; + private final UserRepository userRepository; + private final UserStatusMapper userStatusMapper; - @Transactional - @Override - public UserStatus create(UserStatusCreateRequest request) { - User user = userRepository.findById(request.userId()).orElseThrow(UserNotFoundException::new); + @Transactional + @Override + public UserStatus create(UserStatusCreateRequest request) { + User user = userRepository.findById(request.userId()).orElseThrow(() -> new UserNotFoundException(request.userId())); - if (userStatusRepository.findByUserId(user.getId()).isPresent()) { - throw new DuplicateUserStatusException(); - } + if (userStatusRepository.findByUserId(user.getId()).isPresent()) { + throw new DuplicateUserStatusException(user.getUserStatus().getId()); + } - Instant lastActiveAt = request.lastActiveAt(); - UserStatus userStatus = new UserStatus(user, lastActiveAt); - return userStatusRepository.save(userStatus); - } + Instant lastActiveAt = request.lastActiveAt(); + UserStatus userStatus = new UserStatus(user, lastActiveAt); + return userStatusRepository.save(userStatus); + } - @Transactional(readOnly = true) - @Override - public UserStatus find(UUID userStatusId) { - return userStatusRepository.findById(userStatusId).orElseThrow(UserStatusNotFoundException::new); - } + @Transactional(readOnly = true) + @Override + public UserStatus find(UUID userStatusId) { + return userStatusRepository.findById(userStatusId).orElseThrow(() -> new UserNotFoundException(userStatusId)); + } - @Transactional(readOnly = true) - @Override - public List findAll() { - return userStatusRepository.findAll(); - } + @Transactional(readOnly = true) + @Override + public List findAll() { + return userStatusRepository.findAll(); + } - @Transactional - @Override - public UserStatusDto update(UUID userStatusId, UserStatusUpdateRequest request) { + @Transactional + @Override + public UserStatusDto update(UUID userStatusId, UserStatusUpdateRequest request) { - UserStatus userStatus = userStatusRepository.findById(userStatusId).orElseThrow(UserStatusNotFoundException::new); - userStatus.update(request.newLastActiveAt()); + UserStatus userStatus = userStatusRepository.findById(userStatusId).orElseThrow(() -> new UserNotFoundException(userStatusId)); + userStatus.update(request.newLastActiveAt()); - return userStatusMapper.toDto(userStatus); - } + return userStatusMapper.toDto(userStatus); + } - @Transactional - @Override - public UserStatusDto updateByUserId(UUID userId, UserStatusUpdateRequest request) { + @Transactional + @Override + public UserStatusDto updateByUserId(UUID userId, UserStatusUpdateRequest request) { - UserStatus userStatus = userStatusRepository.findByUserId(userId).orElseThrow(UserStatusNotFoundException::new); - userStatus.update(request.newLastActiveAt()); + UserStatus userStatus = userStatusRepository.findByUserId(userId).orElseThrow(() -> new UserNotFoundException(userId)); + userStatus.update(request.newLastActiveAt()); - return userStatusMapper.toDto(userStatus); - } + return userStatusMapper.toDto(userStatus); + } - @Transactional - @Override - public void delete(UUID userStatusId) { - if (!userStatusRepository.existsById(userStatusId)) { - throw new UserStatusNotFoundException(); + @Transactional + @Override + public void delete(UUID userStatusId) { + if (!userStatusRepository.existsById(userStatusId)) { + throw new UserStatusNotFoundException(userStatusId); + } + userStatusRepository.deleteById(userStatusId); } - userStatusRepository.deleteById(userStatusId); - } } diff --git a/src/main/java/com/sprint/mission/discodeit/storage/BinaryContentStorage.java b/src/main/java/com/sprint/mission/discodeit/storage/BinaryContentStorage.java index a54c4aaba..e35af38c2 100644 --- a/src/main/java/com/sprint/mission/discodeit/storage/BinaryContentStorage.java +++ b/src/main/java/com/sprint/mission/discodeit/storage/BinaryContentStorage.java @@ -8,6 +8,8 @@ public interface BinaryContentStorage { UUID put(UUID id, byte[] bytes); + InputStream get(UUID id); + ResponseEntity download(BinaryContentDto binaryContent); } diff --git a/src/main/java/com/sprint/mission/discodeit/storage/LocalBinaryContentStorage.java b/src/main/java/com/sprint/mission/discodeit/storage/LocalBinaryContentStorage.java index 211f02844..f0a0b67b8 100644 --- a/src/main/java/com/sprint/mission/discodeit/storage/LocalBinaryContentStorage.java +++ b/src/main/java/com/sprint/mission/discodeit/storage/LocalBinaryContentStorage.java @@ -1,8 +1,5 @@ package com.sprint.mission.discodeit.storage; -import com.sprint.mission.discodeit.dto.CodeMessageResponseDto; -import com.sprint.mission.discodeit.dto.ResponseCode; -import com.sprint.mission.discodeit.dto.ResponseMessage; import com.sprint.mission.discodeit.dto.data.BinaryContentDto; import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; @@ -23,7 +20,7 @@ @Component @ConditionalOnProperty(name = "discodeit.storage.type", havingValue = "local") -public class LocalBinaryContentStorage implements BinaryContentStorage{ +public class LocalBinaryContentStorage implements BinaryContentStorage { private final Path ROOT; diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml new file mode 100644 index 000000000..e07a67c0a --- /dev/null +++ b/src/main/resources/application-dev.yaml @@ -0,0 +1,21 @@ +server: + port: 80 + +spring: + datasource: + url: jdbc:postgresql://localhost:5432/discodeit?currentSchema=discodeit + username: discodeit_user + password: discodeit1234 + driver-class-name: org.postgresql.Driver + hikari: + connection-init-sql: SET SESSION hibernate.jdbc.lob.non_contextual_creation=true + jpa: + hibernate: + ddl-auto: create + boot: + admin: + client: + url: http://localhost:9090 +logging: + level: + root: info \ No newline at end of file diff --git a/src/main/resources/application-prod.yaml b/src/main/resources/application-prod.yaml new file mode 100644 index 000000000..f48079bfc --- /dev/null +++ b/src/main/resources/application-prod.yaml @@ -0,0 +1,21 @@ +server: + port: 8080 + +spring: + datasource: + url: jdbc:postgresql://localhost:5432/discodeit?currentSchema=discodeit + username: discodeit_user + password: discodeit1234 + driver-class-name: org.postgresql.Driver + hikari: + connection-init-sql: SET SESSION hibernate.jdbc.lob.non_contextual_creation=true + jpa: + hibernate: + ddl-auto: update + boot: + admin: + client: + url: ${SPRING_BOOT_ADMIN_CLIENT_URL:http://localhost:9090} +logging: + level: + root: info \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index c32586175..04376810e 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,43 +1,63 @@ spring: + profiles: + active: prod application: name: discodeit + boot: + admin: + client: + instance: + name: discodeit servlet: multipart: - maxFileSize: 10MB # 파일 하나의 최대 크기 - maxRequestSize: 30MB # 한 번에 최대 업로드 가능 용량 - datasource: - url: jdbc:postgresql://localhost:5432/discodeit - username: discodeit_user - password: discodeit1234 - driver-class-name: org.postgresql.Driver - # PostgreSQL createClob() 경고 메시지 제거 - hikari: - connection-init-sql: SET SESSION hibernate.jdbc.lob.non_contextual_creation=true - # 1-2. jpa config - jpa: - generate-ddl: false - database: postgresql - database-platform: org.hibernate.dialect.PostgreSQLDialect - properties: - hibernate: - format_sql: true - highlight_sql: true - use_sql_comments: true - default_schema: discodeit - open-in-view: false + maxFileSize: 10MB + maxRequestSize: 30MB discodeit: repository: - type: file # jcf | file + type: file file-directory: .discodeit storage: type: local local: root-path: ./upload -# 2. logging config +management: + endpoints: + web: + exposure: + include: "*" + base-path: /actuator + endpoint: + health: + show-details: always + show-components: always + info: + enabled: true + loggers: + access: unrestricted + info: + env: + enabled: true +info: + app: + name: Discodeit + version: 1.7.0 + spring-boot: 3.5.0 + java-version: 17 + datasource: + url: ${spring.datasource.url} + driver-class-name: ${spring.datasource.driver-class-name} + jpa: + ddl-auto: ${spring.jpa.hibernate.ddl-auto} + storage: + type: local + path: ./upload + multipart: + max-file-size: ${spring.servlet.multipart.max-file-size} + max-request-size: ${spring.servlet.multipart.max-request-size} + logging: level: - # 실행되는 SQL 출력 org.hibernate.SQL: debug - # 바인딩되는 파라미터 출력 - org.hibernate.orm.jdbc.bind: trace \ No newline at end of file + org.hibernate.orm.jdbc.bind: trace + root: info \ No newline at end of file diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 000000000..3532d81c8 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + ${LOG_PATTERN} + + + + + + + + + + ${LOG_PATH}/${LOG_FILE_NAME}.log + + ${FILE_LOG_PATTERN} + UTF-8 + + + ${LOG_PATH}/${LOG_FILE_NAME}.%d{yyyy-MM-dd}.%i.log + 10MB + 30 + 1GB + + + + + + ${LOG_PATH}/${LOG_FILE_NAME}-error.log + + ERROR + ACCEPT + DENY + + + ${FILE_LOG_PATTERN} + UTF-8 + + + ${LOG_PATH}/${LOG_FILE_NAME}-error.%d{yyyy-MM-dd}.%i.log + + 10MB + + 30 + 500MB + + + + + + ${LOG_PATH}/${LOG_FILE_NAME}-business.log + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId:-}] %logger{36} - %msg%n + UTF-8 + + + ${LOG_PATH}/${LOG_FILE_NAME}-business.%d{yyyy-MM-dd}.%i.log + + 10MB + + 90 + 2GB + + + + + + ${LOG_PATH}/${LOG_FILE_NAME}-performance.log + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId:-}] %logger{36} - %msg%n + UTF-8 + + + ${LOG_PATH}/${LOG_FILE_NAME}-performance.%d{yyyy-MM-dd}.%i.log + + 10MB + + 30 + 1GB + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 8f136fe80..854e9c3c1 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -7,16 +7,16 @@ CREATE TABLE tbl_user ( email varchar(100) unique not null, password varchar(60) not null, profile_id uuid, - created_at timestamptz not null, - updated_at timestamptz + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone ); CREATE TABLE tbl_user_status ( id uuid primary key, - last_active_at timestamptz not null, + last_active_at timestamp with time zone NOT NULL, user_id uuid unique, - created_at timestamptz not null, - updated_at timestamptz + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone ); CREATE TABLE tbl_channel ( @@ -24,8 +24,8 @@ CREATE TABLE tbl_channel ( name varchar(100), description varchar(500), type channel_type not null, - created_at timestamptz not null, - updated_at timestamptz + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone ); CREATE TABLE tbl_message ( @@ -33,8 +33,8 @@ CREATE TABLE tbl_message ( content text, channel_id uuid not null, author_id uuid, - created_at timestamptz not null, - updated_at timestamptz + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone ); CREATE TABLE tbl_message_attachment ( @@ -48,16 +48,16 @@ CREATE TABLE tbl_read_status ( last_read_at timestamptz not null, user_id uuid, channel_id uuid, - created_at timestamptz not null, - updated_at timestamptz + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone ); CREATE TABLE tbl_binary_content ( id uuid primary key, file_name varchar(255) not null, size bigint not null, - content_type varchar(100) not null, - created_at timestamptz not null + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone ); ALTER TABLE tbl_user ADD CONSTRAINT fk_user_profile FOREIGN KEY (profile_id) REFERENCES tbl_binary_content(id) ON DELETE SET NULL; diff --git a/src/test/java/com/sprint/mission/discodeit/ChannelServiceIntegrationTest.java b/src/test/java/com/sprint/mission/discodeit/ChannelServiceIntegrationTest.java new file mode 100644 index 000000000..3c2f7a569 --- /dev/null +++ b/src/test/java/com/sprint/mission/discodeit/ChannelServiceIntegrationTest.java @@ -0,0 +1,184 @@ +package com.sprint.mission.discodeit; + +import com.sprint.mission.discodeit.dto.data.ChannelDto; +import com.sprint.mission.discodeit.dto.request.PrivateChannelCreateRequest; +import com.sprint.mission.discodeit.dto.request.PublicChannelCreateRequest; +import com.sprint.mission.discodeit.dto.request.PublicChannelUpdateRequest; +import com.sprint.mission.discodeit.entity.*; +import com.sprint.mission.discodeit.repository.*; +import com.sprint.mission.discodeit.service.ChannelService; +import com.sprint.mission.discodeit.storage.BinaryContentStorage; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.*; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + +@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureMockMvc +@DisplayName("채널 서비스 통합 테스트") +@Transactional +public class ChannelServiceIntegrationTest { + + @Autowired private ChannelService channelService; + @Autowired private UserRepository userRepository; + @Autowired private ChannelRepository channelRepository; + @Autowired private ReadStatusRepository readStatusRepository; + @Autowired private UserStatusRepository userStatusRepository; + @Autowired private BinaryContentRepository binaryContentRepository; + @Autowired private BinaryContentStorage binaryContentStorage; + @Autowired private MessageRepository messageRepository; + @Autowired private MockMvc mockMvc; + + @Test + @DisplayName("PUBLIC 채널 생성 요청 시 모든 계층에서 올바르게 동작해야 한다") + void createPublicChannelProcessIntegration() throws Exception { + // given + String json = """ + { + "name": "test-channel", + "description": "테스트 설명" + } + """; + + // when & then + mockMvc.perform(post("/api/channels/public") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.name").value("test-channel")) + .andExpect(jsonPath("$.type").value("PUBLIC")); + + List found = channelRepository.findAll(); + + Channel savedChannel = found.get(0); + assertThat(savedChannel.getName()).isEqualTo("test-channel"); + assertThat(savedChannel.getDescription()).isEqualTo("테스트 설명"); + assertThat(savedChannel.getType()).isEqualTo(ChannelType.PUBLIC); + } + + /* + * PRIVATE 채널 생성 시 모든 계층에서 정상동작 하는지 검증한다 + * 채널 생성, 참가한 유저들의 ReadStatus 생성 까지의 + * 모든 비즈니스 플로우를 데이터베이스와 함께 검증한다 + * */ + @Test + @DisplayName("PRIVATE 채널 생성 요청 시 모든 계층에서 올바르게 동작해야 한다") + void createPrivateChannelProcessIntegration() throws Exception { + // given + BinaryContent binaryContent = new BinaryContent("test", 100L, "png"); + binaryContentRepository.save(binaryContent); + binaryContentRepository.flush(); + + binaryContentStorage.put(binaryContent.getId(), "test File".getBytes()); + + BinaryContent findBinaryContent = binaryContentRepository.findById(binaryContent.getId()).orElseThrow(); + + User user = userRepository.save(new User("test", "test@gmail.com", "test1234", findBinaryContent)); + UserStatus userStatus = userStatusRepository.save(new UserStatus(user, Instant.now())); + user.setUserStatus(userStatus); + + userRepository.flush(); + userStatusRepository.flush(); + + String json = """ + { + "participantIds": ["%s"] + } + """.formatted(user.getId()); + + // when & then + mockMvc.perform(post("/api/channels/private") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.type").value("PRIVATE")) + .andExpect(jsonPath("$.participants[0].id").value(user.getId().toString())); + + List channels = channelRepository.findAll(); + assertThat(channels).hasSize(1); + + List statuses = readStatusRepository.findAllByChannelId(channels.get(0).getId()); + assertThat(statuses).hasSize(1); + assertThat(statuses.get(0).getUser().getId()).isEqualTo(user.getId()); + } + + @Test + @DisplayName("PUBLIC 채널 수정 요청 시 모든 계층에서 올바르게 동작해야 한다") + void updatePublicChannelProcessIntegration() throws Exception { + // given: 채널 생성 + ChannelDto channelDto = channelService.create(new PublicChannelCreateRequest("초기명", "초기설명")); + + String json = """ + { + "newName": "수정", + "newDescription": "수정" + } + """; + + // when & then + mockMvc.perform(put("/api/channels/" + channelDto.id()) + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("수정")) + .andExpect(jsonPath("$.id").value(channelDto.id().toString())); + + Channel updated = channelRepository.findById(channelDto.id()).orElseThrow(); + assertThat(updated.getName()).isEqualTo("수정"); + } + + /* + * 채널 삭제 시 모든 메세지를 삭제해아 한다 + * */ + @Test + @DisplayName("채널 삭제 요청 시 모든 계층에서 올바르게 동작해야 한다") + void deleteChannelProcessIntegration() throws Exception { + // given + BinaryContent binaryContent = new BinaryContent("test", 100L, "png"); + binaryContentRepository.saveAndFlush(binaryContent); + + binaryContentStorage.put(binaryContent.getId(), "test File".getBytes()); + + User user = userRepository.save(new User("test", "test@gmail.com", "test1234", binaryContent)); + UserStatus userStatus = userStatusRepository.save(new UserStatus(user, Instant.now())); + user.setUserStatus(userStatus); + userRepository.flush(); + userStatusRepository.flush(); + + Channel channel = channelRepository.saveAndFlush(new Channel(ChannelType.PUBLIC, "test", "test")); + + messageRepository.saveAll(List.of( + new Message("test1", channel, user), + new Message("test2", channel, user), + new Message("test3", channel, user) + )); + messageRepository.flush(); + + // when + mockMvc.perform(delete("/api/channels/{channelId}", channel.getId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + // then + assertThat(channelRepository.findById(channel.getId())).isEmpty(); + assertThat(messageRepository.findAllByChannelId(channel.getId())).isEmpty(); + } +} diff --git a/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java b/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java index 3a987a214..581c1564d 100644 --- a/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java +++ b/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java @@ -1,6 +1,7 @@ package com.sprint.mission.discodeit; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest diff --git a/src/test/java/com/sprint/mission/discodeit/MessageServiceIntegrationTest.java b/src/test/java/com/sprint/mission/discodeit/MessageServiceIntegrationTest.java new file mode 100644 index 000000000..32d7ea8f4 --- /dev/null +++ b/src/test/java/com/sprint/mission/discodeit/MessageServiceIntegrationTest.java @@ -0,0 +1,156 @@ +package com.sprint.mission.discodeit; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sprint.mission.discodeit.dto.request.MessageUpdateRequest; +import com.sprint.mission.discodeit.entity.*; +import com.sprint.mission.discodeit.repository.*; +import com.sprint.mission.discodeit.storage.BinaryContentStorage; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.List; + + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; + + +@ActiveProfiles("test") +@AutoConfigureMockMvc +@SpringBootTest +@DisplayName("메세지 서비스 통합 테스트") +@Transactional +public class MessageServiceIntegrationTest { + + @Autowired private BinaryContentRepository binaryContentRepository; + @Autowired private UserStatusRepository userStatusRepository; + @Autowired private UserRepository userRepository; + @Autowired private MessageRepository messageRepository; + @Autowired private ChannelRepository channelRepository; + @Autowired private BinaryContentStorage binaryContentStorage; + @Autowired private MockMvc mockMvc; + @Autowired private ObjectMapper objectMapper; + + private User setupUser() { + BinaryContent binaryContent = new BinaryContent("test", 100L, "png"); + binaryContentRepository.saveAndFlush(binaryContent); + binaryContentStorage.put(binaryContent.getId(), "test File".getBytes()); + User user = new User("tester", "tester@example.com", "password1234", binaryContent); + UserStatus userStatus = userStatusRepository.save(new UserStatus(user, Instant.now())); + user.setUserStatus(userStatus); + return userRepository.save(user); + } + + private Channel setupChannel() { + return channelRepository.saveAndFlush(new Channel(ChannelType.PUBLIC, "test", "desc")); + } + + @Test + @DisplayName("메시지를 생성할 수 있어야 한다") + void createMessage() throws Exception { + // given + User user = setupUser(); + Channel channel = setupChannel(); + + String messageCreateRequestJson = String.format(""" + { + "content": "hello world", + "channelId": "%s", + "authorId": "%s" + } + """, channel.getId(), user.getId()); + + MockMultipartFile jsonPart = new MockMultipartFile( + "messageCreateRequest", + null, + MediaType.APPLICATION_JSON_VALUE, + messageCreateRequestJson.getBytes() + ); + + MockMultipartFile filePart = new MockMultipartFile( + "attachments", + "test.txt", + MediaType.TEXT_PLAIN_VALUE, + "file content".getBytes() + ); + + // when & then + mockMvc.perform(multipart("/api/messages") + .file(jsonPart) + .file(filePart) + .contentType(MediaType.MULTIPART_FORM_DATA) + ) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.content").value("hello world")); + + Message saved = messageRepository.findAll().get(0); + assertThat(saved.getContent()).isEqualTo("hello world"); + assertThat(saved.getChannel().getId()).isEqualTo(channel.getId()); + assertThat(saved.getAuthor().getId()).isEqualTo(user.getId()); + } + + @Test + @DisplayName("메시지를 수정할 수 있어야 한다") + void updateMessage() throws Exception { + User user = setupUser(); + Channel channel = setupChannel(); + Message message = messageRepository.save(new Message("old content", channel, user)); + MessageUpdateRequest update = new MessageUpdateRequest("new content"); + + mockMvc.perform(patch("/api/messages/{id}", message.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(update))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").value("new content")); + + Message updated = messageRepository.findById(message.getId()).orElseThrow(); + assertThat(updated.getContent()).isEqualTo("new content"); + } + + @Test + @DisplayName("메시지를 삭제할 수 있어야 한다") + void deleteMessage() throws Exception { + User user = setupUser(); + Channel channel = setupChannel(); + Message message = messageRepository.save(new Message("to delete", channel, user)); + + mockMvc.perform(delete("/api/messages/{id}", message.getId())) + .andExpect(status().isNoContent()); + + assertThat(messageRepository.findById(message.getId())).isEmpty(); + } + + @Test + @DisplayName("채널의 메시지 목록을 조회할 수 있어야 한다") + void findAllMessagesByChannel() throws Exception { + // given + User user = setupUser(); + Channel channel = setupChannel(); + + for (int i = 0; i < 5; i++) { + messageRepository.save(new Message("msg" + i, channel, user)); + } + + // when & then + mockMvc.perform(get("/api/messages") + .param("channelId", channel.getId().toString()) + .param("size", "10") + .param("page", "0")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content.length()").value(5)); + + List messages = messageRepository.findAllByChannelId(channel.getId()); + assertThat(messages.size()).isEqualTo(5); + } +} diff --git a/src/test/java/com/sprint/mission/discodeit/UserServiceIntegrationTest.java b/src/test/java/com/sprint/mission/discodeit/UserServiceIntegrationTest.java new file mode 100644 index 000000000..1b369f5b3 --- /dev/null +++ b/src/test/java/com/sprint/mission/discodeit/UserServiceIntegrationTest.java @@ -0,0 +1,105 @@ +package com.sprint.mission.discodeit; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sprint.mission.discodeit.dto.data.UserDto; +import com.sprint.mission.discodeit.dto.request.BinaryContentCreateRequest; +import com.sprint.mission.discodeit.dto.request.UserCreateRequest; +import com.sprint.mission.discodeit.entity.BinaryContent; +import com.sprint.mission.discodeit.entity.User; +import com.sprint.mission.discodeit.entity.UserStatus; +import com.sprint.mission.discodeit.repository.BinaryContentRepository; +import com.sprint.mission.discodeit.repository.UserRepository; +import com.sprint.mission.discodeit.repository.UserStatusRepository; +import com.sprint.mission.discodeit.service.UserService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; + +@ActiveProfiles("test") +@AutoConfigureMockMvc +@SpringBootTest +@DisplayName("유저 서비스 통합 테스트") +public class UserServiceIntegrationTest { + + @Autowired private UserService userService; + @Autowired private UserRepository userRepository; + @Autowired private BinaryContentRepository binaryContentRepository; + @Autowired private UserStatusRepository userStatusRepository; + @Autowired private MockMvc mockMvc; + + /* + * 유저 생성 프로세스가 모든 계층에서 올바르게 동작하는지 검증한다. + * 유서 생성 시 BinaryContent(프로필 이미지), UserStatus 생성 까지의 + * 전체 비즈니스 플로우를 데이터베이스와 함께 테스트한다. + * */ + @Test + @DisplayName("유저 생성 프로세스가 모든 계층에서 올바르게 동작해야 한다") + @Transactional + void completeUserCreateProcessIntegration() throws Exception { + // given + UserCreateRequest request = new UserCreateRequest("test", "test@gmail.com", "test1234"); + + // Jackson으로 JSON 직렬화 + String json = new ObjectMapper().writeValueAsString(request); + + MockMultipartFile jsonPart = new MockMultipartFile( + "userCreateRequest", + "", + "application/json", + json.getBytes(StandardCharsets.UTF_8) + ); + + MockMultipartFile filePart = new MockMultipartFile( + "profile", + "test.txt", + "text/plain", + "test".getBytes(StandardCharsets.UTF_8) + ); + + // when & then + MvcResult result = mockMvc.perform(multipart("/api/users") + .file(jsonPart) + .file(filePart) + .contentType(MediaType.MULTIPART_FORM_DATA)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.username").value("test")) + .andExpect(jsonPath("$.profile.fileName").value("test.txt")) + .andReturn(); + + // then - 응답에서 userId 추출해서 DB 검증 + String responseBody = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode responseJson = objectMapper.readTree(responseBody); + UUID userId = UUID.fromString(responseJson.get("id").asText()); + UUID profileId = UUID.fromString(responseJson.get("profile").get("id").asText()); + + User user = userRepository.findById(userId).orElseThrow(); + assertThat(user.getUsername()).isEqualTo("test"); + + BinaryContent profile = binaryContentRepository.findById(profileId).orElseThrow(); + assertThat(profile.getFileName()).isEqualTo("test.txt"); + + UserStatus userStatus = userStatusRepository.findByUserId(userId).orElseThrow(); + assertThat(userStatus.getUser().getId()).isEqualTo(userId); + } +} diff --git a/src/test/java/com/sprint/mission/discodeit/config/TestJpaAuditingConfig.java b/src/test/java/com/sprint/mission/discodeit/config/TestJpaAuditingConfig.java new file mode 100644 index 000000000..5d05adc00 --- /dev/null +++ b/src/test/java/com/sprint/mission/discodeit/config/TestJpaAuditingConfig.java @@ -0,0 +1,18 @@ +package com.sprint.mission.discodeit.config; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +import java.util.Optional; +import java.util.UUID; + +@TestConfiguration +@EnableJpaAuditing +public class TestJpaAuditingConfig { + @Bean + public AuditorAware auditorProvider() { + return () -> Optional.of(UUID.randomUUID()); // 테스트용 랜덤 UUID + } +} \ No newline at end of file diff --git a/src/test/java/com/sprint/mission/discodeit/controller/ChannelControllerTest.java b/src/test/java/com/sprint/mission/discodeit/controller/ChannelControllerTest.java new file mode 100644 index 000000000..46acd0ceb --- /dev/null +++ b/src/test/java/com/sprint/mission/discodeit/controller/ChannelControllerTest.java @@ -0,0 +1,118 @@ +package com.sprint.mission.discodeit.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sprint.mission.discodeit.config.TestJpaAuditingConfig; +import com.sprint.mission.discodeit.dto.data.ChannelDto; +import com.sprint.mission.discodeit.dto.request.PrivateChannelCreateRequest; +import com.sprint.mission.discodeit.dto.request.PublicChannelCreateRequest; +import com.sprint.mission.discodeit.dto.request.PublicChannelUpdateRequest; +import com.sprint.mission.discodeit.entity.ChannelType; +import com.sprint.mission.discodeit.exception.channel.ChannelNotFoundException; +import com.sprint.mission.discodeit.service.ChannelService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.AuditorAware; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Optional; +import java.util.UUID; + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ActiveProfiles("test") +@WebMvcTest(ChannelController.class) +@DisplayName("ChannelController 슬라이스 테스트") +public class ChannelControllerTest { + + @Autowired private MockMvc mockMvc; + + @Autowired private ObjectMapper objectMapper; + + @MockitoBean private ChannelService channelService; + + @Test + @DisplayName("PUBLIC 채널 생성 API가 정상적으로 동작한다") + void createPublicChannel_Success() throws Exception { + // given + PublicChannelCreateRequest publicChannelCreateRequest = new PublicChannelCreateRequest("test", "test채널"); + ChannelDto channelDto = new ChannelDto(UUID.randomUUID(), ChannelType.PUBLIC, "test", "test채널", null, null); + + given(channelService.create(publicChannelCreateRequest)).willReturn(channelDto); + + // when & then + mockMvc.perform(post("/api/channels/public") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(publicChannelCreateRequest))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.name").value("test")) + .andExpect(jsonPath("$.type").value(ChannelType.PUBLIC.name())) + .andExpect(jsonPath("$.description").value("test채널")); + } + + @Test + @DisplayName("PRIVATE 채널 생성 API가 정상적으로 동작한다") + void createPrivateChannel_Success() throws Exception { + // given + PrivateChannelCreateRequest privateChannelCreateRequest = new PrivateChannelCreateRequest(null); + ChannelDto channelDto = new ChannelDto(UUID.randomUUID(), ChannelType.PRIVATE, "test", "test채널", null, null); + + given(channelService.create(privateChannelCreateRequest)).willReturn(channelDto); + + // when & then + mockMvc.perform(post("/api/channels/private") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(privateChannelCreateRequest))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.name").value("test")) + .andExpect(jsonPath("$.type").value(ChannelType.PRIVATE.name())) + .andExpect(jsonPath("$.description").value("test채널")); + } + + @Test + @DisplayName("PUBLIC 채널 수정 API가 정상적으로 동작한다") + void updatePublicChannel_Success() throws Exception { + // given + UUID channelId = UUID.randomUUID(); + PublicChannelUpdateRequest publicChannelUpdateRequest = new PublicChannelUpdateRequest("수정한 채널명", "수정한 설명"); + ChannelDto channelDto = new ChannelDto(channelId, ChannelType.PUBLIC, "수정한 채널명", "수정한 설명", null, null); + + given(channelService.update(channelId, publicChannelUpdateRequest)).willReturn(channelDto); + // when & then + mockMvc.perform(put("/api/channels/{channelId}", channelId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(publicChannelUpdateRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name").value("수정한 채널명")) + .andExpect(jsonPath("$.type").value(ChannelType.PUBLIC.name())) + .andExpect(jsonPath("$.description").value("수정한 설명")); + } + + @Test + @DisplayName("존재하지 않는 채널 수정 시 404 반환") + void updateChannel_Fail_NotFound() throws Exception { + // given + UUID nonExistentId = UUID.randomUUID(); + PublicChannelUpdateRequest updateRequest = new PublicChannelUpdateRequest("채널", "설명"); + + given(channelService.update(nonExistentId, updateRequest)) + .willThrow(new ChannelNotFoundException(nonExistentId)); + + // when & then + mockMvc.perform(put("/api/channels/{channelId}", nonExistentId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updateRequest))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status").value(404)); + } + +} diff --git a/src/test/java/com/sprint/mission/discodeit/controller/MessageControllerTest.java b/src/test/java/com/sprint/mission/discodeit/controller/MessageControllerTest.java new file mode 100644 index 000000000..60f3b1cb2 --- /dev/null +++ b/src/test/java/com/sprint/mission/discodeit/controller/MessageControllerTest.java @@ -0,0 +1,102 @@ +package com.sprint.mission.discodeit.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sprint.mission.discodeit.dto.data.MessageDto; +import com.sprint.mission.discodeit.dto.data.UserDto; +import com.sprint.mission.discodeit.dto.request.MessageCreateRequest; +import com.sprint.mission.discodeit.entity.Channel; +import com.sprint.mission.discodeit.entity.ChannelType; +import com.sprint.mission.discodeit.entity.Message; +import com.sprint.mission.discodeit.entity.User; +import com.sprint.mission.discodeit.service.MessageService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.Instant; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ActiveProfiles("test") +@WebMvcTest(MessageController.class) +@DisplayName("MessageController 슬라이스 테스트") +public class MessageControllerTest { + + @Autowired MockMvc mockMvc; + + @Autowired ObjectMapper objectMapper; + + @MockitoBean private MessageService messageService; + + @Test + @DisplayName("메세지 생성 API가 정상적으로 동작 한다") + void createMessage_Success() throws Exception { + // given + UUID channelId = UUID.randomUUID(); + UUID userId = UUID.randomUUID(); + MessageCreateRequest messageCreateRequest = new MessageCreateRequest("test", channelId, userId); + + MessageDto messageDto = new MessageDto( + UUID.randomUUID(), + Instant.now(), + Instant.now(), + "test", + channelId, + new UserDto(userId, "tester", "test@gmail.com", null, null), + null + ); + + given(messageService.create(any(MessageCreateRequest.class), any())).willReturn(messageDto); + + MockMultipartFile requestPart = new MockMultipartFile( + "messageCreateRequest", + "", + "application/json", + objectMapper.writeValueAsBytes(messageCreateRequest) + ); + + // when & then + mockMvc.perform(multipart("/api/messages") + .file(requestPart) + .contentType(MediaType.MULTIPART_FORM_DATA)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.content").value("test")) + .andExpect(jsonPath("$.channelId").value(channelId.toString())) + .andExpect(jsonPath("$.author.id").value(userId.toString())); + } + + @Test + @DisplayName("메세지 생성 API - 필드 유효성 실패로 400 반환") + void createMessage_Fail_InvalidContent() throws Exception { + // given - userId가 비어 있는 경우 + UUID channelId = UUID.randomUUID(); + UUID userId = null; + MessageCreateRequest messageCreateRequest = new MessageCreateRequest("test", channelId, userId); + + MockMultipartFile requestPart = new MockMultipartFile( + "messageCreateRequest", + "", + "application/json", + objectMapper.writeValueAsBytes(messageCreateRequest) + ); + + // when & then + mockMvc.perform(multipart("/api/messages") + .file(requestPart) + .contentType(MediaType.MULTIPART_FORM_DATA)) + .andExpect(status().isBadRequest()); + } +} diff --git a/src/test/java/com/sprint/mission/discodeit/controller/UserControllerTest.java b/src/test/java/com/sprint/mission/discodeit/controller/UserControllerTest.java new file mode 100644 index 000000000..db6b924e8 --- /dev/null +++ b/src/test/java/com/sprint/mission/discodeit/controller/UserControllerTest.java @@ -0,0 +1,87 @@ +package com.sprint.mission.discodeit.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sprint.mission.discodeit.dto.data.UserDto; +import com.sprint.mission.discodeit.dto.request.UserCreateRequest; +import com.sprint.mission.discodeit.service.UserService; +import com.sprint.mission.discodeit.service.UserStatusService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Optional; +import java.util.UUID; + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ActiveProfiles("test") +@WebMvcTest(controllers = UserController.class) +@DisplayName("UserController 슬라이스 테스트") +public class UserControllerTest { + + @Autowired private MockMvc mockMvc; + + @Autowired private ObjectMapper objectMapper; + + @MockitoBean private UserService userService; + @MockitoBean private UserStatusService userStatusService; + + @Test + @DisplayName("유저 생성 API가 정상적으로 동작한다") + void createUser_Success() throws Exception { + // given + UserCreateRequest userCreateRequest = new UserCreateRequest("테스트", "test@gmail.com", "000000000"); + UserDto userDto = new UserDto(UUID.randomUUID(), "테스트", "test@gmail.com", null, null); + + given(userService.create(userCreateRequest, Optional.empty())).willReturn(userDto); + + MockMultipartFile userCreateRequestFile = new MockMultipartFile( + "userCreateRequest", + "", + "application/json", + objectMapper.writeValueAsBytes(userCreateRequest) + ); + + // when & then + mockMvc.perform(multipart("/api/users") + .file(userCreateRequestFile) + .contentType(MediaType.MULTIPART_FORM_DATA)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.username").value("테스트")) + .andExpect(jsonPath("$.email").value("test@gmail.com")); + } + + @Test + @DisplayName("비밀번호 8자 미만일 시 생성 실패") + void createUser_Fail() throws Exception { + // given + UserCreateRequest userCreateRequest = new UserCreateRequest("테스트", "test@gmail.com", "0000000"); + UserDto userDto = new UserDto(UUID.randomUUID(), "테스트", "test@gmail.com", null, null); + + given(userService.create(userCreateRequest, Optional.empty())).willReturn(userDto); + + MockMultipartFile userCreateRequestFile = new MockMultipartFile( + "userCreateRequest", + "", + "application/json", + objectMapper.writeValueAsBytes(userCreateRequest) + ); + + // when & then + mockMvc.perform(multipart("/api/users") + .file(userCreateRequestFile) + .contentType(MediaType.MULTIPART_FORM_DATA)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("잘못된 요청입니다.")) + .andExpect(jsonPath("$.details.message").value(org.hamcrest.Matchers.containsString("password"))); + } +} diff --git a/src/test/java/com/sprint/mission/discodeit/repository/ChannelRepositoryTest.java b/src/test/java/com/sprint/mission/discodeit/repository/ChannelRepositoryTest.java new file mode 100644 index 000000000..be94e40f0 --- /dev/null +++ b/src/test/java/com/sprint/mission/discodeit/repository/ChannelRepositoryTest.java @@ -0,0 +1,35 @@ +package com.sprint.mission.discodeit.repository; + +import com.sprint.mission.discodeit.entity.Channel; +import com.sprint.mission.discodeit.entity.ChannelType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.*; + +@ActiveProfiles("test") +@DataJpaTest +@DisplayName("ChannelRepository 슬라이스 테스트") +public class ChannelRepositoryTest { + + @Autowired + private ChannelRepository channelRepository; + + @Test + @DisplayName("채널을 저장하고 조회할 수 있다") + void saveAndFindChannel() { + // given + Channel channel = new Channel(ChannelType.PRIVATE, "test", "test"); + + // when + Channel result = channelRepository.save(channel); + + // then + assertThat(result).isNotNull(); + assertThat(result.getName()).isEqualTo("test"); + assertThat(result.getDescription()).isEqualTo("test"); + } +} diff --git a/src/test/java/com/sprint/mission/discodeit/repository/MessageRepositoryTest.java b/src/test/java/com/sprint/mission/discodeit/repository/MessageRepositoryTest.java new file mode 100644 index 000000000..ce799c25d --- /dev/null +++ b/src/test/java/com/sprint/mission/discodeit/repository/MessageRepositoryTest.java @@ -0,0 +1,146 @@ +package com.sprint.mission.discodeit.repository; + +import com.sprint.mission.discodeit.config.TestJpaAuditingConfig; +import com.sprint.mission.discodeit.entity.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.test.context.ActiveProfiles; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.List; +import java.util.TimeZone; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.*; + +@ActiveProfiles("test") +@Import(TestJpaAuditingConfig.class) +@DataJpaTest +@EntityScan(basePackages = "com.sprint.mission.discodeit.entity") +@DisplayName("MessageRepository 슬라이스 테스트") +public class MessageRepositoryTest { + @Autowired + private MessageRepository messageRepository; + + @Autowired + private TestEntityManager em; + + private User user; + + private Channel channel; + + @BeforeEach + void setUp() { + // 유저와 연관 엔티티 저장 + BinaryContent profile = new BinaryContent("profile.jpg", 123L, "jpg"); + em.persist(profile); + + user = new User("tester", "tester@example.com", "pw1234", profile); + em.persist(user); + + UserStatus status = new UserStatus(user, Instant.now()); + em.persist(status); + + channel = new Channel(ChannelType.PUBLIC, "general", "일반 채널"); + em.persist(channel); + + System.out.println("셋업" + channel.getId()); + + // 메시지 3개 생성 + for (int i = 0; i < 3; i++) { + Message message = new Message("내용 " + i, channel, user); + em.persist(message); + em.flush(); + try { + Thread.sleep(1500); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + em.clear(); + } + + @BeforeAll + static void beforeAll() { + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + } + + @Test + void save() { + // given + Message message = new Message("test", channel, user); + // when + Message result = messageRepository.save(message); + + // then + assertThat(result).isNotNull(); + assertThat(result.getContent()).isEqualTo("test"); + } + + @Test + @DisplayName("채널 ID로 최신순 메시지 목록을 조회할 수 있다") + void findByChannelIdOrderByCreatedAtDesc() { + // when + Pageable pageable = PageRequest.of(0, 10); + Slice result = messageRepository.findByChannelIdOrderByCreatedAtDesc(channel.getId(), pageable); + + // then + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(3); + assertThat(result.getContent().get(0).getContent()).isEqualTo("내용 2"); + } + + @Test + @DisplayName("커서 기반으로 이전 메시지를 조회할 수 있다") + void findByChannelIdAndCreatedAtLessThanOrderByCreatedAtDesc() { + // given + UUID channelId = channel.getId(); + Pageable pageable = PageRequest.of(0, 10, Sort.by("createdAt").descending()); + List messages = messageRepository + .findByChannelIdOrderByCreatedAtDesc(channelId, pageable) + .getContent(); + + Message cursorTarget = messages.get(0); + Instant cursor = cursorTarget.getCreatedAt(); + cursor = cursor.atOffset(ZoneOffset.UTC).toInstant(); + + // when + Slice result = messageRepository.findByChannelIdAndCreatedAtLessThanOrderByCreatedAtDesc( + channelId, cursor, pageable); + + + // then + assertThat(result.getContent()).hasSize(2); + assertThat(result.getContent().get(0).getContent()).isEqualTo("내용 1"); + } + + @Test + @DisplayName("채널 ID로 모든 메시지를 삭제할 수 있다") + void deleteAllByChannelId() { + // when + messageRepository.deleteAllByChannelId(channel.getId()); + + em.flush(); + em.clear(); + + // then + Pageable pageable = PageRequest.of(0, 10); + Slice result = messageRepository.findByChannelIdOrderByCreatedAtDesc(channel.getId(), pageable); + + assertThat(result.getContent()).isEmpty(); + } +} diff --git a/src/test/java/com/sprint/mission/discodeit/repository/UserRepositoryTest.java b/src/test/java/com/sprint/mission/discodeit/repository/UserRepositoryTest.java new file mode 100644 index 000000000..e872d7f2d --- /dev/null +++ b/src/test/java/com/sprint/mission/discodeit/repository/UserRepositoryTest.java @@ -0,0 +1,103 @@ +package com.sprint.mission.discodeit.repository; + +import com.sprint.mission.discodeit.entity.BinaryContent; +import com.sprint.mission.discodeit.entity.User; +import com.sprint.mission.discodeit.entity.UserStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.test.context.ActiveProfiles; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.*; + + +@ActiveProfiles("test") +@DataJpaTest +@DisplayName("UserRepository 슬라이스 테스트") +public class UserRepositoryTest { + + @Autowired private UserRepository userRepository; + + @Autowired + private TestEntityManager em; + + private User user; + + @BeforeEach + void setUp() { + // given: 연관된 엔티티 먼저 저장 + BinaryContent profile = new BinaryContent("file2", 100L, "png"); + em.persist(profile); + + user = new User("test", "email@test.com", "00000000", profile); + em.persist(user); + + UserStatus status = new UserStatus(user, Instant.now()); + em.persist(status); + + em.flush(); + em.clear(); + } + + @Test + @DisplayName("유저를 저장하고 조회할 수 있다") + void saveAndFindUser() { + // given + User user = new User("testUser", "test@gmail.com", "00000000", null); + + // when + User result = userRepository.save(user); + + // then + assertThat(result).isNotNull(); + assertThat(result.getUsername()).isEqualTo("testUser"); + assertThat(result.getEmail()).isEqualTo("test@gmail.com"); + } + + @Test + @DisplayName("모든 유저를 조회할 수 있다") + void findAllUsers() { + // when + List result = userRepository.findAll(); + + // then + assertThat(result).hasSize(1); + assertThat(result.get(0).getUserStatus()).isNotNull(); + assertThat(result.get(0).getProfile()).isNotNull(); + } + + @Test + @DisplayName("유저 ID를 통해 유저를 조회할 수 있다") + void findUserById() { + // when + user = userRepository.findById(user.getId()).orElse(null); + + // then + assertThat(user).isNotNull(); + assertThat(user.getUsername()).isEqualTo("test"); + } + + @Test + @DisplayName("존재하지 않는 유저 ID 조회 시 실패") + void findUserByNotExistingId() { + // given + UUID uuid = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); + + // when + Optional result = userRepository.findById(uuid); + + // then + assertThat(result).isEmpty(); + } +} diff --git a/src/test/java/com/sprint/mission/discodeit/service/ChannelServiceTest.java b/src/test/java/com/sprint/mission/discodeit/service/ChannelServiceTest.java new file mode 100644 index 000000000..f518abe19 --- /dev/null +++ b/src/test/java/com/sprint/mission/discodeit/service/ChannelServiceTest.java @@ -0,0 +1,245 @@ +package com.sprint.mission.discodeit.service; + +import com.sprint.mission.discodeit.dto.data.ChannelDto; +import com.sprint.mission.discodeit.dto.data.UserDto; +import com.sprint.mission.discodeit.dto.request.PrivateChannelCreateRequest; +import com.sprint.mission.discodeit.dto.request.PublicChannelCreateRequest; +import com.sprint.mission.discodeit.dto.request.PublicChannelUpdateRequest; +import com.sprint.mission.discodeit.entity.Channel; +import com.sprint.mission.discodeit.entity.ChannelType; +import com.sprint.mission.discodeit.entity.ReadStatus; +import com.sprint.mission.discodeit.entity.User; +import com.sprint.mission.discodeit.exception.channel.ChannelNotFoundException; +import com.sprint.mission.discodeit.exception.channel.PrivateChannelUpdateDeniedException; +import com.sprint.mission.discodeit.exception.user.UserNotFoundException; +import com.sprint.mission.discodeit.mapper.ChannelMapper; +import com.sprint.mission.discodeit.repository.ChannelRepository; +import com.sprint.mission.discodeit.repository.MessageRepository; +import com.sprint.mission.discodeit.repository.ReadStatusRepository; +import com.sprint.mission.discodeit.repository.UserRepository; +import com.sprint.mission.discodeit.service.basic.BasicChannelService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.Instant; +import java.util.*; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("채널 서비스 단위테스트") +public class ChannelServiceTest { + + @Mock private ChannelMapper channelMapper; + @Mock private ChannelRepository channelRepository; + @Mock private ReadStatusRepository readStatusRepository; + @Mock private MessageRepository messageRepository; + @Mock private UserRepository userRepository; + + @InjectMocks private BasicChannelService channelService; + + private Channel publicChannel; + private ChannelDto puclicChannelDto; + + @BeforeEach + public void setUp() { + publicChannel = new Channel(ChannelType.PUBLIC, "test Channel", "test Description"); + puclicChannelDto = new ChannelDto( + UUID.randomUUID(), ChannelType.PUBLIC, "test Channel", "test Description", new ArrayList(), null + ); + } + + @Test + @DisplayName("정상적인 요청을 통해 PUBLIC 채널을 생성할 수 있다") + void shouldCreatePublicChannelWhenRequestValid() { + // given + PublicChannelCreateRequest publicChannelCreateRequest = new PublicChannelCreateRequest("new Channel", "new Description"); + + Channel channel = new Channel(ChannelType.PUBLIC, "new Channel", "new Description"); + ChannelDto channelDto = new ChannelDto( + UUID.randomUUID(), ChannelType.PUBLIC, "new Channel", "new Description", new ArrayList(), null + ); + + given(channelRepository.save(any(Channel.class))).willReturn(channel); + given(channelMapper.toDto(any(Channel.class))).willReturn(channelDto); + + // when + ChannelDto result = channelService.create(publicChannelCreateRequest); + + // then + assertThat(result).isNotNull(); + assertThat(result.name()).isEqualTo("new Channel"); + assertThat(result.description()).isEqualTo("new Description"); + assertThat(result.type()).isEqualTo(ChannelType.PUBLIC); + } + + @Test + @DisplayName("정상적인 요청을 통해 PRIVATE 채널을 생성할 수 있다") + void shouldCreatePrivateChannelWhenRequestValid() { + // given + PrivateChannelCreateRequest privateChannelCreateRequest = new PrivateChannelCreateRequest(new ArrayList<>()); + + Channel channel = new Channel(ChannelType.PRIVATE, "private Channel", "private Description"); + ChannelDto channelDto = new ChannelDto( + UUID.randomUUID(), ChannelType.PRIVATE, "private Channel", "private Description", new ArrayList(), null + ); + + given(channelRepository.save(any(Channel.class))).willReturn(channel); + given(channelMapper.toDto(any(Channel.class))).willReturn(channelDto); + + // when + ChannelDto result = channelService.create(privateChannelCreateRequest); + + // then + assertThat(result).isNotNull(); + assertThat(result.name()).isEqualTo("private Channel"); + assertThat(result.description()).isEqualTo("private Description"); + assertThat(result.type()).isEqualTo(ChannelType.PRIVATE); + } + + @Test + @DisplayName("정상적인 요청을 통해 채널을 수정할 수 있다") + void shouldUpdatePublicChannelWhenRequestValid() { + // given + UUID channelId = publicChannel.getId(); + PublicChannelUpdateRequest publicChannelUpdateRequest = new PublicChannelUpdateRequest("new Channel", "new Description"); + + given(channelRepository.findById(channelId)).willReturn(Optional.of(publicChannel)); + given(channelRepository.save(any(Channel.class))).willReturn(publicChannel); + given(channelMapper.toDto(any(Channel.class))).willReturn(puclicChannelDto); + + // when + ChannelDto result = channelService.update(channelId, publicChannelUpdateRequest); + + // then + assertThat(result).isNotNull(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Channel.class); + verify(channelRepository).save(captor.capture()); + Channel updated = captor.getValue(); + + assertThat(updated.getName()).isEqualTo("new Channel"); + assertThat(updated.getDescription()).isEqualTo("new Description"); + } + + @Test + @DisplayName("존재하지 않는 채널 ID로 수정 요청 시 예외 발생") + void shouldNotUpdateWhenRequestInvalid() { + // given + UUID channelId = UUID.randomUUID(); + PublicChannelUpdateRequest publicChannelUpdateRequest = new PublicChannelUpdateRequest("new Channel", "new Description"); + + given(channelRepository.findById(channelId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> channelService.update(channelId, publicChannelUpdateRequest)) + .isInstanceOf(ChannelNotFoundException.class); + } + + @Test + @DisplayName("PRIVATE 채널 수정 요청 시 예외 발생") + void shouldNotUpdateWhenPrivateChannel() { + // given + UUID channelId = UUID.randomUUID(); + Channel privateChannel = new Channel(ChannelType.PRIVATE, "test Channel", "test Description"); + PublicChannelUpdateRequest publicChannelUpdateRequest = new PublicChannelUpdateRequest("new Channel", "new Description"); + + given(channelRepository.findById(channelId)).willReturn(Optional.of(privateChannel)); + + // when & then + assertThatThrownBy(() -> channelService.update(channelId, publicChannelUpdateRequest)) + .isInstanceOf(PrivateChannelUpdateDeniedException.class); + } + + @Test + @DisplayName("정상적인 요청을 통해 채널을 삭제할 수 있다") + void shouldDeleteChannelWhenRequestValid() { + // given + UUID channelId = publicChannel.getId(); + + given(channelRepository.findById(channelId)).willReturn(Optional.of(publicChannel)); + + // when + + channelService.delete(channelId); + + // then + verify(channelRepository).deleteById(channelId); + verify(readStatusRepository).deleteAllByChannelId(channelId); + verify(messageRepository).deleteAllByChannelId(channelId); + } + + @Test + @DisplayName("존재하지 않는 채널 ID로 삭제 요청 시 예외 발생") + void shouldNotDeleteWhenNotFoundChannelId() { + // given + UUID channelId = UUID.randomUUID(); + + given(channelRepository.findById(channelId)).willReturn(Optional.empty()); + // when & then + assertThatThrownBy(() -> channelService.delete(channelId)) + .isInstanceOf(ChannelNotFoundException.class); + } + + @Test + @DisplayName("정상적인 요청을 통해 해당 사용자의 채널 목록을 조회할 수 있다") + void shouldFindAllChannelsByUserId() { + // given + User user = new User("test", "test@gmail.com", "000000001", null); + UUID userId = user.getId(); + + given(userRepository.findById(userId)).willReturn(Optional.of(user)); + + Channel publicChannel = new Channel(ChannelType.PUBLIC, "public", "공개 채널"); + UUID publicChannelId = UUID.randomUUID(); + ReflectionTestUtils.setField(publicChannel, "id", publicChannelId); + + Channel privateChannel = new Channel(ChannelType.PRIVATE, "private", "비공개 채널"); + UUID privateChannelId = UUID.randomUUID(); + ReflectionTestUtils.setField(privateChannel, "id", privateChannelId); + + ReadStatus readStatus = mock(ReadStatus.class); + given(readStatus.getChannel()).willReturn(privateChannel); + + given(channelRepository.findAll()).willReturn(List.of(publicChannel, privateChannel)); + given(readStatusRepository.findAllByUserId(userId)).willReturn(List.of(readStatus)); + + ChannelDto publicDto = new ChannelDto(publicChannelId, ChannelType.PUBLIC, "public", "공개 채널", null, null); + ChannelDto privateDto = new ChannelDto(privateChannelId, ChannelType.PRIVATE, "private", "비공개 채널", null, null); + + given(channelMapper.toDto(publicChannel)).willReturn(publicDto); + given(channelMapper.toDto(privateChannel)).willReturn(privateDto); + + // when + List result = channelService.findAllByUserId(userId); + + // then + assertThat(result).hasSize(2); + assertThat(result).extracting(ChannelDto::type) + .containsExactlyInAnyOrder(ChannelType.PUBLIC, ChannelType.PRIVATE); + + verify(channelRepository).findAll(); + verify(readStatusRepository).findAllByUserId(userId); + } + + @Test + @DisplayName("존재하지 않는 유저 ID로 채널 목록 조회 시 예외 발생") + void shouldNotFindAllChannelsByUserId() { + // given + UUID userId = UUID.randomUUID(); + + given(userRepository.findById(userId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> channelService.findAllByUserId(userId)) + .isInstanceOf(UserNotFoundException.class); + } +} diff --git a/src/test/java/com/sprint/mission/discodeit/service/MessageServiceTest.java b/src/test/java/com/sprint/mission/discodeit/service/MessageServiceTest.java new file mode 100644 index 000000000..93b2f2639 --- /dev/null +++ b/src/test/java/com/sprint/mission/discodeit/service/MessageServiceTest.java @@ -0,0 +1,264 @@ +package com.sprint.mission.discodeit.service; + +import com.sprint.mission.discodeit.dto.data.BinaryContentDto; +import com.sprint.mission.discodeit.dto.data.MessageDto; +import com.sprint.mission.discodeit.dto.data.UserDto; +import com.sprint.mission.discodeit.dto.request.BinaryContentCreateRequest; +import com.sprint.mission.discodeit.dto.request.MessageCreateRequest; +import com.sprint.mission.discodeit.dto.request.MessageUpdateRequest; +import com.sprint.mission.discodeit.dto.response.PageResponse; +import com.sprint.mission.discodeit.entity.*; +import com.sprint.mission.discodeit.exception.channel.ChannelNotFoundException; +import com.sprint.mission.discodeit.exception.message.MessageNotFoundException; +import com.sprint.mission.discodeit.exception.user.UserNotFoundException; +import com.sprint.mission.discodeit.mapper.MessageMapper; +import com.sprint.mission.discodeit.mapper.PageResponseMapper; +import com.sprint.mission.discodeit.repository.BinaryContentRepository; +import com.sprint.mission.discodeit.repository.ChannelRepository; +import com.sprint.mission.discodeit.repository.MessageRepository; +import com.sprint.mission.discodeit.repository.UserRepository; +import com.sprint.mission.discodeit.service.basic.BasicMessageService; +import com.sprint.mission.discodeit.storage.BinaryContentStorage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.domain.Sort; +import org.springframework.test.util.ReflectionTestUtils; + +import org.springframework.data.domain.Pageable; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("메세지 서비스 단위 테스트") +public class MessageServiceTest { + + @Mock UserRepository userRepository; + @Mock ChannelRepository channelRepository; + @Mock BinaryContentRepository binaryContentRepository; + @Mock BinaryContentStorage binaryContentStorage; + @Mock MessageRepository messageRepository; + @Mock MessageMapper messageMapper; + @Mock PageResponseMapper pageResponseMapper; + + @InjectMocks private BasicMessageService messageService; + + + private User user; + private Channel channel; + + @BeforeEach + void setUp() { + user = new User("테스트유저", "test@gmail.com", "00000000", null); + channel = new Channel(ChannelType.PRIVATE, "테스트 채널", "테스트 채널"); + } + + @Test + @DisplayName("정상적인 요청을 통해 메세지를 등록할 수 있다") + void shouldCreateMessageWhenRequestValid() { + // given + UUID authorId = user.getId(); + UUID channelId = channel.getId(); + + MessageCreateRequest messageCreateRequest = new MessageCreateRequest("테스트", authorId, channelId); + + BinaryContentCreateRequest req1 = new BinaryContentCreateRequest("file1", "png", "bytes".getBytes()); + BinaryContentCreateRequest req2 = new BinaryContentCreateRequest("file2", "jpg", "more".getBytes()); + BinaryContentCreateRequest req3 = new BinaryContentCreateRequest("file3", "gif", "data".getBytes()); + + BinaryContentDto dto1 = new BinaryContentDto(UUID.randomUUID(), "file1", 10L, "file1", "test1".getBytes()); + BinaryContentDto dto2 = new BinaryContentDto(UUID.randomUUID(), "file2", 10L, "file2", "test2".getBytes()); + BinaryContentDto dto3 = new BinaryContentDto(UUID.randomUUID(), "file3", 10L, "file3", "test3".getBytes()); + + Message message = new Message("테스트", channel, user); + MessageDto messageDto = new MessageDto(UUID.randomUUID(), Instant.now(), Instant.now(), "테스트", channelId, null, List.of(dto1, dto2, dto3)); + + given(userRepository.findById(authorId)).willReturn(Optional.of(user)); + given(channelRepository.findById(channelId)).willReturn(Optional.of(channel)); + given(binaryContentRepository.save(any(BinaryContent.class))) + .willAnswer(invocation -> invocation.getArgument(0)); + given(messageRepository.save(any(Message.class))).willReturn(message); + given(messageMapper.toDto(any(Message.class))).willReturn(messageDto); + + // when + MessageDto result = messageService.create(messageCreateRequest, List.of(req1, req2, req3)); + + // then + assertThat(result).isNotNull(); + assertThat(result.content()).isEqualTo("테스트"); + assertThat(result.attachments().size()).isEqualTo(3); + assertThat(result.attachments().get(0)).isEqualTo(dto1); + + verify(binaryContentRepository, times(3)).save(any()); + verify(binaryContentStorage, times(3)).put(any(), any()); + verify(messageRepository).save(any(Message.class)); + } + + @Test + @DisplayName("존재하지 않는 채널 ID로 메세지 생성 요청 시 예외 발생") + void shouldNotCreateMessageWhenNotFoundChannel() { + // given + UUID authorId = UUID.fromString("00000000-0000-0000-0000-000000000001"); + UUID channelId = UUID.fromString("00000000-0000-0000-0000-000000000001"); + MessageCreateRequest messageCreateRequest = new MessageCreateRequest("테스트", authorId, channelId); + + BinaryContentCreateRequest req1 = new BinaryContentCreateRequest("file1", "png", "bytes".getBytes()); + BinaryContentCreateRequest req2 = new BinaryContentCreateRequest("file2", "jpg", "more".getBytes()); + BinaryContentCreateRequest req3 = new BinaryContentCreateRequest("file3", "gif", "data".getBytes()); + + given(channelRepository.findById(channelId)).willReturn(Optional.empty()); + + // when * then + assertThatThrownBy(() -> messageService.create(messageCreateRequest, List.of(req1, req2, req3))) + .isInstanceOf(ChannelNotFoundException.class); + } + + @Test + @DisplayName("정상적인 요청을 통해 메세지를 수정할 수 있다") + void shouldUpdateMessageWhenRequestValid() { + // given + UUID authorId = UUID.fromString("00000000-0000-0000-0000-000000000001"); + UUID channelId = UUID.fromString("00000000-0000-0000-0000-000000000001"); + UUID messageId = UUID.fromString("00000000-0000-0000-0000-000000000001"); + + ReflectionTestUtils.setField(user, "id", authorId); + ReflectionTestUtils.setField(channel, "id", channelId); + + MessageUpdateRequest messageUpdateRequest = new MessageUpdateRequest("newContent"); + + UserDto userDto = new UserDto(UUID.randomUUID(), "test", "test@gmail.com", null, null); + Message message = new Message("test", channel, user); + ReflectionTestUtils.setField(message, "id", messageId); + + MessageDto messageDto = new MessageDto(UUID.randomUUID(), null, null, "newContent", channelId, userDto, null); + + given(messageRepository.findById(messageId)).willReturn(Optional.of(message)); + given(messageRepository.save(any(Message.class))).willReturn(message); + given(messageMapper.toDto(message)).willReturn(messageDto); + // when + + MessageDto result = messageService.update(messageId, messageUpdateRequest); + + // then + assertThat(result).isNotNull(); + assertThat(result.content()).isEqualTo("newContent"); + } + + @Test + @DisplayName("존재하지 않는 메세지 ID로 수정 요청 시 예외 발생") + void shouldNotUpdateMessageWhenNotFoundMessageId() { + // given + UUID messageId = UUID.fromString("00000000-0000-0000-0000-000000000001"); + + MessageUpdateRequest messageUpdateRequest = new MessageUpdateRequest("newContent"); + + given(messageRepository.findById(messageId)).willReturn(Optional.empty()); + // when & then + assertThatThrownBy(() -> messageService.update(messageId, messageUpdateRequest)) + .isInstanceOf(MessageNotFoundException.class); + } + + @Test + @DisplayName("정상적인 요청을 통해 메세지를 삭제할 수 있다") + void shouldDeleteMessageWhenRequestValid() { + // given + UUID messageId = UUID.fromString("00000000-0000-0000-0000-000000000001"); + UUID attachmentId = UUID.randomUUID(); + BinaryContent attachment = new BinaryContent("file", 100L, "png"); + ReflectionTestUtils.setField(attachment, "id", attachmentId); // UUID 세팅 + + Message message = new Message("test", channel, user); + message.addAttachment(attachment); + ReflectionTestUtils.setField(message, "id", messageId); + + given(messageRepository.findById(messageId)).willReturn(Optional.of(message)); + + // when + messageService.delete(messageId); + + // then + verify(binaryContentRepository).deleteById(attachmentId); // <- 실제 첨부파일 ID로 + verify(messageRepository).deleteById(messageId); + } + + @Test + @DisplayName("존재하지 않는 메세지 ID로 삭제 요청 시 예외 발생") + void shouldNotDeleteMessageWhenNotFoundMessageId() { + // given + UUID messageId = UUID.fromString("00000000-0000-0000-0000-000000000001"); + + given(messageRepository.findById(messageId)).willReturn(Optional.empty()); + // when & then + assertThatThrownBy(() -> messageService.delete(messageId)) + .isInstanceOf(MessageNotFoundException.class); + } + + @Test + @DisplayName("정상적인 요청을 통해 채널의 메세지 목록을 조회할 수 있다") + void shouldFindMessageListByChannelIdWhenRequestValid() { + // given + UUID channelId = UUID.fromString("00000000-0000-0000-0000-000000000001"); + Instant cursor = null; + Pageable pageable = PageRequest.of(0, 10, Sort.by("createdAt").descending()); + + Message message1 = new Message("msg1", channel, user); + Message message2 = new Message("msg2", channel, user); + + List messages = List.of(message1, message2); + Slice slice = new SliceImpl<>(messages, pageable, false); + + List messageDtos = messages.stream() + .map(msg -> new MessageDto(UUID.randomUUID(), Instant.now(), Instant.now(), msg.getContent(), channelId, null, null)) + .toList(); + + given(channelRepository.findById(channelId)).willReturn(Optional.of(channel)); + given(messageRepository.findByChannelIdOrderByCreatedAtDesc(channelId, pageable)) + .willReturn(slice); + given(messageMapper.toDto(any())).willAnswer(invocation -> { + Message msg = invocation.getArgument(0); + return new MessageDto(UUID.randomUUID(), Instant.now(), Instant.now(), msg.getContent(), channelId, null, null); + }); + + given(pageResponseMapper.fromSlice(any(Slice.class))) + .willReturn(new PageResponse<>(messageDtos, null, messageDtos.size(), false, (long) messageDtos.size())); + + // when + PageResponse result = messageService.findAllByChannelId(channelId, cursor, pageable); + + // then + assertThat(result).isNotNull(); + assertThat(result.content()).hasSize(2); + assertThat(result.hasNext()).isFalse(); + assertThat(result.content()) + .extracting("content") + .containsExactly("msg1", "msg2"); + } + + @Test + @DisplayName("존재하지 않는 채널 ID로 메세지 목록 조회 시 예외 발생") + void shouldFindMessageListByChannelIdWhenNotFoundChannelId() { + // given + UUID channelId = UUID.randomUUID(); + Instant cursor = null; + Pageable pageable = PageRequest.of(0, 10, Sort.by("createdAt").descending()); + + + given(channelRepository.findById(channelId)).willReturn(Optional.empty()); + // when & then + assertThatThrownBy(() -> messageService.findAllByChannelId(channelId, cursor, pageable)) + .isInstanceOf(ChannelNotFoundException.class); + } +} diff --git a/src/test/java/com/sprint/mission/discodeit/service/UserServiceTest.java b/src/test/java/com/sprint/mission/discodeit/service/UserServiceTest.java new file mode 100644 index 000000000..047ee766d --- /dev/null +++ b/src/test/java/com/sprint/mission/discodeit/service/UserServiceTest.java @@ -0,0 +1,192 @@ +package com.sprint.mission.discodeit.service; + +import com.sprint.mission.discodeit.dto.data.BinaryContentDto; +import com.sprint.mission.discodeit.dto.data.UserDto; +import com.sprint.mission.discodeit.dto.request.BinaryContentCreateRequest; +import com.sprint.mission.discodeit.dto.request.UserCreateRequest; +import com.sprint.mission.discodeit.dto.request.UserUpdateRequest; +import com.sprint.mission.discodeit.entity.BinaryContent; +import com.sprint.mission.discodeit.entity.User; +import com.sprint.mission.discodeit.exception.user.DuplicateUserException; +import com.sprint.mission.discodeit.exception.user.UserNotFoundException; +import com.sprint.mission.discodeit.mapper.UserMapper; +import com.sprint.mission.discodeit.repository.BinaryContentRepository; +import com.sprint.mission.discodeit.repository.UserRepository; +import com.sprint.mission.discodeit.service.basic.BasicUserService; +import com.sprint.mission.discodeit.storage.BinaryContentStorage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; +import static org.mockito.ArgumentMatchers.any; + +@ExtendWith(MockitoExtension.class) +@DisplayName("유저 서비스 단위 테스트") +public class UserServiceTest { + + @Mock private UserRepository userRepository; + @Mock private BinaryContentRepository binaryContentRepository; + @Mock private BinaryContentStorage binaryContentStorage; + @Mock private UserMapper userMapper; + + @InjectMocks + private BasicUserService userService; + + private BinaryContent binaryContent; + private BinaryContentCreateRequest profileCreateRequest; + private User user; + private UserDto userDto; + + @BeforeEach + void setUp() { + byte[] profileBytes = "test image data".getBytes(); + + binaryContent = new BinaryContent("test프로필", (long) profileBytes.length, "png"); + + profileCreateRequest = new BinaryContentCreateRequest("test프로필", "png", profileBytes); + + user = new User("테스트유저", "test@gmail.com", "12345678", binaryContent); + + userDto = new UserDto( + UUID.randomUUID(), "테스트유저", "test@gmail.com", + new BinaryContentDto(UUID.randomUUID(), "test프로필", 10L, "png", profileBytes), + null); + + // 유저 상태는 도중에 세팅되므로 무시 가능 + } + + @Test + @DisplayName("정상적인 요청을 통해 유저를 생성할 수 있다") + void shouldCreateUserWhenRequestValid() { + // given + UserCreateRequest userCreateRequest = new UserCreateRequest("테스트유저", "test@gmail.com", "12345678"); + Optional optionalProfile = Optional.of(profileCreateRequest); + + given(binaryContentRepository.save(any(BinaryContent.class))).willReturn(binaryContent); + given(binaryContentStorage.put(any(), any())).willReturn(userDto.profile().id()); + given(userRepository.save(any(User.class))).willReturn(user); + given(userMapper.toDto(any(User.class))).willReturn(userDto); + + // when + UserDto result = userService.create(userCreateRequest, optionalProfile); + + // then + assertThat(result).isNotNull(); + assertThat(result.username()).isEqualTo(userCreateRequest.username()); + assertThat(result.email()).isEqualTo(userCreateRequest.email()); + assertThat(result.profile().fileName()).isEqualTo("test프로필"); + + verify(binaryContentStorage).put(any(), any()); + verify(binaryContentRepository).save(any(BinaryContent.class)); + verify(userRepository).save(any(User.class)); + } + + @Test + @DisplayName("중복된 이메일은 생성할 수 없습니다") + void shouldNotCreateUserWhenRequestInvalid() { + // given + UserCreateRequest userCreateRequest = new UserCreateRequest("테스트유저", "test@gmail.com", "12345678"); + Optional optionalProfile = Optional.of(profileCreateRequest); + + // 기존에 동일 이메일이 이미 존재하는 상황 가정 + given(userRepository.existsByEmail(userCreateRequest.email())).willReturn(true); + + // when & then + assertThatThrownBy(() -> userService.create(userCreateRequest, optionalProfile)) + .isInstanceOf(DuplicateUserException.class); + + } + + @Test + @DisplayName("정상적인 요청을 통해 유저 정보를 수정할 수 있다") + void shouldUpdateUserWhenRequestValid() { + // given + UUID userId = user.getId(); + byte[] profileBytes = "update test image data".getBytes(); + profileCreateRequest = new BinaryContentCreateRequest("update-profile", "png", profileBytes); + + UserUpdateRequest userUpdateRequest = new UserUpdateRequest("update유저", "update@gmail.com", "000000000"); + Optional optionalProfileCreateRequest = Optional.of(profileCreateRequest); + + BinaryContent updatedProfile = new BinaryContent("update-profile", (long) profileBytes.length, "png"); + + given(userRepository.findById(userId)).willReturn(Optional.of(user)); + given(binaryContentRepository.save(any())).willReturn(updatedProfile); + given(binaryContentStorage.put(any(), any())).willReturn(UUID.randomUUID()); + + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + given(userMapper.toDto(userCaptor.capture())).willReturn(userDto); + + // when + UserDto result = userService.update(userId, userUpdateRequest, optionalProfileCreateRequest); + + // then + User updated = userCaptor.getValue(); + assertThat(updated.getUsername()).isEqualTo("update유저"); + assertThat(updated.getEmail()).isEqualTo("update@gmail.com"); + assertThat(updated.getProfile().getFileName()).isEqualTo("update-profile"); + + assertThat(result).isNotNull(); + verify(userRepository).findById(userId); + verify(binaryContentRepository).save(any()); + verify(binaryContentStorage).put(any(), any()); + verify(userMapper).toDto(any()); + } + + @Test + @DisplayName("존재하지 않는 유저 ID로 수정 요청 시 예외가 발생한다") + void shouldNotUpdateUserWhenUserIdNotFound() { + // given + UUID userId = UUID.randomUUID(); + byte[] profileBytes = "update test image data".getBytes(); + profileCreateRequest = new BinaryContentCreateRequest("update-profile", "png", profileBytes); + + UserUpdateRequest userUpdateRequest = new UserUpdateRequest("update유저", "update@gmail.com", "000000000"); + Optional optionalProfileCreateRequest = Optional.of(profileCreateRequest); + + // when * then + assertThatThrownBy(() -> userService.update(userId, userUpdateRequest, optionalProfileCreateRequest)) + .isInstanceOf(UserNotFoundException.class); + + + } + + @Test + @DisplayName("정상적인 요청을 통해 유저를 삭제할 수 있다") + void shouldDeleteUserWhenRequestValid() { + // given + UUID userId = user.getId(); + + given(userRepository.findById(userId)).willReturn(Optional.of(user)); + doNothing().when(binaryContentRepository).deleteById(binaryContent.getId()); + given(binaryContentStorage.put(user.getProfile().getId(), null)).willReturn(UUID.randomUUID()); + // when + + userService.delete(userId); + + // then + verify(userRepository).deleteById(userId); + verify(binaryContentRepository).deleteById(binaryContent.getId()); + verify(binaryContentStorage).put(user.getProfile().getId(), null); + } + + @Test + @DisplayName("존재하지 않는 유저 ID로 삭제 요청 시 예외가 발생한다") + void shouldNotDeleteUserWhenUserIdNotFound() { + // given + UUID userId = UUID.randomUUID(); + + // when & then + assertThatThrownBy(() -> userService.delete(userId)).isInstanceOf(UserNotFoundException.class); + } +} diff --git a/src/test/java/com/sprint/mission/discodeit/service/UserStatusServiceTest.java b/src/test/java/com/sprint/mission/discodeit/service/UserStatusServiceTest.java new file mode 100644 index 000000000..7057974b0 --- /dev/null +++ b/src/test/java/com/sprint/mission/discodeit/service/UserStatusServiceTest.java @@ -0,0 +1,194 @@ +package com.sprint.mission.discodeit.service; + +import com.sprint.mission.discodeit.dto.data.UserStatusDto; +import com.sprint.mission.discodeit.dto.request.UserStatusCreateRequest; +import com.sprint.mission.discodeit.dto.request.UserStatusUpdateRequest; +import com.sprint.mission.discodeit.entity.User; +import com.sprint.mission.discodeit.entity.UserStatus; +import com.sprint.mission.discodeit.exception.user.UserNotFoundException; +import com.sprint.mission.discodeit.exception.userStatus.DuplicateUserStatusException; +import com.sprint.mission.discodeit.exception.userStatus.UserStatusNotFoundException; +import com.sprint.mission.discodeit.mapper.UserStatusMapper; +import com.sprint.mission.discodeit.repository.UserRepository; +import com.sprint.mission.discodeit.repository.UserStatusRepository; +import com.sprint.mission.discodeit.service.basic.BasicUserStatusService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Field; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("유저 상태 서비스 단위 테스트") +public class UserStatusServiceTest { + + @Mock private UserRepository userRepository; + @Mock private UserStatusRepository userStatusRepository; + @Mock private UserStatusMapper userStatusMapper; + + @InjectMocks private BasicUserStatusService userStatusService; + + private User user; + + @BeforeEach + void setUp() { + user = new User("테스트유저", "test@email.com", "0000", null); + } + + @Test + @DisplayName("정상적으로 UserStatus를 생성할 수 있다") + void createUserStatusSuccess() { + // given + UUID userId = user.getId(); + Instant now = Instant.now(); + UserStatusCreateRequest request = new UserStatusCreateRequest(userId, now); + + given(userRepository.findById(userId)).willReturn(Optional.of(user)); + given(userStatusRepository.findByUserId(userId)).willReturn(Optional.empty()); + given(userStatusRepository.save(any())).willAnswer(inv -> inv.getArgument(0)); + + // when + UserStatus result = userStatusService.create(request); + + // then + assertThat(result).isNotNull(); + assertThat(result.getLastActiveAt()).isEqualTo(now); + verify(userStatusRepository).save(any(UserStatus.class)); + } + + @Test + @DisplayName("이미 존재하는 UserStatus가 있는 경우 예외 발생") + void createUserStatusDuplicate() throws NoSuchFieldException, IllegalAccessException { + // given + UUID userId = user.getId(); + Instant now = Instant.now(); + UserStatusCreateRequest request = new UserStatusCreateRequest(userId, now); + + UserStatus existingStatus = new UserStatus(user, now); + + // BaseUpdatableEntity의 id 필드에 접근해서 UUID 설정 + Field idField = UserStatus.class.getSuperclass().getSuperclass().getDeclaredField("id"); + idField.setAccessible(true); + UUID statusId = UUID.randomUUID(); + idField.set(existingStatus, statusId); + + user.setUserStatus(existingStatus); + + given(userRepository.findById(userId)).willReturn(Optional.of(user)); + given(userStatusRepository.findByUserId(userId)).willReturn(Optional.of(existingStatus)); + + // when & then + assertThatThrownBy(() -> userStatusService.create(request)) + .isInstanceOf(DuplicateUserStatusException.class); + } + + @Test + @DisplayName("ID로 UserStatus 조회 - 존재하지 않으면 예외 발생") + void findUserStatusNotFound() { + // given + UUID statusId = UUID.randomUUID(); + given(userStatusRepository.findById(statusId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> userStatusService.find(statusId)) + .isInstanceOf(UserNotFoundException.class); + } + + @Test + @DisplayName("모든 UserStatus 조회") + void findAllUserStatuses() { + // given + List statuses = List.of( + new UserStatus(user, Instant.now()) + ); + given(userStatusRepository.findAll()).willReturn(statuses); + + // when + List result = userStatusService.findAll(); + + // then + assertThat(result).hasSize(1); + verify(userStatusRepository).findAll(); + } + + @Test + @DisplayName("UserStatus ID로 업데이트") + void updateUserStatus() { + // given + UUID userId = user.getId(); + UUID statusId = UUID.randomUUID(); + Instant updated = Instant.now(); + UserStatus userStatus = new UserStatus(user, Instant.now()); + + given(userStatusRepository.findById(statusId)).willReturn(Optional.of(userStatus)); + given(userStatusMapper.toDto(userStatus)).willReturn( + new UserStatusDto(UUID.randomUUID(), userId, updated) + ); + + // when + UserStatusDto dto = userStatusService.update(statusId, new UserStatusUpdateRequest(updated)); + + // then + assertThat(dto).isNotNull(); + assertThat(dto.userId()).isEqualTo(userId); + verify(userStatusRepository).findById(statusId); + } + + @Test + @DisplayName("UserId로 UserStatus 업데이트") + void updateByUserId() { + // given + UUID userId = user.getId(); + Instant updated = Instant.now(); + UserStatus userStatus = new UserStatus(user, Instant.now()); + + given(userStatusRepository.findByUserId(userId)).willReturn(Optional.of(userStatus)); + given(userStatusMapper.toDto(userStatus)).willReturn( + new UserStatusDto(UUID.randomUUID(), userId, updated) + ); + + // when + UserStatusDto dto = userStatusService.updateByUserId(userId, new UserStatusUpdateRequest(updated)); + + // then + assertThat(dto).isNotNull(); + assertThat(dto.userId()).isEqualTo(userId); + } + + @Test + @DisplayName("존재하지 않는 ID 삭제 시 예외 발생") + void deleteNotFound() { + // given + UUID id = UUID.randomUUID(); + given(userStatusRepository.existsById(id)).willReturn(false); + + // when & then + assertThatThrownBy(() -> userStatusService.delete(id)) + .isInstanceOf(UserStatusNotFoundException.class); + } + + @Test + @DisplayName("정상적으로 UserStatus 삭제") + void deleteSuccess() { + // given + UUID id = UUID.randomUUID(); + given(userStatusRepository.existsById(id)).willReturn(true); + + // when + userStatusService.delete(id); + + // then + verify(userStatusRepository).deleteById(id); + } +} diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml new file mode 100644 index 000000000..b6abfafc5 --- /dev/null +++ b/src/test/resources/application-test.yaml @@ -0,0 +1,31 @@ +spring: + jackson: + time-zone: UTC + h2: + console: + enabled: true + datasource: + url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false + username: sa + password: + driver-class-name: org.h2.Driver + sql: + init: + mode: never + platform: h2 + schema-locations: classpath:schema.sql + jpa: + hibernate: + ddl-auto: create-drop + database-platform: org.hibernate.dialect.PostgreSQLDialect + show-sql: true + properties: + hibernate: + format_sql: true + jdbc: + time_zone: UTC +logging: + level: + org.hibernate.SQL: info + org.hibernate.type.descriptor.sql.BasicBinder: trace + root: info diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql new file mode 100644 index 000000000..356497b50 --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1,97 @@ +create schema if not exists discodeit; +CREATE TYPE channel_type AS ENUM ('PUBLIC', 'PRIVATE'); + +CREATE TABLE tbl_user ( + id uuid primary key, + username varchar(50) unique not null, + email varchar(100) unique not null, + password varchar(60) not null, + profile_id uuid, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone +); + +CREATE TABLE tbl_user_status ( + id uuid primary key, + last_active_at timestamp with time zone NOT NULL, + user_id uuid unique, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone +); + +CREATE TABLE tbl_channel ( + id uuid primary key, + name varchar(100), + description varchar(500), + type channel_type not null, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone +); + +CREATE TABLE tbl_message ( + id uuid primary key, + content text, + channel_id uuid not null, + author_id uuid, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone +); + +CREATE TABLE tbl_message_attachment ( + id serial primary key, + message_id uuid, + attachment_id uuid +); + +CREATE TABLE tbl_read_status ( + id uuid primary key, + last_read_at timestamp with time zone not null, + user_id uuid, + channel_id uuid, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone +); + +CREATE TABLE tbl_binary_content ( + id uuid primary key, + file_name varchar(255) not null, + size bigint not null, + content_type VARCHAR(255) not null, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone +); + +ALTER TABLE tbl_user ADD CONSTRAINT fk_user_profile FOREIGN KEY (profile_id) REFERENCES tbl_binary_content(id) ON DELETE SET NULL; +ALTER TABLE tbl_user_status ADD FOREIGN KEY (user_id) REFERENCES tbl_user(id) ON DELETE CASCADE; +ALTER TABLE tbl_read_status ADD FOREIGN KEY (user_id) REFERENCES tbl_user(id) ON DELETE CASCADE; +ALTER TABLE tbl_read_status ADD FOREIGN KEY (channel_id) REFERENCES tbl_channel(id) ON DELETE CASCADE; +ALTER TABLE tbl_read_status ADD UNIQUE (user_id, channel_id); +ALTER TABLE tbl_message ADD FOREIGN KEY (author_id) REFERENCES tbl_user(id) ON DELETE SET NULL; +ALTER TABLE tbl_message ADD FOREIGN KEY (channel_id) REFERENCES tbl_channel(id) ON DELETE CASCADE; +ALTER TABLE tbl_message_attachment ADD FOREIGN KEY (message_id) REFERENCES tbl_message(id) ON DELETE CASCADE; +ALTER TABLE tbl_message_attachment ADD FOREIGN KEY (attachment_id) REFERENCES tbl_binary_content(id) ON DELETE CASCADE; + +-- -- 바이너리 콘텐츠 +-- INSERT INTO tbl_binary_content (id, file_name, size, content_type, created_at) +-- VALUES ('b1f83f77-1b1d-4c56-ae99-0b1ccf2d2024', 'profile.jpg', 123, 'jpg', '2025-06-19T08:00:00Z'); +-- +-- -- 사용자 +-- INSERT INTO tbl_user (id, username, email, password, profile_id, created_at, updated_at) +-- VALUES ('11111111-1111-1111-1111-111111111111', 'tester', 'tester@example.com', 'pw1234', 'b1f83f77-1b1d-4c56-ae99-0b1ccf2d2024', '2025-06-19T08:00:01Z', '2025-06-19T08:00:01Z'); +-- +-- -- 사용자 상태 +-- INSERT INTO tbl_user_status (id, user_id, last_active_at, created_at, updated_at) +-- VALUES ('22222222-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', '2025-06-19T08:00:02Z', '2025-06-19T08:00:02Z', '2025-06-19T08:00:02Z'); +-- +-- -- 채널 +-- INSERT INTO tbl_channel (id, type, name, description, created_at, updated_at) +-- VALUES ('33333333-1111-1111-1111-111111111111', 'PUBLIC', 'general', '일반 채널입니다.', '2025-06-19T08:00:03Z', '2025-06-19T08:00:03Z'); +-- +-- -- 메시지 5개 (2초 간격) +-- INSERT INTO tbl_message (id, content, channel_id, author_id, created_at, updated_at) +-- VALUES +-- ('aaaaaaaa-aaaa-bbbb-cccc-000000000001', '내용 0', '33333333-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', '2025-06-19T08:00:05Z', '2025-06-19T08:00:05Z'), +-- ('aaaaaaaa-aaaa-bbbb-cccc-000000000002', '내용 1', '33333333-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', '2025-06-19T08:00:07Z', '2025-06-19T08:00:07Z'), +-- ('aaaaaaaa-aaaa-bbbb-cccc-000000000003', '내용 2', '33333333-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', '2025-06-19T08:00:09Z', '2025-06-19T08:00:09Z'), +-- ('aaaaaaaa-aaaa-bbbb-cccc-000000000004', '내용 3', '33333333-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', '2025-06-19T08:00:11Z', '2025-06-19T08:00:11Z'), +-- ('aaaaaaaa-aaaa-bbbb-cccc-000000000005', '내용 4', '33333333-1111-1111-1111-111111111111', '11111111-1111-1111-1111-111111111111', '2025-06-19T08:00:13Z', '2025-06-19T08:00:13Z');