From a329db7d1a9b0255b797a638a9f6e474c49e6207 Mon Sep 17 00:00:00 2001 From: subin930 Date: Mon, 29 Dec 2025 21:07:51 +0900 Subject: [PATCH 01/19] =?UTF-8?q?[Docs]=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프로젝트 생성 및 .gitignore 파일 생성 --- coachcoach/.gitattributes | 3 + coachcoach/.gitignore | 207 +++++++++++++++ coachcoach/build.gradle | 40 +++ coachcoach/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45633 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + coachcoach/gradlew | 248 ++++++++++++++++++ coachcoach/gradlew.bat | 93 +++++++ coachcoach/settings.gradle | 1 + .../coachcoach/CoachcoachApplication.java | 13 + .../src/main/resources/application.properties | 1 + .../CoachcoachApplicationTests.java | 13 + 11 files changed, 626 insertions(+) create mode 100644 coachcoach/.gitattributes create mode 100644 coachcoach/.gitignore create mode 100644 coachcoach/build.gradle create mode 100644 coachcoach/gradle/wrapper/gradle-wrapper.jar create mode 100644 coachcoach/gradle/wrapper/gradle-wrapper.properties create mode 100644 coachcoach/gradlew create mode 100644 coachcoach/gradlew.bat create mode 100644 coachcoach/settings.gradle create mode 100644 coachcoach/src/main/java/com/chord/coachcoach/CoachcoachApplication.java create mode 100644 coachcoach/src/main/resources/application.properties create mode 100644 coachcoach/src/test/java/com/chord/coachcoach/CoachcoachApplicationTests.java diff --git a/coachcoach/.gitattributes b/coachcoach/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/coachcoach/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/coachcoach/.gitignore b/coachcoach/.gitignore new file mode 100644 index 0000000..6aab3c7 --- /dev/null +++ b/coachcoach/.gitignore @@ -0,0 +1,207 @@ +# Created by https://www.toptal.com/developers/gitignore/api/java,gradle,intellij+all,visualstudiocode,macos +# Edit at https://www.toptal.com/developers/gitignore?templates=java,gradle,intellij+all,visualstudiocode,macos + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/artifacts +.idea/compiler.xml +.idea/jarRepositories.xml +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Gradle ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Gradle Patch ### +# Java heap dump +*.hprof + +### yml/yaml ### +application-*.properties +application-*.yml +.env +.env.* +!application.yml +!application.properties +!application-example.yml.sample + + +# End of https://www.toptal.com/developers/gitignore/api/java,gradle,intellij+all,visualstudiocode,macos diff --git a/coachcoach/build.gradle b/coachcoach/build.gradle new file mode 100644 index 0000000..e79043e --- /dev/null +++ b/coachcoach/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.2' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'chord' +version = '0.0.1-SNAPSHOT' +description = 'coachcoach' + +allprojects { + repositories { + mavenCentral() + } + + apply plugin: 'java' + apply plugin: 'org.springframework.boot' + apply plugin: 'io.spring.dependency-management' + + java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + dependencies { + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + } + + tasks.named('test') { + useJUnitPlatform() + } +} + diff --git a/coachcoach/gradle/wrapper/gradle-wrapper.jar b/coachcoach/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f8e1ee3125fe0768e9a76ee977ac089eb657005e GIT binary patch literal 45633 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�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 literal 0 HcmV?d00001 diff --git a/coachcoach/gradle/wrapper/gradle-wrapper.properties b/coachcoach/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..23449a2 --- /dev/null +++ b/coachcoach/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/coachcoach/gradlew b/coachcoach/gradlew new file mode 100644 index 0000000..adff685 --- /dev/null +++ b/coachcoach/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 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 + + + +# 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" ) + + 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" \ + -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/coachcoach/gradlew.bat b/coachcoach/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/coachcoach/gradlew.bat @@ -0,0 +1,93 @@ +@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 + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -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/coachcoach/settings.gradle b/coachcoach/settings.gradle new file mode 100644 index 0000000..b1d6f60 --- /dev/null +++ b/coachcoach/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'coachcoach' diff --git a/coachcoach/src/main/java/com/chord/coachcoach/CoachcoachApplication.java b/coachcoach/src/main/java/com/chord/coachcoach/CoachcoachApplication.java new file mode 100644 index 0000000..b8f35de --- /dev/null +++ b/coachcoach/src/main/java/com/chord/coachcoach/CoachcoachApplication.java @@ -0,0 +1,13 @@ +package com.chord.coachcoach; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CoachcoachApplication { + + public static void main(String[] args) { + SpringApplication.run(CoachcoachApplication.class, args); + } + +} diff --git a/coachcoach/src/main/resources/application.properties b/coachcoach/src/main/resources/application.properties new file mode 100644 index 0000000..eabf53f --- /dev/null +++ b/coachcoach/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=coachcoach diff --git a/coachcoach/src/test/java/com/chord/coachcoach/CoachcoachApplicationTests.java b/coachcoach/src/test/java/com/chord/coachcoach/CoachcoachApplicationTests.java new file mode 100644 index 0000000..66f2284 --- /dev/null +++ b/coachcoach/src/test/java/com/chord/coachcoach/CoachcoachApplicationTests.java @@ -0,0 +1,13 @@ +package com.chord.coachcoach; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CoachcoachApplicationTests { + + @Test + void contextLoads() { + } + +} From 4d8291c06ec802f792458b465207575ebd1730f3 Mon Sep 17 00:00:00 2001 From: subin930 Date: Tue, 30 Dec 2025 10:35:32 +0900 Subject: [PATCH 02/19] =?UTF-8?q?[Docs]=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MSA 기반 도메인 분리 --- coachcoach/common/security/build.gradle | 0 coachcoach/service/auth/build.gradle | 0 coachcoach/service/catalog/build.gradle | 0 coachcoach/service/insight/build.gradle | 0 coachcoach/service/user-store/build.gradle | 0 coachcoach/settings.gradle | 9 +++++++++ .../com/chord/coachcoach/CoachcoachApplication.java | 13 ------------- .../src/main/resources/application.properties | 1 - .../coachcoach/CoachcoachApplicationTests.java | 13 ------------- 9 files changed, 9 insertions(+), 27 deletions(-) create mode 100644 coachcoach/common/security/build.gradle create mode 100644 coachcoach/service/auth/build.gradle create mode 100644 coachcoach/service/catalog/build.gradle create mode 100644 coachcoach/service/insight/build.gradle create mode 100644 coachcoach/service/user-store/build.gradle delete mode 100644 coachcoach/src/main/java/com/chord/coachcoach/CoachcoachApplication.java delete mode 100644 coachcoach/src/main/resources/application.properties delete mode 100644 coachcoach/src/test/java/com/chord/coachcoach/CoachcoachApplicationTests.java diff --git a/coachcoach/common/security/build.gradle b/coachcoach/common/security/build.gradle new file mode 100644 index 0000000..e69de29 diff --git a/coachcoach/service/auth/build.gradle b/coachcoach/service/auth/build.gradle new file mode 100644 index 0000000..e69de29 diff --git a/coachcoach/service/catalog/build.gradle b/coachcoach/service/catalog/build.gradle new file mode 100644 index 0000000..e69de29 diff --git a/coachcoach/service/insight/build.gradle b/coachcoach/service/insight/build.gradle new file mode 100644 index 0000000..e69de29 diff --git a/coachcoach/service/user-store/build.gradle b/coachcoach/service/user-store/build.gradle new file mode 100644 index 0000000..e69de29 diff --git a/coachcoach/settings.gradle b/coachcoach/settings.gradle index b1d6f60..c2feeb6 100644 --- a/coachcoach/settings.gradle +++ b/coachcoach/settings.gradle @@ -1 +1,10 @@ rootProject.name = 'coachcoach' + +include 'common' +include 'common:security' + +include 'service' +include 'service:auth' +include 'service:user-store' +include 'service:catalog' +include 'service:insight' \ No newline at end of file diff --git a/coachcoach/src/main/java/com/chord/coachcoach/CoachcoachApplication.java b/coachcoach/src/main/java/com/chord/coachcoach/CoachcoachApplication.java deleted file mode 100644 index b8f35de..0000000 --- a/coachcoach/src/main/java/com/chord/coachcoach/CoachcoachApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.chord.coachcoach; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class CoachcoachApplication { - - public static void main(String[] args) { - SpringApplication.run(CoachcoachApplication.class, args); - } - -} diff --git a/coachcoach/src/main/resources/application.properties b/coachcoach/src/main/resources/application.properties deleted file mode 100644 index eabf53f..0000000 --- a/coachcoach/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=coachcoach diff --git a/coachcoach/src/test/java/com/chord/coachcoach/CoachcoachApplicationTests.java b/coachcoach/src/test/java/com/chord/coachcoach/CoachcoachApplicationTests.java deleted file mode 100644 index 66f2284..0000000 --- a/coachcoach/src/test/java/com/chord/coachcoach/CoachcoachApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.chord.coachcoach; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class CoachcoachApplicationTests { - - @Test - void contextLoads() { - } - -} From ff4c0f811485bd46dca443927e59caa46453e869 Mon Sep 17 00:00:00 2001 From: subin930 Date: Tue, 30 Dec 2025 17:37:44 +0900 Subject: [PATCH 03/19] =?UTF-8?q?[Docs]=20API=20Gateway,=20Eureka=20Server?= =?UTF-8?q?=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API Gateway, Eureka Server 구축 및 비즈니스 모듈과 연결 및 테스트 완료 --- coachcoach/common/security/build.gradle | 0 coachcoach/infra/eureka/build.gradle | 20 ++++++++++++++ .../coachcoach/eureka/EurekaApplication.java | 13 +++++++++ .../eureka/src/main/resources/application.yml | 19 +++++++++++++ coachcoach/infra/gateway/build.gradle | 20 ++++++++++++++ .../gateway/GatewayApplication.java | 11 ++++++++ .../gateway/config/GatewayConfig.java | 27 +++++++++++++++++++ .../src/main/resources/application.yml | 19 +++++++++++++ coachcoach/service/auth/build.gradle | 17 ++++++++++++ .../com/coachcoach/auth/AuthApplication.java | 11 ++++++++ .../auth/controller/AuthController.java | 12 +++++++++ .../auth/src/main/resources/application.yml | 14 ++++++++++ coachcoach/service/catalog/build.gradle | 17 ++++++++++++ .../catalog/CatalogApplication.java | 11 ++++++++ .../src/main/resources/application.yml | 14 ++++++++++ coachcoach/service/insight/build.gradle | 17 ++++++++++++ .../insight/InsightApplication.java | 11 ++++++++ .../src/main/resources/application.yml | 14 ++++++++++ coachcoach/service/user-store/build.gradle | 17 ++++++++++++ .../userstore/UserStoreApplication.java | 11 ++++++++ .../src/main/resources/application.yml | 14 ++++++++++ coachcoach/settings.gradle | 6 ++++- 22 files changed, 314 insertions(+), 1 deletion(-) delete mode 100644 coachcoach/common/security/build.gradle create mode 100644 coachcoach/infra/eureka/build.gradle create mode 100644 coachcoach/infra/eureka/src/main/java/com/coachcoach/eureka/EurekaApplication.java create mode 100644 coachcoach/infra/eureka/src/main/resources/application.yml create mode 100644 coachcoach/infra/gateway/build.gradle create mode 100644 coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/GatewayApplication.java create mode 100644 coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/GatewayConfig.java create mode 100644 coachcoach/infra/gateway/src/main/resources/application.yml create mode 100644 coachcoach/service/auth/src/main/java/com/coachcoach/auth/AuthApplication.java create mode 100644 coachcoach/service/auth/src/main/java/com/coachcoach/auth/controller/AuthController.java create mode 100644 coachcoach/service/auth/src/main/resources/application.yml create mode 100644 coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/CatalogApplication.java create mode 100644 coachcoach/service/catalog/src/main/resources/application.yml create mode 100644 coachcoach/service/insight/src/main/java/com/coachcoach/insight/InsightApplication.java create mode 100644 coachcoach/service/insight/src/main/resources/application.yml create mode 100644 coachcoach/service/user-store/src/main/java/com/coachcoach/userstore/UserStoreApplication.java create mode 100644 coachcoach/service/user-store/src/main/resources/application.yml diff --git a/coachcoach/common/security/build.gradle b/coachcoach/common/security/build.gradle deleted file mode 100644 index e69de29..0000000 diff --git a/coachcoach/infra/eureka/build.gradle b/coachcoach/infra/eureka/build.gradle new file mode 100644 index 0000000..f7b7ff8 --- /dev/null +++ b/coachcoach/infra/eureka/build.gradle @@ -0,0 +1,20 @@ +ext { + set('springCloudVersion', "2023.0.3") +} + +dependencies { + // Spring Boot Starter Web + implementation 'org.springframework.boot:spring-boot-starter-web' + + //Eureka Server + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server' + + // Spring Actuator + implementation 'org.springframework.boot:spring-boot-starter-actuator' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} \ No newline at end of file diff --git a/coachcoach/infra/eureka/src/main/java/com/coachcoach/eureka/EurekaApplication.java b/coachcoach/infra/eureka/src/main/java/com/coachcoach/eureka/EurekaApplication.java new file mode 100644 index 0000000..769abc8 --- /dev/null +++ b/coachcoach/infra/eureka/src/main/java/com/coachcoach/eureka/EurekaApplication.java @@ -0,0 +1,13 @@ +package com.coachcoach.eureka; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; + +@SpringBootApplication +@EnableEurekaServer +public class EurekaApplication { + public static void main(String[] args) { + SpringApplication.run(EurekaApplication.class, args); + } +} diff --git a/coachcoach/infra/eureka/src/main/resources/application.yml b/coachcoach/infra/eureka/src/main/resources/application.yml new file mode 100644 index 0000000..adc2ab9 --- /dev/null +++ b/coachcoach/infra/eureka/src/main/resources/application.yml @@ -0,0 +1,19 @@ +server.port: 8761 +spring: + application: + name: eureka-service + +eureka: + client: + register-with-eureka: false + fetch-registry: false + service-url: + defaultZone: http://localhost:8761/eureka/ + server: + wait-time-in-ms-when-sync-empty: 300000 #5분(deafult와 동일) + +management: + endpoints: + web: + exposure: + include: health, info #공개할 엔드포인트 \ No newline at end of file diff --git a/coachcoach/infra/gateway/build.gradle b/coachcoach/infra/gateway/build.gradle new file mode 100644 index 0000000..5c99d1a --- /dev/null +++ b/coachcoach/infra/gateway/build.gradle @@ -0,0 +1,20 @@ +ext { + set('springCloudVersion', "2023.0.3") +} + +dependencies { + // WebFlux Starter + implementation 'org.springframework.boot:spring-boot-starter-webflux' + + // Spring Cloud Gateway + implementation 'org.springframework.cloud:spring-cloud-starter-gateway' + + // Eureka Client + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} \ No newline at end of file diff --git a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/GatewayApplication.java b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/GatewayApplication.java new file mode 100644 index 0000000..9d7d5a9 --- /dev/null +++ b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/GatewayApplication.java @@ -0,0 +1,11 @@ +package com.coachcoach.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class GatewayApplication { + public static void main(String[] args) { + SpringApplication.run(GatewayApplication.class, args); + } +} diff --git a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/GatewayConfig.java b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/GatewayConfig.java new file mode 100644 index 0000000..779099b --- /dev/null +++ b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/GatewayConfig.java @@ -0,0 +1,27 @@ +package com.coachcoach.gateway.config; + +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class GatewayConfig { + @Bean + public RouteLocator myRoutes(RouteLocatorBuilder builder) { + return builder.routes() + .route("auth-service", + p -> p.path("/api/auth/**") + .uri("lb://auth-service")) + .route("user-store-service", + p -> p.path("/api/user-store/**") + .uri("lb://user-store-service")) + .route("catalog-service", + p -> p.path("/api/catalog/**") + .uri("lb://catalog-service")) + .route("insight-service", + p -> p.path("/api/insight/**") + .uri("lb://insight-service")) + .build(); + } +} diff --git a/coachcoach/infra/gateway/src/main/resources/application.yml b/coachcoach/infra/gateway/src/main/resources/application.yml new file mode 100644 index 0000000..2220151 --- /dev/null +++ b/coachcoach/infra/gateway/src/main/resources/application.yml @@ -0,0 +1,19 @@ +server.port: 8000 +spring: + application: + name: gateway-service + cloud: + gateway: + discovery: + locator: + enabled: true #Eureka 기반 라우팅 + lower-case-service-id: true +eureka: + instance: + prefer-ip-address: true # 호스트명 대신 IP로 등록 + hostname: localhost + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://localhost:8761/eureka/ \ No newline at end of file diff --git a/coachcoach/service/auth/build.gradle b/coachcoach/service/auth/build.gradle index e69de29..ea3fa1a 100644 --- a/coachcoach/service/auth/build.gradle +++ b/coachcoach/service/auth/build.gradle @@ -0,0 +1,17 @@ +ext { + set('springCloudVersion', "2023.0.3") +} + +dependencies { + // Spring Boot Starter Web + implementation 'org.springframework.boot:spring-boot-starter-web' + + // Eureka Client + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} \ No newline at end of file diff --git a/coachcoach/service/auth/src/main/java/com/coachcoach/auth/AuthApplication.java b/coachcoach/service/auth/src/main/java/com/coachcoach/auth/AuthApplication.java new file mode 100644 index 0000000..9f6f31a --- /dev/null +++ b/coachcoach/service/auth/src/main/java/com/coachcoach/auth/AuthApplication.java @@ -0,0 +1,11 @@ +package com.coachcoach.auth; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AuthApplication { + public static void main(String[] args) { + SpringApplication.run(AuthApplication.class, args); + } +} \ No newline at end of file diff --git a/coachcoach/service/auth/src/main/java/com/coachcoach/auth/controller/AuthController.java b/coachcoach/service/auth/src/main/java/com/coachcoach/auth/controller/AuthController.java new file mode 100644 index 0000000..0a2ca05 --- /dev/null +++ b/coachcoach/service/auth/src/main/java/com/coachcoach/auth/controller/AuthController.java @@ -0,0 +1,12 @@ +package com.coachcoach.auth.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class AuthController { + @GetMapping("/api/auth/hi") + public String hi(){ + return "hi"; + } +} diff --git a/coachcoach/service/auth/src/main/resources/application.yml b/coachcoach/service/auth/src/main/resources/application.yml new file mode 100644 index 0000000..08c58ed --- /dev/null +++ b/coachcoach/service/auth/src/main/resources/application.yml @@ -0,0 +1,14 @@ +server.port: 9000 +spring: + application: + name: auth-service + +eureka: + instance: + prefer-ip-address: true # 호스트명 대신 IP로 등록 + hostname: localhost + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://localhost:8761/eureka/ \ No newline at end of file diff --git a/coachcoach/service/catalog/build.gradle b/coachcoach/service/catalog/build.gradle index e69de29..ea3fa1a 100644 --- a/coachcoach/service/catalog/build.gradle +++ b/coachcoach/service/catalog/build.gradle @@ -0,0 +1,17 @@ +ext { + set('springCloudVersion', "2023.0.3") +} + +dependencies { + // Spring Boot Starter Web + implementation 'org.springframework.boot:spring-boot-starter-web' + + // Eureka Client + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} \ No newline at end of file diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/CatalogApplication.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/CatalogApplication.java new file mode 100644 index 0000000..8b5967e --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/CatalogApplication.java @@ -0,0 +1,11 @@ +package com.coachcoach.catalog; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CatalogApplication { + public static void main(String[] args) { + SpringApplication.run(CatalogApplication.class, args); + } +} diff --git a/coachcoach/service/catalog/src/main/resources/application.yml b/coachcoach/service/catalog/src/main/resources/application.yml new file mode 100644 index 0000000..9d91af1 --- /dev/null +++ b/coachcoach/service/catalog/src/main/resources/application.yml @@ -0,0 +1,14 @@ +server.port: 9002 +spring: + application: + name: catalog-service + +eureka: + instance: + prefer-ip-address: true # 호스트명 대신 IP로 등록 + hostname: localhost + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://localhost:8761/eureka/ \ No newline at end of file diff --git a/coachcoach/service/insight/build.gradle b/coachcoach/service/insight/build.gradle index e69de29..ea3fa1a 100644 --- a/coachcoach/service/insight/build.gradle +++ b/coachcoach/service/insight/build.gradle @@ -0,0 +1,17 @@ +ext { + set('springCloudVersion', "2023.0.3") +} + +dependencies { + // Spring Boot Starter Web + implementation 'org.springframework.boot:spring-boot-starter-web' + + // Eureka Client + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} \ No newline at end of file diff --git a/coachcoach/service/insight/src/main/java/com/coachcoach/insight/InsightApplication.java b/coachcoach/service/insight/src/main/java/com/coachcoach/insight/InsightApplication.java new file mode 100644 index 0000000..16b179d --- /dev/null +++ b/coachcoach/service/insight/src/main/java/com/coachcoach/insight/InsightApplication.java @@ -0,0 +1,11 @@ +package com.coachcoach.insight; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class InsightApplication { + public static void main(String[] args) { + SpringApplication.run(InsightApplication.class, args); + } +} diff --git a/coachcoach/service/insight/src/main/resources/application.yml b/coachcoach/service/insight/src/main/resources/application.yml new file mode 100644 index 0000000..59d11cf --- /dev/null +++ b/coachcoach/service/insight/src/main/resources/application.yml @@ -0,0 +1,14 @@ +server.port: 9003 +spring: + application: + name: insight-service + +eureka: + instance: + prefer-ip-address: true # 호스트명 대신 IP로 등록 + hostname: localhost + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://localhost:8761/eureka/ \ No newline at end of file diff --git a/coachcoach/service/user-store/build.gradle b/coachcoach/service/user-store/build.gradle index e69de29..ea3fa1a 100644 --- a/coachcoach/service/user-store/build.gradle +++ b/coachcoach/service/user-store/build.gradle @@ -0,0 +1,17 @@ +ext { + set('springCloudVersion', "2023.0.3") +} + +dependencies { + // Spring Boot Starter Web + implementation 'org.springframework.boot:spring-boot-starter-web' + + // Eureka Client + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} \ No newline at end of file diff --git a/coachcoach/service/user-store/src/main/java/com/coachcoach/userstore/UserStoreApplication.java b/coachcoach/service/user-store/src/main/java/com/coachcoach/userstore/UserStoreApplication.java new file mode 100644 index 0000000..fc1aa60 --- /dev/null +++ b/coachcoach/service/user-store/src/main/java/com/coachcoach/userstore/UserStoreApplication.java @@ -0,0 +1,11 @@ +package com.coachcoach.userstore; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UserStoreApplication { + public static void main(String[] args) { + SpringApplication.run(UserStoreApplication.class, args); + } +} diff --git a/coachcoach/service/user-store/src/main/resources/application.yml b/coachcoach/service/user-store/src/main/resources/application.yml new file mode 100644 index 0000000..2c01007 --- /dev/null +++ b/coachcoach/service/user-store/src/main/resources/application.yml @@ -0,0 +1,14 @@ +server.port: 9001 +spring: + application: + name: user-store-service + +eureka: + instance: + prefer-ip-address: true # 호스트명 대신 IP로 등록 + hostname: localhost + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://localhost:8761/eureka/ \ No newline at end of file diff --git a/coachcoach/settings.gradle b/coachcoach/settings.gradle index c2feeb6..145d562 100644 --- a/coachcoach/settings.gradle +++ b/coachcoach/settings.gradle @@ -1,7 +1,11 @@ rootProject.name = 'coachcoach' +include 'infra' +include 'infra:gateway' +include 'infra:eureka' +include 'infra:config' + include 'common' -include 'common:security' include 'service' include 'service:auth' From 659bf2094dc1e29703785a01dba48d3a34c2a3e6 Mon Sep 17 00:00:00 2001 From: subin930 Date: Wed, 31 Dec 2025 10:31:28 +0900 Subject: [PATCH 04/19] =?UTF-8?q?[Remove]=20infra:config=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit infra:config 모듈 삭제, 추후 추가 구현 시 구현하는 것으로 변경 --- coachcoach/settings.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/coachcoach/settings.gradle b/coachcoach/settings.gradle index 145d562..1b041b4 100644 --- a/coachcoach/settings.gradle +++ b/coachcoach/settings.gradle @@ -3,7 +3,6 @@ rootProject.name = 'coachcoach' include 'infra' include 'infra:gateway' include 'infra:eureka' -include 'infra:config' include 'common' From 6b10bec19961e096c082c6735b7f8260ec415216 Mon Sep 17 00:00:00 2001 From: Subin Oh <117198327+subin930@users.noreply.github.com> Date: Fri, 2 Jan 2026 14:52:59 +0900 Subject: [PATCH 05/19] =?UTF-8?q?[Feat]=20CI/CD=20=ED=8C=8C=EC=9D=B4?= =?UTF-8?q?=ED=94=84=EB=9D=BC=EC=9D=B8=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deployment.yml | 81 ++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 .github/workflows/deployment.yml diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml new file mode 100644 index 0000000..46a7c41 --- /dev/null +++ b/.github/workflows/deployment.yml @@ -0,0 +1,81 @@ +name: CI/CD Pipeline + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - name: gateway + path: ./coach/coach/infra/gateway + - name: eureka + path: ./coach/coach/infra/eureka + - name: auth + path: ./coach/service/auth + - name: user-store + path: ./coach/service/user-store + - name: catalog + path: ./coach/service/catalog + - name: insight + path: ./coach/service/insight + steps: + - uses: actions/checkout@v3 + - run: echo "${{ secrets.QUAY_TOKEN }}" | docker login quay.io -u bread0930 --password-stdin + - run: | + docker build -t quay.io/bread0930/chord-server-${{ matrix.name }}:${{ github.sha }} ${{ matrix.path }} + docker tag quay.io/bread0930/chord-server-${{ matrix.name }}:${{ github.sha }} quay.io/bread0930/chord-server-${{ matrix.name }}:latest + docker push quay.io/bread0930/chord-server-${{ matrix.name }}:${{ github.sha }} + docker push quay.io/bread0930/chord-server-${{ matrix.name }}:latest + + deploy-1: + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to public EC2 + uses: appleboy/ssh-action@v0.1.10 + with: + host: ${{ secrets.PUBLIC_EC2_1_HOST }} + username: ubuntu + key: ${{ secrets.PUBLIC_EC2_1_SSH_KEY }} + script: | + docker-compose -f /home/ubuntu/public1-compose.yml pull + docker-compose -f /home/ubuntu/public1-compose.yml up -d + + deploy-2: + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to private EC2 - 1 (User-store, Auth) + uses: appleboy/ssh-action@v0.1.10 + with: + host: ${{ secrets.PRIVATE_EC2_1_HOST }} + username: ubuntu + key: ${{ secrets.PRIVATE_EC2_1_SSH_KEY }} + proxy_host: ${{ secrets.PUBLIC_EC2_1_HOST }} + proxy_username: ubuntu + proxy_key: ${{ secrets.PUBLIC_EC2_1_SSH_KEY }} + script: | + docker-compose -f /home/ubuntu/private1-compose.yml pull + docker-compose -f /home/ubuntu/private1-compose.yml up -d + deploy-3: + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to private EC2 - 2 (Catalog, Insight) + uses: appleboy/ssh-action@v0.1.10 + with: + host: ${{ secrets.PRIVATE_EC2_2_HOST }} + username: ubuntu + key: ${{ secrets.PRIVATE_EC2_2_SSH_KEY }} + proxy_host: ${{ secrets.PUBLIC_EC2_1_HOST }} + proxy_username: ubuntu + proxy_key: ${{ secrets.PUBLIC_EC2_1_SSH_KEY }} + script: | + docker-compose -f /home/ubuntu/private2-compose.yml pull + docker-compose -f /home/ubuntu/private2-compose.yml up -d From 992b2737e1871af7bce112677627570c9aebe478 Mon Sep 17 00:00:00 2001 From: subin930 Date: Fri, 2 Jan 2026 14:55:12 +0900 Subject: [PATCH 06/19] =?UTF-8?q?[Feat]=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{application.yml => application-prod.yml} | 0 .../catalog/src/main/resources/application-prod.yml | 13 +++++++++++++ .../src/main/resources/application-prod.yml} | 3 +-- .../src/main/resources/application-prod.yml | 13 +++++++++++++ 4 files changed, 27 insertions(+), 2 deletions(-) rename coachcoach/service/auth/src/main/resources/{application.yml => application-prod.yml} (100%) create mode 100644 coachcoach/service/catalog/src/main/resources/application-prod.yml rename coachcoach/service/{catalog/src/main/resources/application.yml => insight/src/main/resources/application-prod.yml} (76%) create mode 100644 coachcoach/service/user-store/src/main/resources/application-prod.yml diff --git a/coachcoach/service/auth/src/main/resources/application.yml b/coachcoach/service/auth/src/main/resources/application-prod.yml similarity index 100% rename from coachcoach/service/auth/src/main/resources/application.yml rename to coachcoach/service/auth/src/main/resources/application-prod.yml diff --git a/coachcoach/service/catalog/src/main/resources/application-prod.yml b/coachcoach/service/catalog/src/main/resources/application-prod.yml new file mode 100644 index 0000000..9946b9e --- /dev/null +++ b/coachcoach/service/catalog/src/main/resources/application-prod.yml @@ -0,0 +1,13 @@ +server.port: 9000 +spring: + application: + name: auth-service + +eureka: + instance: + prefer-ip-address: true # 호스트명 대신 IP로 등록 + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://10.0.0.107:8761/eureka/ \ No newline at end of file diff --git a/coachcoach/service/catalog/src/main/resources/application.yml b/coachcoach/service/insight/src/main/resources/application-prod.yml similarity index 76% rename from coachcoach/service/catalog/src/main/resources/application.yml rename to coachcoach/service/insight/src/main/resources/application-prod.yml index 9d91af1..22c3647 100644 --- a/coachcoach/service/catalog/src/main/resources/application.yml +++ b/coachcoach/service/insight/src/main/resources/application-prod.yml @@ -6,9 +6,8 @@ spring: eureka: instance: prefer-ip-address: true # 호스트명 대신 IP로 등록 - hostname: localhost client: register-with-eureka: true fetch-registry: true service-url: - defaultZone: http://localhost:8761/eureka/ \ No newline at end of file + defaultZone: http://10.0.0.107:8761/eureka/ \ No newline at end of file diff --git a/coachcoach/service/user-store/src/main/resources/application-prod.yml b/coachcoach/service/user-store/src/main/resources/application-prod.yml new file mode 100644 index 0000000..fea533d --- /dev/null +++ b/coachcoach/service/user-store/src/main/resources/application-prod.yml @@ -0,0 +1,13 @@ +server.port: 9003 +spring: + application: + name: insight-service + +eureka: + instance: + prefer-ip-address: true # 호스트명 대신 IP로 등록 + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://10.0.0.107:8761/eureka/ \ No newline at end of file From 39de7032a7296191b5f68c063c3e4b8931012076 Mon Sep 17 00:00:00 2001 From: subin930 Date: Fri, 2 Jan 2026 15:01:57 +0900 Subject: [PATCH 07/19] =?UTF-8?q?[Feat]=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EA=B5=AC=EC=84=B1=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coachcoach/.gitignore | 4 +--- .../service/auth/src/main/resources/application-prod.yml | 3 +-- .../service/catalog/src/main/resources/application-prod.yml | 4 ++-- .../service/insight/src/main/resources/application-prod.yml | 4 ++-- .../user-store/src/main/resources/application-prod.yml | 2 +- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/coachcoach/.gitignore b/coachcoach/.gitignore index 6aab3c7..e371b9c 100644 --- a/coachcoach/.gitignore +++ b/coachcoach/.gitignore @@ -195,13 +195,11 @@ gradle-app.setting *.hprof ### yml/yaml ### -application-*.properties -application-*.yml +application-dev.yml .env .env.* !application.yml !application.properties -!application-example.yml.sample # End of https://www.toptal.com/developers/gitignore/api/java,gradle,intellij+all,visualstudiocode,macos diff --git a/coachcoach/service/auth/src/main/resources/application-prod.yml b/coachcoach/service/auth/src/main/resources/application-prod.yml index 08c58ed..9946b9e 100644 --- a/coachcoach/service/auth/src/main/resources/application-prod.yml +++ b/coachcoach/service/auth/src/main/resources/application-prod.yml @@ -6,9 +6,8 @@ spring: eureka: instance: prefer-ip-address: true # 호스트명 대신 IP로 등록 - hostname: localhost client: register-with-eureka: true fetch-registry: true service-url: - defaultZone: http://localhost:8761/eureka/ \ No newline at end of file + defaultZone: http://10.0.0.107:8761/eureka/ \ No newline at end of file diff --git a/coachcoach/service/catalog/src/main/resources/application-prod.yml b/coachcoach/service/catalog/src/main/resources/application-prod.yml index 9946b9e..22c3647 100644 --- a/coachcoach/service/catalog/src/main/resources/application-prod.yml +++ b/coachcoach/service/catalog/src/main/resources/application-prod.yml @@ -1,7 +1,7 @@ -server.port: 9000 +server.port: 9002 spring: application: - name: auth-service + name: catalog-service eureka: instance: diff --git a/coachcoach/service/insight/src/main/resources/application-prod.yml b/coachcoach/service/insight/src/main/resources/application-prod.yml index 22c3647..fea533d 100644 --- a/coachcoach/service/insight/src/main/resources/application-prod.yml +++ b/coachcoach/service/insight/src/main/resources/application-prod.yml @@ -1,7 +1,7 @@ -server.port: 9002 +server.port: 9003 spring: application: - name: catalog-service + name: insight-service eureka: instance: diff --git a/coachcoach/service/user-store/src/main/resources/application-prod.yml b/coachcoach/service/user-store/src/main/resources/application-prod.yml index fea533d..c034f1b 100644 --- a/coachcoach/service/user-store/src/main/resources/application-prod.yml +++ b/coachcoach/service/user-store/src/main/resources/application-prod.yml @@ -1,7 +1,7 @@ server.port: 9003 spring: application: - name: insight-service + name: user-store-service eureka: instance: From 3259ed95a79bbadc92e42680578d9f3d2448d657 Mon Sep 17 00:00:00 2001 From: subin930 Date: Fri, 2 Jan 2026 15:25:23 +0900 Subject: [PATCH 08/19] =?UTF-8?q?[Fix]=20cicd=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=82=B4=20=EB=AA=A8=EB=93=88=20=EA=B2=BD=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deployment.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 46a7c41..a701c88 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -13,17 +13,17 @@ jobs: matrix: include: - name: gateway - path: ./coach/coach/infra/gateway + path: ./coachcoach/infra/gateway - name: eureka - path: ./coach/coach/infra/eureka + path: ./coachcoach/infra/eureka - name: auth - path: ./coach/service/auth + path: ./coachcoach/service/auth - name: user-store - path: ./coach/service/user-store + path: ./coachcoach/service/user-store - name: catalog - path: ./coach/service/catalog + path: ./coachcoach/service/catalog - name: insight - path: ./coach/service/insight + path: ./coachcoach/service/insight steps: - uses: actions/checkout@v3 - run: echo "${{ secrets.QUAY_TOKEN }}" | docker login quay.io -u bread0930 --password-stdin From af476787ac43dcaa0ec6309a4b18eb80b19f9b46 Mon Sep 17 00:00:00 2001 From: subin930 Date: Fri, 2 Jan 2026 15:37:56 +0900 Subject: [PATCH 09/19] =?UTF-8?q?[Fix]=20=EB=AA=A8=EB=93=88=20=EB=B3=84=20?= =?UTF-8?q?=EB=8F=84=EC=BB=A4=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coachcoach/infra/eureka/Dockerfile | 0 coachcoach/infra/gateway/Dockerfile | 10 ++++++++++ coachcoach/service/auth/Dockerfile | 10 ++++++++++ coachcoach/service/catalog/Dockerfile | 10 ++++++++++ coachcoach/service/insight/Dockerfile | 10 ++++++++++ coachcoach/service/user-store/Dockerfile | 0 6 files changed, 40 insertions(+) create mode 100644 coachcoach/infra/eureka/Dockerfile create mode 100644 coachcoach/infra/gateway/Dockerfile create mode 100644 coachcoach/service/auth/Dockerfile create mode 100644 coachcoach/service/catalog/Dockerfile create mode 100644 coachcoach/service/insight/Dockerfile create mode 100644 coachcoach/service/user-store/Dockerfile diff --git a/coachcoach/infra/eureka/Dockerfile b/coachcoach/infra/eureka/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/coachcoach/infra/gateway/Dockerfile b/coachcoach/infra/gateway/Dockerfile new file mode 100644 index 0000000..ced66d2 --- /dev/null +++ b/coachcoach/infra/gateway/Dockerfile @@ -0,0 +1,10 @@ +# Eureka Dockerfile +FROM eclipse-temurin:21-jdk AS build +WORKDIR /app +COPY . . +RUN ./mvnw clean package -DskipTests + +FROM eclipse-temurin:21-jre +WORKDIR /app +COPY --from=build /app/target/*.jar app.jar +ENTRYPOINT ["java","-jar","app.jar"] \ No newline at end of file diff --git a/coachcoach/service/auth/Dockerfile b/coachcoach/service/auth/Dockerfile new file mode 100644 index 0000000..ffc1e82 --- /dev/null +++ b/coachcoach/service/auth/Dockerfile @@ -0,0 +1,10 @@ +# User-store Dockerfile +FROM eclipse-temurin:21-jdk AS build +WORKDIR /app +COPY . . +RUN ./mvnw clean package -DskipTests + +FROM eclipse-temurin:21-jre +WORKDIR /app +COPY --from=build /app/target/*.jar app.jar +ENTRYPOINT ["java","-jar","app.jar"] \ No newline at end of file diff --git a/coachcoach/service/catalog/Dockerfile b/coachcoach/service/catalog/Dockerfile new file mode 100644 index 0000000..ffc1e82 --- /dev/null +++ b/coachcoach/service/catalog/Dockerfile @@ -0,0 +1,10 @@ +# User-store Dockerfile +FROM eclipse-temurin:21-jdk AS build +WORKDIR /app +COPY . . +RUN ./mvnw clean package -DskipTests + +FROM eclipse-temurin:21-jre +WORKDIR /app +COPY --from=build /app/target/*.jar app.jar +ENTRYPOINT ["java","-jar","app.jar"] \ No newline at end of file diff --git a/coachcoach/service/insight/Dockerfile b/coachcoach/service/insight/Dockerfile new file mode 100644 index 0000000..ffc1e82 --- /dev/null +++ b/coachcoach/service/insight/Dockerfile @@ -0,0 +1,10 @@ +# User-store Dockerfile +FROM eclipse-temurin:21-jdk AS build +WORKDIR /app +COPY . . +RUN ./mvnw clean package -DskipTests + +FROM eclipse-temurin:21-jre +WORKDIR /app +COPY --from=build /app/target/*.jar app.jar +ENTRYPOINT ["java","-jar","app.jar"] \ No newline at end of file diff --git a/coachcoach/service/user-store/Dockerfile b/coachcoach/service/user-store/Dockerfile new file mode 100644 index 0000000..e69de29 From eff4b70266ff2897cad8d8acac4a8f669572ea2c Mon Sep 17 00:00:00 2001 From: subin930 Date: Sun, 4 Jan 2026 22:46:44 +0900 Subject: [PATCH 10/19] =?UTF-8?q?[Feat]=20=EC=9D=91=EB=8B=B5/=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=98=95=EC=8B=9D=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coachcoach/build.gradle | 8 +- coachcoach/common/build.gradle | 17 +++++ .../common/config/ApiResponseAdvice.java | 65 +++++++++++++++++ .../common/config/GlobalExceptionHandler.java | 70 ++++++++++++++++++ .../coachcoach/common/dto/ApiResponse.java | 73 +++++++++++++++++++ .../coachcoach/common/dto/ErrorResponse.java | 56 ++++++++++++++ .../common/exception/BusinessException.java | 18 +++++ .../common/exception/CommonErrorCode.java | 26 +++++++ .../common/exception/ErrorCode.java | 9 +++ coachcoach/service/catalog/build.gradle | 12 ++- coachcoach/settings.gradle | 3 + 11 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 coachcoach/common/build.gradle create mode 100644 coachcoach/common/src/main/java/com/coachcoach/common/config/ApiResponseAdvice.java create mode 100644 coachcoach/common/src/main/java/com/coachcoach/common/config/GlobalExceptionHandler.java create mode 100644 coachcoach/common/src/main/java/com/coachcoach/common/dto/ApiResponse.java create mode 100644 coachcoach/common/src/main/java/com/coachcoach/common/dto/ErrorResponse.java create mode 100644 coachcoach/common/src/main/java/com/coachcoach/common/exception/BusinessException.java create mode 100644 coachcoach/common/src/main/java/com/coachcoach/common/exception/CommonErrorCode.java create mode 100644 coachcoach/common/src/main/java/com/coachcoach/common/exception/ErrorCode.java diff --git a/coachcoach/build.gradle b/coachcoach/build.gradle index 73c5bac..5e0cd8f 100644 --- a/coachcoach/build.gradle +++ b/coachcoach/build.gradle @@ -4,7 +4,7 @@ plugins { id 'org.springframework.boot' version '3.3.2' apply false } -group = 'chord' +group = 'com.coachcoach' version = '0.0.1-SNAPSHOT' allprojects { @@ -22,6 +22,12 @@ subprojects { targetCompatibility = JavaVersion.VERSION_21 } + dependencyManagement { + imports { + mavenBom "org.springframework.boot:spring-boot-dependencies:3.3.2" + } + } + dependencies { compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/coachcoach/common/build.gradle b/coachcoach/common/build.gradle new file mode 100644 index 0000000..5be1ffe --- /dev/null +++ b/coachcoach/common/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'java-library' + id 'io.spring.dependency-management' +} + +dependencies { + api 'org.springframework.boot:spring-boot-starter-web' + api 'org.springframework.boot:spring-boot-starter-validation' + api 'org.springframework.boot:spring-boot-starter-data-jpa' + + api 'com.fasterxml.jackson.core:jackson-databind' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' +} + +jar { + enabled = true +} \ No newline at end of file diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/config/ApiResponseAdvice.java b/coachcoach/common/src/main/java/com/coachcoach/common/config/ApiResponseAdvice.java new file mode 100644 index 0000000..5bdb10d --- /dev/null +++ b/coachcoach/common/src/main/java/com/coachcoach/common/config/ApiResponseAdvice.java @@ -0,0 +1,65 @@ +package com.coachcoach.common.config; + +import com.coachcoach.common.dto.ApiResponse; +import com.coachcoach.common.dto.ErrorResponse; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + + +@RestControllerAdvice +public class ApiResponseAdvice implements ResponseBodyAdvice { + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + Class parameterType = returnType.getParameterType(); + return !ApiResponse.class.isAssignableFrom(parameterType) + && !ErrorResponse.class.isAssignableFrom(parameterType); + } + + @Override + public Object beforeBodyWrite(Object body, + MethodParameter returnType, + MediaType contentType, + Class> converterType, + ServerHttpRequest request, + ServerHttpResponse response) { + + if (body instanceof ApiResponse || body instanceof ErrorResponse) { + return body; + } + + if (body instanceof String) { + return ApiResponse.success(body); + } + + if (body == null) { + HttpStatus status = extractHttpStatus(response); + + // 204 No Content는 래핑 안 함 + if (status == HttpStatus.NO_CONTENT) { + return null; + } + + return ApiResponse.success(null); + } + + return ApiResponse.success(body); + } + + private HttpStatus extractHttpStatus(ServerHttpResponse response) { + if (response instanceof ServletServerHttpResponse servletResponse) { + HttpStatus resolved = HttpStatus.resolve( + servletResponse.getServletResponse().getStatus() + ); + return resolved != null ? resolved : HttpStatus.OK; + } + return HttpStatus.OK; + } +} \ No newline at end of file diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/config/GlobalExceptionHandler.java b/coachcoach/common/src/main/java/com/coachcoach/common/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..90be3d3 --- /dev/null +++ b/coachcoach/common/src/main/java/com/coachcoach/common/config/GlobalExceptionHandler.java @@ -0,0 +1,70 @@ +package com.coachcoach.common.config; + +import com.coachcoach.common.dto.ErrorResponse; +import com.coachcoach.common.exception.BusinessException; +import com.coachcoach.common.exception.CommonErrorCode; +import com.coachcoach.common.exception.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + // 비즈니스 예외 처리 + @ExceptionHandler(BusinessException.class) + public ResponseEntity handleBusinessException(BusinessException e) { + log.warn("Business exception occurred: {}", e.getMessage()); + + ErrorCode errorCode = e.getErrorCode(); + ErrorResponse response = ErrorResponse.of(errorCode); + + return ResponseEntity + .status(errorCode.getHttpStatus()) + .body(response); + } + + // Validation 예외 처리 + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationException( + MethodArgumentNotValidException e + ) { + log.warn("Validation exception occurred"); + + Map errors = new HashMap<>(); + e.getBindingResult().getAllErrors().forEach(error -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.put(fieldName, errorMessage); + }); + + ErrorResponse response = ErrorResponse.of( + CommonErrorCode.INVALID_INPUT_VALUE.getCode(), + "입력값이 유효하지 않습니다", + errors + ); + + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(response); + } + + // 일반 예외 처리 + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e) { + log.error("Unexpected exception occurred", e); + + ErrorResponse response = ErrorResponse.of(CommonErrorCode.INTERNAL_SERVER_ERROR); + + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(response); + } +} diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/dto/ApiResponse.java b/coachcoach/common/src/main/java/com/coachcoach/common/dto/ApiResponse.java new file mode 100644 index 0000000..0facdc9 --- /dev/null +++ b/coachcoach/common/src/main/java/com/coachcoach/common/dto/ApiResponse.java @@ -0,0 +1,73 @@ +package com.coachcoach.common.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.http.HttpStatus; + +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ApiResponse { + + @Builder.Default + private boolean success = true; // 성공 여부 + + private String message; // 응답 메시지(선택) + + private T data; //실제 데이터 + + @Builder.Default + private LocalDateTime timestamp = LocalDateTime.now(); // 응답 시간 + + @JsonIgnore + @Builder.Default + private HttpStatus status = HttpStatus.OK; + + // Data + public static ApiResponse success(T data) { + return ApiResponse.builder() + .success(true) + .data(data) + .status(HttpStatus.OK) + .build(); + } + + // Data + Message + public static ApiResponse success(T data, String message) { + return ApiResponse.builder() + .success(true) + .message(message) + .data(data) + .status(HttpStatus.OK) + .build(); + } + + // Message - Data X + public static ApiResponse success(String message) { + return ApiResponse.builder() + .success(true) + .message(message) + .status(HttpStatus.OK) + .build(); + } + + public static ApiResponse success(T data, HttpStatus status) { + return ApiResponse.builder() + .success(true) + .data(data) + .status(status) + .build(); + } + + public HttpStatus httpStatus() { + return status; + } +} diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/dto/ErrorResponse.java b/coachcoach/common/src/main/java/com/coachcoach/common/dto/ErrorResponse.java new file mode 100644 index 0000000..69aa60a --- /dev/null +++ b/coachcoach/common/src/main/java/com/coachcoach/common/dto/ErrorResponse.java @@ -0,0 +1,56 @@ +package com.coachcoach.common.dto; + +import com.coachcoach.common.exception.ErrorCode; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Map; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ErrorResponse { + + @Builder.Default + private boolean success = false; // 실패 + + private String code; // 에러 코드 + + private String message; // 에러 메시지 + + private Map errors; // Validation 에러 + + @Builder.Default + private LocalDateTime timestamp = LocalDateTime.now(); // 응답 시간 + + // ErrorCode Enum 활용 에러 + public static ErrorResponse of(ErrorCode errorCode) { + return ErrorResponse.builder() + .code(errorCode.getCode()) + .message(errorCode.getMessage()) + .build(); + } + + // code + message 직접 작성 + public static ErrorResponse of(String code, String message) { + return ErrorResponse.builder() + .code(code) + .message(message) + .build(); + } + + // Validation 에러 + public static ErrorResponse of(String code, String message, Map errors) { + return ErrorResponse.builder() + .code(code) + .message(message) + .errors(errors) + .build(); + } +} diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/exception/BusinessException.java b/coachcoach/common/src/main/java/com/coachcoach/common/exception/BusinessException.java new file mode 100644 index 0000000..bf2bfb7 --- /dev/null +++ b/coachcoach/common/src/main/java/com/coachcoach/common/exception/BusinessException.java @@ -0,0 +1,18 @@ +package com.coachcoach.common.exception; + +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException { + private final ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public BusinessException(ErrorCode errorCode, String message) { + super(message); + this.errorCode = errorCode; + } +} diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/exception/CommonErrorCode.java b/coachcoach/common/src/main/java/com/coachcoach/common/exception/CommonErrorCode.java new file mode 100644 index 0000000..d0a28d1 --- /dev/null +++ b/coachcoach/common/src/main/java/com/coachcoach/common/exception/CommonErrorCode.java @@ -0,0 +1,26 @@ +package com.coachcoach.common.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum CommonErrorCode implements ErrorCode { + // 공통 에러 + INVALID_INPUT_VALUE("COMMON_001", "입력값이 올바르지 않습니다", HttpStatus.BAD_REQUEST), + METHOD_NOT_ALLOWED("COMMON_002", "지원하지 않는 HTTP 메서드입니다", HttpStatus.METHOD_NOT_ALLOWED), + INTERNAL_SERVER_ERROR("COMMON_003", "서버 내부 오류가 발생했습니다", HttpStatus.INTERNAL_SERVER_ERROR), + INVALID_TYPE_VALUE("COMMON_004", "잘못된 타입입니다", HttpStatus.BAD_REQUEST), + MISSING_REQUEST_PARAMETER("COMMON_005", "필수 파라미터가 누락되었습니다", HttpStatus.BAD_REQUEST), + + // 인증/인가 + UNAUTHORIZED("AUTH_001", "인증이 필요합니다", HttpStatus.UNAUTHORIZED), + FORBIDDEN("AUTH_002", "접근 권한이 없습니다", HttpStatus.FORBIDDEN), + INVALID_TOKEN("AUTH_003", "유효하지 않은 토큰입니다", HttpStatus.UNAUTHORIZED), + ; + + private final String code; + private final String message; + private final HttpStatus httpStatus; +} diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/exception/ErrorCode.java b/coachcoach/common/src/main/java/com/coachcoach/common/exception/ErrorCode.java new file mode 100644 index 0000000..a878e44 --- /dev/null +++ b/coachcoach/common/src/main/java/com/coachcoach/common/exception/ErrorCode.java @@ -0,0 +1,9 @@ +package com.coachcoach.common.exception; + +import org.springframework.http.HttpStatus; + +public interface ErrorCode { + String getCode(); + String getMessage(); + HttpStatus getHttpStatus(); +} diff --git a/coachcoach/service/catalog/build.gradle b/coachcoach/service/catalog/build.gradle index f39072c..8bd8fe9 100644 --- a/coachcoach/service/catalog/build.gradle +++ b/coachcoach/service/catalog/build.gradle @@ -10,9 +10,19 @@ ext { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' implementation 'org.springframework.boot:spring-boot-starter-actuator' + + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'io.micrometer:micrometer-registry-prometheus' + + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.postgresql:postgresql' + + implementation 'org.springframework.boot:spring-boot-starter-validation' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + + implementation project(':common') } dependencyManagement { diff --git a/coachcoach/settings.gradle b/coachcoach/settings.gradle index b53732c..1b041b4 100644 --- a/coachcoach/settings.gradle +++ b/coachcoach/settings.gradle @@ -1,9 +1,12 @@ rootProject.name = 'coachcoach' +include 'infra' include 'infra:gateway' include 'infra:eureka' +include 'common' +include 'service' include 'service:auth' include 'service:user-store' include 'service:catalog' From 2080e8ff6d396ed568cde6ca9d1ceb035224835a Mon Sep 17 00:00:00 2001 From: subin930 Date: Mon, 5 Jan 2026 13:37:14 +0900 Subject: [PATCH 11/19] =?UTF-8?q?[Feat]=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=ED=97=A4=EB=8D=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coachcoach/infra/gateway/build.gradle | 1 + .../gateway/config/GatewayConfig.java | 8 +++++++ .../gateway/config/SwaggerConfig.java | 23 ++++++++++++++++++ coachcoach/service/catalog/build.gradle | 2 ++ .../catalog/controller/CatalogController.java | 22 +++++++++++++++++ .../catalog/global/config/SwaggerConfig.java | 24 +++++++++++++++++++ .../global/exception/CatalogErrorCode.java | 16 +++++++++++++ 7 files changed, 96 insertions(+) create mode 100644 coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/SwaggerConfig.java create mode 100644 coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java create mode 100644 coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/SwaggerConfig.java create mode 100644 coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/exception/CatalogErrorCode.java diff --git a/coachcoach/infra/gateway/build.gradle b/coachcoach/infra/gateway/build.gradle index e0f7aa5..6faddee 100644 --- a/coachcoach/infra/gateway/build.gradle +++ b/coachcoach/infra/gateway/build.gradle @@ -14,6 +14,7 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' + implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.4.0' } dependencyManagement { diff --git a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/GatewayConfig.java b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/GatewayConfig.java index 779099b..37a8681 100644 --- a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/GatewayConfig.java +++ b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/GatewayConfig.java @@ -12,15 +12,23 @@ public RouteLocator myRoutes(RouteLocatorBuilder builder) { return builder.routes() .route("auth-service", p -> p.path("/api/auth/**") + .filters(f -> f.rewritePath("/api/auth/(?.*)", "/${segment}")) .uri("lb://auth-service")) .route("user-store-service", p -> p.path("/api/user-store/**") + .filters(f -> f.rewritePath("/api/user-store/(?.*)", "/${segment}")) .uri("lb://user-store-service")) .route("catalog-service", p -> p.path("/api/catalog/**") + .filters( + f -> f + .rewritePath("/api/catalog/(?.*)", "/${segment}") + .addRequestHeader("userId", "1") + ) .uri("lb://catalog-service")) .route("insight-service", p -> p.path("/api/insight/**") + .filters(f -> f.rewritePath("/api/insight/(?.*)", "/${segment}")) .uri("lb://insight-service")) .build(); } diff --git a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/SwaggerConfig.java b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/SwaggerConfig.java new file mode 100644 index 0000000..a65e0db --- /dev/null +++ b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/SwaggerConfig.java @@ -0,0 +1,23 @@ +package com.coachcoach.gateway.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +@OpenAPIDefinition +public class SwaggerConfig { + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("코치코치 API") + .version("v1")); + } +} diff --git a/coachcoach/service/catalog/build.gradle b/coachcoach/service/catalog/build.gradle index 8bd8fe9..e7a4938 100644 --- a/coachcoach/service/catalog/build.gradle +++ b/coachcoach/service/catalog/build.gradle @@ -22,6 +22,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' developmentOnly 'org.springframework.boot:spring-boot-devtools' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0' + implementation project(':common') } diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java new file mode 100644 index 0000000..d9c7632 --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java @@ -0,0 +1,22 @@ +package com.coachcoach.catalog.controller; + +import com.coachcoach.common.dto.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +/** + * @RequestHeader Map headers → headers.get("userId") + */ + +@RestController +@RequiredArgsConstructor +public class CatalogController { + @GetMapping(path = "/hi") + public ApiResponse hi(@RequestHeader Map headers) { + return ApiResponse.success(headers.get("user_id")); + } +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/SwaggerConfig.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/SwaggerConfig.java new file mode 100644 index 0000000..d33adcc --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/SwaggerConfig.java @@ -0,0 +1,24 @@ +package com.coachcoach.catalog.global.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.servers.Server; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@RequiredArgsConstructor +@Configuration +@OpenAPIDefinition +public class SwaggerConfig { + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("코치코치 CATALOG SERVICE 명세서") + .version("v1")) + .addServersItem(new Server().url("/api/catalog")); + } +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/exception/CatalogErrorCode.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/exception/CatalogErrorCode.java new file mode 100644 index 0000000..c2ef73a --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/exception/CatalogErrorCode.java @@ -0,0 +1,16 @@ +package com.coachcoach.catalog.global.exception; + +import com.coachcoach.common.exception.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum CatalogErrorCode implements ErrorCode { + UNAUTHORIZED("AUTH_001", "인증이 필요합니다", HttpStatus.UNAUTHORIZED), + ; + private final String code; + private final String message; + private final HttpStatus httpStatus; +} From fbeace960deb9d37cbaac4ef980d862fd7647b04 Mon Sep 17 00:00:00 2001 From: subin930 Date: Mon, 5 Jan 2026 16:11:27 +0900 Subject: [PATCH 12/19] =?UTF-8?q?[Fix]=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=91=EB=8B=B5=ED=98=95=EC=8B=9D=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Api Gateway에서 응답 래핑 및 공통 에러(서버 에러) 처리 - 비즈니스 에러는 각 모듈에서 래핑해서 반환 --- coachcoach/common/build.gradle | 3 +- .../common/config/ApiResponseAdvice.java | 65 --------- .../common/exception/CommonErrorCode.java | 2 + coachcoach/infra/gateway/build.gradle | 2 + .../gateway/config/SwaggerConfig.java | 3 +- .../gateway/exception/GlobalErrorHandler.java | 104 ++++++++++++++ .../gateway/filter/ResponseWrapperFilter.java | 134 ++++++++++++++++++ coachcoach/service/catalog/build.gradle | 1 - .../config/GlobalExceptionHandler.java | 4 +- .../catalog/global/config/SwaggerConfig.java | 2 +- 10 files changed, 247 insertions(+), 73 deletions(-) delete mode 100644 coachcoach/common/src/main/java/com/coachcoach/common/config/ApiResponseAdvice.java create mode 100644 coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/exception/GlobalErrorHandler.java create mode 100644 coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/filter/ResponseWrapperFilter.java rename coachcoach/{common/src/main/java/com/coachcoach/common => service/catalog/src/main/java/com/coachcoach/catalog/global}/config/GlobalExceptionHandler.java (98%) diff --git a/coachcoach/common/build.gradle b/coachcoach/common/build.gradle index 5be1ffe..0c584ea 100644 --- a/coachcoach/common/build.gradle +++ b/coachcoach/common/build.gradle @@ -4,9 +4,8 @@ plugins { } dependencies { - api 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.springframework.boot:spring-boot-starter-web' api 'org.springframework.boot:spring-boot-starter-validation' - api 'org.springframework.boot:spring-boot-starter-data-jpa' api 'com.fasterxml.jackson.core:jackson-databind' api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/config/ApiResponseAdvice.java b/coachcoach/common/src/main/java/com/coachcoach/common/config/ApiResponseAdvice.java deleted file mode 100644 index 5bdb10d..0000000 --- a/coachcoach/common/src/main/java/com/coachcoach/common/config/ApiResponseAdvice.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.coachcoach.common.config; - -import com.coachcoach.common.dto.ApiResponse; -import com.coachcoach.common.dto.ErrorResponse; -import org.springframework.core.MethodParameter; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.http.server.ServletServerHttpResponse; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; - - -@RestControllerAdvice -public class ApiResponseAdvice implements ResponseBodyAdvice { - - @Override - public boolean supports(MethodParameter returnType, Class> converterType) { - Class parameterType = returnType.getParameterType(); - return !ApiResponse.class.isAssignableFrom(parameterType) - && !ErrorResponse.class.isAssignableFrom(parameterType); - } - - @Override - public Object beforeBodyWrite(Object body, - MethodParameter returnType, - MediaType contentType, - Class> converterType, - ServerHttpRequest request, - ServerHttpResponse response) { - - if (body instanceof ApiResponse || body instanceof ErrorResponse) { - return body; - } - - if (body instanceof String) { - return ApiResponse.success(body); - } - - if (body == null) { - HttpStatus status = extractHttpStatus(response); - - // 204 No Content는 래핑 안 함 - if (status == HttpStatus.NO_CONTENT) { - return null; - } - - return ApiResponse.success(null); - } - - return ApiResponse.success(body); - } - - private HttpStatus extractHttpStatus(ServerHttpResponse response) { - if (response instanceof ServletServerHttpResponse servletResponse) { - HttpStatus resolved = HttpStatus.resolve( - servletResponse.getServletResponse().getStatus() - ); - return resolved != null ? resolved : HttpStatus.OK; - } - return HttpStatus.OK; - } -} \ No newline at end of file diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/exception/CommonErrorCode.java b/coachcoach/common/src/main/java/com/coachcoach/common/exception/CommonErrorCode.java index d0a28d1..6535b4a 100644 --- a/coachcoach/common/src/main/java/com/coachcoach/common/exception/CommonErrorCode.java +++ b/coachcoach/common/src/main/java/com/coachcoach/common/exception/CommonErrorCode.java @@ -13,6 +13,8 @@ public enum CommonErrorCode implements ErrorCode { INTERNAL_SERVER_ERROR("COMMON_003", "서버 내부 오류가 발생했습니다", HttpStatus.INTERNAL_SERVER_ERROR), INVALID_TYPE_VALUE("COMMON_004", "잘못된 타입입니다", HttpStatus.BAD_REQUEST), MISSING_REQUEST_PARAMETER("COMMON_005", "필수 파라미터가 누락되었습니다", HttpStatus.BAD_REQUEST), + SERVICE_UNAVAILABLE("COMMON_006", "서비스에 연결할 수 없습니다", HttpStatus.SERVICE_UNAVAILABLE), + NOT_FOUND("COMMON_007", "요청하신 자원을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), // 인증/인가 UNAUTHORIZED("AUTH_001", "인증이 필요합니다", HttpStatus.UNAUTHORIZED), diff --git a/coachcoach/infra/gateway/build.gradle b/coachcoach/infra/gateway/build.gradle index 6faddee..1872093 100644 --- a/coachcoach/infra/gateway/build.gradle +++ b/coachcoach/infra/gateway/build.gradle @@ -15,6 +15,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.4.0' + + implementation project(':common') } dependencyManagement { diff --git a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/SwaggerConfig.java b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/SwaggerConfig.java index a65e0db..adfc6f2 100644 --- a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/SwaggerConfig.java +++ b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/SwaggerConfig.java @@ -1,6 +1,5 @@ package com.coachcoach.gateway.config; -import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.servers.Server; @@ -11,11 +10,11 @@ import java.util.List; @Configuration -@OpenAPIDefinition public class SwaggerConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() + .openapi("3.0.0") .info(new Info() .title("코치코치 API") .version("v1")); diff --git a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/exception/GlobalErrorHandler.java b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/exception/GlobalErrorHandler.java new file mode 100644 index 0000000..965aedf --- /dev/null +++ b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/exception/GlobalErrorHandler.java @@ -0,0 +1,104 @@ +package com.coachcoach.gateway.exception; + +import com.coachcoach.common.dto.ErrorResponse; +import com.coachcoach.common.exception.CommonErrorCode; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; + +@Slf4j +@Component +@Order(-1) +@RequiredArgsConstructor +public class GlobalErrorHandler implements ErrorWebExceptionHandler { + + private final ObjectMapper objectMapper; + + @Override + public Mono handle(ServerWebExchange exchange, Throwable ex) { + log.error("Gateway error occurred: ", ex); + + ErrorResponse errorResponse; + HttpStatus httpStatus; + + log.info("Gateway error occurred: ", ex); + + // ResponseStatusException 처리 (Gateway 라우팅 오류 등) + if (ex instanceof ResponseStatusException) { + ResponseStatusException rse = (ResponseStatusException) ex; + httpStatus = HttpStatus.valueOf(rse.getStatusCode().value()); + + // 서비스 연결 실패 + if (httpStatus == HttpStatus.SERVICE_UNAVAILABLE || httpStatus == HttpStatus.GATEWAY_TIMEOUT) { + errorResponse = ErrorResponse.of(CommonErrorCode.SERVICE_UNAVAILABLE); + } + + // 라우팅 실패 (404) + else if (httpStatus == HttpStatus.NOT_FOUND) { + errorResponse = ErrorResponse.of(CommonErrorCode.NOT_FOUND); + } + + // 기타 ResponseStatusException + else { + log.error("Unexpected gateway error occurred: ", ex); + errorResponse = ErrorResponse.of(CommonErrorCode.INTERNAL_SERVER_ERROR); + } + } + // 일반 예외 처리 + else { + httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; + errorResponse = ErrorResponse.of(CommonErrorCode.INTERNAL_SERVER_ERROR); + } + + return writeErrorResponse(exchange, errorResponse, httpStatus); + } + + + private Mono writeErrorResponse( + ServerWebExchange exchange, + ErrorResponse errorResponse, + HttpStatus httpStatus + ) { + exchange.getResponse().setStatusCode(httpStatus); + exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); + + try { + byte[] bytes = objectMapper.writeValueAsBytes(errorResponse); + DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); + return exchange.getResponse().writeWith(Mono.just(buffer)); + + } catch (JsonProcessingException e) { + log.error("Error serializing error response", e); + return writeFallbackError(exchange); + } + } + + private Mono writeFallbackError(ServerWebExchange exchange) { + String fallbackJson = """ + { + "success": false, + "code": "COMMON_003", + "message": "서버 내부 오류가 발생했습니다", + "timestamp": "%s" + } + """.formatted(java.time.LocalDateTime.now()); + + DataBuffer buffer = exchange.getResponse().bufferFactory() + .wrap(fallbackJson.getBytes(StandardCharsets.UTF_8)); + + return exchange.getResponse().writeWith(Mono.just(buffer)); + } +} \ No newline at end of file diff --git a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/filter/ResponseWrapperFilter.java b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/filter/ResponseWrapperFilter.java new file mode 100644 index 0000000..2cd119d --- /dev/null +++ b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/filter/ResponseWrapperFilter.java @@ -0,0 +1,134 @@ +package com.coachcoach.gateway.filter; + +import com.coachcoach.common.dto.ApiResponse; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.reactivestreams.Publisher; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpResponseDecorator; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; + +@Slf4j +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class ResponseWrapperFilter implements WebFilter { + + private final ObjectMapper objectMapper; + + public ResponseWrapperFilter(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + String path = exchange.getRequest().getURI().getPath(); + log.info(">>> Filter START - Path: {}", path); + + if (isSwaggerPath(path)) { + log.info(">>> Swagger path, skip wrapping"); + return chain.filter(exchange); + } + + ServerHttpResponse originalResponse = exchange.getResponse(); + DataBufferFactory bufferFactory = originalResponse.bufferFactory(); + + ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) { + @Override + public Mono writeWith(Publisher body) { + log.info(">>> writeWith called"); + + if(body instanceof Flux) { + Flux fluxBody = (Flux) body; + + return super.writeWith(fluxBody.buffer().map(dataBuffers -> { + DataBuffer joinedBuffer = bufferFactory.join(dataBuffers); + byte[] content = new byte[joinedBuffer.readableByteCount()]; + joinedBuffer.read(content); + DataBufferUtils.release(joinedBuffer); + + String responseBody = new String(content, StandardCharsets.UTF_8); + log.info(">>> Original response: {}", responseBody); + + HttpStatus statusCode = (HttpStatus) getDelegate().getStatusCode(); + log.info(">>> Status code: {}", statusCode); + + // 에러 응답인 경우 래핑 X + if (statusCode != null && statusCode.isError()) { + log.info(">>> Error response, skip wrapping"); + return bufferFactory.wrap(content); + } + + // 이미 래핑이 되어 있는 경우 + if(isAlreadyWrapped(responseBody)) { + log.info(">>> Already wrapped, skip"); + return bufferFactory.wrap(content); + } + + // 래핑 + try { + log.info(">>> START wrapping"); + Object originalData = objectMapper.readValue(responseBody, Object.class); + log.info(">>> Parsed data: {}", originalData); + + ApiResponse wrappedResponse = ApiResponse.success(originalData); + log.info(">>> Created ApiResponse: success={}, message={}", + wrappedResponse.isSuccess(), + wrappedResponse.getMessage()); + + byte[] wrappedBytes = objectMapper.writeValueAsBytes(wrappedResponse); + String wrappedJson = new String(wrappedBytes, StandardCharsets.UTF_8); + log.info(">>> Wrapped JSON: {}", wrappedJson); + + // Content-Length 업데이트 + originalResponse.getHeaders().setContentLength(wrappedBytes.length); + log.info(">>> Wrapping SUCCESS"); + + return bufferFactory.wrap(wrappedBytes); + } catch (Exception e) { + log.error(">>> Wrapping FAILED", e); + return bufferFactory.wrap(content); + } + })); + } + + log.warn(">>> Body is not Flux"); + return super.writeWith(body); + } + }; + + return chain.filter(exchange.mutate().response(decoratedResponse).build()); + } + + private boolean isSwaggerPath(String path) { + return path.contains("/v3/api-docs") || + path.contains("/swagger-ui") || + path.contains("/swagger-resources") || + path.contains("/webjars/"); + } + + private boolean isAlreadyWrapped(String responseBody) { + try { + JsonNode node = objectMapper.readTree(responseBody); + boolean wrapped = node.has("success"); + log.info(">>> isAlreadyWrapped check: {}", wrapped); + return wrapped; + } catch (Exception e) { + log.warn(">>> JSON parse failed in isAlreadyWrapped", e); + return false; + } + } +} \ No newline at end of file diff --git a/coachcoach/service/catalog/build.gradle b/coachcoach/service/catalog/build.gradle index e7a4938..4115a7e 100644 --- a/coachcoach/service/catalog/build.gradle +++ b/coachcoach/service/catalog/build.gradle @@ -20,7 +20,6 @@ dependencies { implementation 'org.postgresql:postgresql' implementation 'org.springframework.boot:spring-boot-starter-validation' - developmentOnly 'org.springframework.boot:spring-boot-devtools' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0' diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/config/GlobalExceptionHandler.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/GlobalExceptionHandler.java similarity index 98% rename from coachcoach/common/src/main/java/com/coachcoach/common/config/GlobalExceptionHandler.java rename to coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/GlobalExceptionHandler.java index 90be3d3..7defaae 100644 --- a/coachcoach/common/src/main/java/com/coachcoach/common/config/GlobalExceptionHandler.java +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/GlobalExceptionHandler.java @@ -1,4 +1,4 @@ -package com.coachcoach.common.config; +package com.coachcoach.catalog.global.config; import com.coachcoach.common.dto.ErrorResponse; import com.coachcoach.common.exception.BusinessException; @@ -67,4 +67,4 @@ public ResponseEntity handleException(Exception e) { .status(HttpStatus.INTERNAL_SERVER_ERROR) .body(response); } -} +} \ No newline at end of file diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/SwaggerConfig.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/SwaggerConfig.java index d33adcc..4e79ae1 100644 --- a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/SwaggerConfig.java +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/SwaggerConfig.java @@ -10,12 +10,12 @@ @RequiredArgsConstructor @Configuration -@OpenAPIDefinition public class SwaggerConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() + .openapi("3.0.0") .info(new Info() .title("코치코치 CATALOG SERVICE 명세서") .version("v1")) From 667fa104bd742245ac5b8902c0a6c75b6996449f Mon Sep 17 00:00:00 2001 From: subin930 Date: Mon, 5 Jan 2026 16:11:27 +0900 Subject: [PATCH 13/19] =?UTF-8?q?[Fix]=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=91=EB=8B=B5=ED=98=95=EC=8B=9D=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Api Gateway에서 응답 래핑 및 공통 에러(서버 에러) 처리 - 비즈니스 에러는 각 모듈에서 래핑해서 반환 --- coachcoach/common/build.gradle | 3 +- .../common/config/ApiResponseAdvice.java | 65 --------- .../common/exception/CommonErrorCode.java | 2 + coachcoach/infra/gateway/build.gradle | 2 + .../gateway/config/SwaggerConfig.java | 3 +- .../gateway/exception/GlobalErrorHandler.java | 104 ++++++++++++++ .../gateway/filter/ResponseWrapperFilter.java | 134 ++++++++++++++++++ coachcoach/service/catalog/build.gradle | 1 - .../catalog/controller/CatalogController.java | 12 +- .../config/GlobalExceptionHandler.java | 4 +- .../catalog/global/config/SwaggerConfig.java | 2 +- 11 files changed, 250 insertions(+), 82 deletions(-) delete mode 100644 coachcoach/common/src/main/java/com/coachcoach/common/config/ApiResponseAdvice.java create mode 100644 coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/exception/GlobalErrorHandler.java create mode 100644 coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/filter/ResponseWrapperFilter.java rename coachcoach/{common/src/main/java/com/coachcoach/common => service/catalog/src/main/java/com/coachcoach/catalog/global}/config/GlobalExceptionHandler.java (98%) diff --git a/coachcoach/common/build.gradle b/coachcoach/common/build.gradle index 5be1ffe..0c584ea 100644 --- a/coachcoach/common/build.gradle +++ b/coachcoach/common/build.gradle @@ -4,9 +4,8 @@ plugins { } dependencies { - api 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.springframework.boot:spring-boot-starter-web' api 'org.springframework.boot:spring-boot-starter-validation' - api 'org.springframework.boot:spring-boot-starter-data-jpa' api 'com.fasterxml.jackson.core:jackson-databind' api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/config/ApiResponseAdvice.java b/coachcoach/common/src/main/java/com/coachcoach/common/config/ApiResponseAdvice.java deleted file mode 100644 index 5bdb10d..0000000 --- a/coachcoach/common/src/main/java/com/coachcoach/common/config/ApiResponseAdvice.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.coachcoach.common.config; - -import com.coachcoach.common.dto.ApiResponse; -import com.coachcoach.common.dto.ErrorResponse; -import org.springframework.core.MethodParameter; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.http.server.ServletServerHttpResponse; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; - - -@RestControllerAdvice -public class ApiResponseAdvice implements ResponseBodyAdvice { - - @Override - public boolean supports(MethodParameter returnType, Class> converterType) { - Class parameterType = returnType.getParameterType(); - return !ApiResponse.class.isAssignableFrom(parameterType) - && !ErrorResponse.class.isAssignableFrom(parameterType); - } - - @Override - public Object beforeBodyWrite(Object body, - MethodParameter returnType, - MediaType contentType, - Class> converterType, - ServerHttpRequest request, - ServerHttpResponse response) { - - if (body instanceof ApiResponse || body instanceof ErrorResponse) { - return body; - } - - if (body instanceof String) { - return ApiResponse.success(body); - } - - if (body == null) { - HttpStatus status = extractHttpStatus(response); - - // 204 No Content는 래핑 안 함 - if (status == HttpStatus.NO_CONTENT) { - return null; - } - - return ApiResponse.success(null); - } - - return ApiResponse.success(body); - } - - private HttpStatus extractHttpStatus(ServerHttpResponse response) { - if (response instanceof ServletServerHttpResponse servletResponse) { - HttpStatus resolved = HttpStatus.resolve( - servletResponse.getServletResponse().getStatus() - ); - return resolved != null ? resolved : HttpStatus.OK; - } - return HttpStatus.OK; - } -} \ No newline at end of file diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/exception/CommonErrorCode.java b/coachcoach/common/src/main/java/com/coachcoach/common/exception/CommonErrorCode.java index d0a28d1..6535b4a 100644 --- a/coachcoach/common/src/main/java/com/coachcoach/common/exception/CommonErrorCode.java +++ b/coachcoach/common/src/main/java/com/coachcoach/common/exception/CommonErrorCode.java @@ -13,6 +13,8 @@ public enum CommonErrorCode implements ErrorCode { INTERNAL_SERVER_ERROR("COMMON_003", "서버 내부 오류가 발생했습니다", HttpStatus.INTERNAL_SERVER_ERROR), INVALID_TYPE_VALUE("COMMON_004", "잘못된 타입입니다", HttpStatus.BAD_REQUEST), MISSING_REQUEST_PARAMETER("COMMON_005", "필수 파라미터가 누락되었습니다", HttpStatus.BAD_REQUEST), + SERVICE_UNAVAILABLE("COMMON_006", "서비스에 연결할 수 없습니다", HttpStatus.SERVICE_UNAVAILABLE), + NOT_FOUND("COMMON_007", "요청하신 자원을 찾을 수 없습니다.", HttpStatus.NOT_FOUND), // 인증/인가 UNAUTHORIZED("AUTH_001", "인증이 필요합니다", HttpStatus.UNAUTHORIZED), diff --git a/coachcoach/infra/gateway/build.gradle b/coachcoach/infra/gateway/build.gradle index 6faddee..1872093 100644 --- a/coachcoach/infra/gateway/build.gradle +++ b/coachcoach/infra/gateway/build.gradle @@ -15,6 +15,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.4.0' + + implementation project(':common') } dependencyManagement { diff --git a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/SwaggerConfig.java b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/SwaggerConfig.java index a65e0db..adfc6f2 100644 --- a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/SwaggerConfig.java +++ b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/config/SwaggerConfig.java @@ -1,6 +1,5 @@ package com.coachcoach.gateway.config; -import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.servers.Server; @@ -11,11 +10,11 @@ import java.util.List; @Configuration -@OpenAPIDefinition public class SwaggerConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() + .openapi("3.0.0") .info(new Info() .title("코치코치 API") .version("v1")); diff --git a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/exception/GlobalErrorHandler.java b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/exception/GlobalErrorHandler.java new file mode 100644 index 0000000..965aedf --- /dev/null +++ b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/exception/GlobalErrorHandler.java @@ -0,0 +1,104 @@ +package com.coachcoach.gateway.exception; + +import com.coachcoach.common.dto.ErrorResponse; +import com.coachcoach.common.exception.CommonErrorCode; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; + +@Slf4j +@Component +@Order(-1) +@RequiredArgsConstructor +public class GlobalErrorHandler implements ErrorWebExceptionHandler { + + private final ObjectMapper objectMapper; + + @Override + public Mono handle(ServerWebExchange exchange, Throwable ex) { + log.error("Gateway error occurred: ", ex); + + ErrorResponse errorResponse; + HttpStatus httpStatus; + + log.info("Gateway error occurred: ", ex); + + // ResponseStatusException 처리 (Gateway 라우팅 오류 등) + if (ex instanceof ResponseStatusException) { + ResponseStatusException rse = (ResponseStatusException) ex; + httpStatus = HttpStatus.valueOf(rse.getStatusCode().value()); + + // 서비스 연결 실패 + if (httpStatus == HttpStatus.SERVICE_UNAVAILABLE || httpStatus == HttpStatus.GATEWAY_TIMEOUT) { + errorResponse = ErrorResponse.of(CommonErrorCode.SERVICE_UNAVAILABLE); + } + + // 라우팅 실패 (404) + else if (httpStatus == HttpStatus.NOT_FOUND) { + errorResponse = ErrorResponse.of(CommonErrorCode.NOT_FOUND); + } + + // 기타 ResponseStatusException + else { + log.error("Unexpected gateway error occurred: ", ex); + errorResponse = ErrorResponse.of(CommonErrorCode.INTERNAL_SERVER_ERROR); + } + } + // 일반 예외 처리 + else { + httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; + errorResponse = ErrorResponse.of(CommonErrorCode.INTERNAL_SERVER_ERROR); + } + + return writeErrorResponse(exchange, errorResponse, httpStatus); + } + + + private Mono writeErrorResponse( + ServerWebExchange exchange, + ErrorResponse errorResponse, + HttpStatus httpStatus + ) { + exchange.getResponse().setStatusCode(httpStatus); + exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); + + try { + byte[] bytes = objectMapper.writeValueAsBytes(errorResponse); + DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); + return exchange.getResponse().writeWith(Mono.just(buffer)); + + } catch (JsonProcessingException e) { + log.error("Error serializing error response", e); + return writeFallbackError(exchange); + } + } + + private Mono writeFallbackError(ServerWebExchange exchange) { + String fallbackJson = """ + { + "success": false, + "code": "COMMON_003", + "message": "서버 내부 오류가 발생했습니다", + "timestamp": "%s" + } + """.formatted(java.time.LocalDateTime.now()); + + DataBuffer buffer = exchange.getResponse().bufferFactory() + .wrap(fallbackJson.getBytes(StandardCharsets.UTF_8)); + + return exchange.getResponse().writeWith(Mono.just(buffer)); + } +} \ No newline at end of file diff --git a/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/filter/ResponseWrapperFilter.java b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/filter/ResponseWrapperFilter.java new file mode 100644 index 0000000..2cd119d --- /dev/null +++ b/coachcoach/infra/gateway/src/main/java/com/coachcoach/gateway/filter/ResponseWrapperFilter.java @@ -0,0 +1,134 @@ +package com.coachcoach.gateway.filter; + +import com.coachcoach.common.dto.ApiResponse; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.reactivestreams.Publisher; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpResponseDecorator; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; + +@Slf4j +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class ResponseWrapperFilter implements WebFilter { + + private final ObjectMapper objectMapper; + + public ResponseWrapperFilter(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + String path = exchange.getRequest().getURI().getPath(); + log.info(">>> Filter START - Path: {}", path); + + if (isSwaggerPath(path)) { + log.info(">>> Swagger path, skip wrapping"); + return chain.filter(exchange); + } + + ServerHttpResponse originalResponse = exchange.getResponse(); + DataBufferFactory bufferFactory = originalResponse.bufferFactory(); + + ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) { + @Override + public Mono writeWith(Publisher body) { + log.info(">>> writeWith called"); + + if(body instanceof Flux) { + Flux fluxBody = (Flux) body; + + return super.writeWith(fluxBody.buffer().map(dataBuffers -> { + DataBuffer joinedBuffer = bufferFactory.join(dataBuffers); + byte[] content = new byte[joinedBuffer.readableByteCount()]; + joinedBuffer.read(content); + DataBufferUtils.release(joinedBuffer); + + String responseBody = new String(content, StandardCharsets.UTF_8); + log.info(">>> Original response: {}", responseBody); + + HttpStatus statusCode = (HttpStatus) getDelegate().getStatusCode(); + log.info(">>> Status code: {}", statusCode); + + // 에러 응답인 경우 래핑 X + if (statusCode != null && statusCode.isError()) { + log.info(">>> Error response, skip wrapping"); + return bufferFactory.wrap(content); + } + + // 이미 래핑이 되어 있는 경우 + if(isAlreadyWrapped(responseBody)) { + log.info(">>> Already wrapped, skip"); + return bufferFactory.wrap(content); + } + + // 래핑 + try { + log.info(">>> START wrapping"); + Object originalData = objectMapper.readValue(responseBody, Object.class); + log.info(">>> Parsed data: {}", originalData); + + ApiResponse wrappedResponse = ApiResponse.success(originalData); + log.info(">>> Created ApiResponse: success={}, message={}", + wrappedResponse.isSuccess(), + wrappedResponse.getMessage()); + + byte[] wrappedBytes = objectMapper.writeValueAsBytes(wrappedResponse); + String wrappedJson = new String(wrappedBytes, StandardCharsets.UTF_8); + log.info(">>> Wrapped JSON: {}", wrappedJson); + + // Content-Length 업데이트 + originalResponse.getHeaders().setContentLength(wrappedBytes.length); + log.info(">>> Wrapping SUCCESS"); + + return bufferFactory.wrap(wrappedBytes); + } catch (Exception e) { + log.error(">>> Wrapping FAILED", e); + return bufferFactory.wrap(content); + } + })); + } + + log.warn(">>> Body is not Flux"); + return super.writeWith(body); + } + }; + + return chain.filter(exchange.mutate().response(decoratedResponse).build()); + } + + private boolean isSwaggerPath(String path) { + return path.contains("/v3/api-docs") || + path.contains("/swagger-ui") || + path.contains("/swagger-resources") || + path.contains("/webjars/"); + } + + private boolean isAlreadyWrapped(String responseBody) { + try { + JsonNode node = objectMapper.readTree(responseBody); + boolean wrapped = node.has("success"); + log.info(">>> isAlreadyWrapped check: {}", wrapped); + return wrapped; + } catch (Exception e) { + log.warn(">>> JSON parse failed in isAlreadyWrapped", e); + return false; + } + } +} \ No newline at end of file diff --git a/coachcoach/service/catalog/build.gradle b/coachcoach/service/catalog/build.gradle index e7a4938..4115a7e 100644 --- a/coachcoach/service/catalog/build.gradle +++ b/coachcoach/service/catalog/build.gradle @@ -20,7 +20,6 @@ dependencies { implementation 'org.postgresql:postgresql' implementation 'org.springframework.boot:spring-boot-starter-validation' - developmentOnly 'org.springframework.boot:spring-boot-devtools' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0' diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java index d9c7632..ebccf64 100644 --- a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java @@ -1,22 +1,16 @@ package com.coachcoach.catalog.controller; -import com.coachcoach.common.dto.ApiResponse; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; -import java.util.Map; /** - * @RequestHeader Map headers → headers.get("userId") + * @RequestHeader(value = "userId", required = false)로 헤더 GET + * return 자료형으로 원시 자료형 사용 불가 (무조건 DTO로 래핑 / 참조 자료형 사용) */ @RestController @RequiredArgsConstructor public class CatalogController { - @GetMapping(path = "/hi") - public ApiResponse hi(@RequestHeader Map headers) { - return ApiResponse.success(headers.get("user_id")); - } + } diff --git a/coachcoach/common/src/main/java/com/coachcoach/common/config/GlobalExceptionHandler.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/GlobalExceptionHandler.java similarity index 98% rename from coachcoach/common/src/main/java/com/coachcoach/common/config/GlobalExceptionHandler.java rename to coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/GlobalExceptionHandler.java index 90be3d3..7defaae 100644 --- a/coachcoach/common/src/main/java/com/coachcoach/common/config/GlobalExceptionHandler.java +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/GlobalExceptionHandler.java @@ -1,4 +1,4 @@ -package com.coachcoach.common.config; +package com.coachcoach.catalog.global.config; import com.coachcoach.common.dto.ErrorResponse; import com.coachcoach.common.exception.BusinessException; @@ -67,4 +67,4 @@ public ResponseEntity handleException(Exception e) { .status(HttpStatus.INTERNAL_SERVER_ERROR) .body(response); } -} +} \ No newline at end of file diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/SwaggerConfig.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/SwaggerConfig.java index d33adcc..4e79ae1 100644 --- a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/SwaggerConfig.java +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/config/SwaggerConfig.java @@ -10,12 +10,12 @@ @RequiredArgsConstructor @Configuration -@OpenAPIDefinition public class SwaggerConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() + .openapi("3.0.0") .info(new Info() .title("코치코치 CATALOG SERVICE 명세서") .version("v1")) From bf4fb4f1f1658c982fcccfeb077e80709ef10cc1 Mon Sep 17 00:00:00 2001 From: subin930 Date: Mon, 5 Jan 2026 21:42:38 +0900 Subject: [PATCH 14/19] =?UTF-8?q?[Feat]=20=EC=9E=AC=EB=A3=8C=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EB=93=B1=EB=A1=9D=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../catalog/controller/CatalogController.java | 15 +++++++ .../catalog/entity/IngredientCategory.java | 36 +++++++++++++++++ .../global/exception/CatalogErrorCode.java | 2 +- .../IngredientCategoryRepository.java | 10 +++++ .../catalog/service/CatalogService.java | 39 +++++++++++++++++++ .../IngredientCategoryCreateRequest.java | 12 ++++++ .../response/IngredientCategoryResponse.java | 25 ++++++++++++ 7 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/entity/IngredientCategory.java create mode 100644 coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/IngredientCategoryRepository.java create mode 100644 coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/CatalogService.java create mode 100644 coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/request/IngredientCategoryCreateRequest.java create mode 100644 coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/response/IngredientCategoryResponse.java diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java index ebccf64..93c6b12 100644 --- a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java @@ -1,6 +1,12 @@ package com.coachcoach.catalog.controller; +import com.coachcoach.catalog.service.CatalogService; +import com.coachcoach.catalog.service.request.IngredientCategoryCreateRequest; +import com.coachcoach.catalog.service.response.IngredientCategoryResponse; import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @@ -13,4 +19,13 @@ @RequiredArgsConstructor public class CatalogController { + private final CatalogService catalogService; + + /** + * 재료 카테고리 생성 + */ + @PostMapping("/ingredients/category") + public IngredientCategoryResponse createIngredientCategory(@RequestHeader(name = "userId", required = false) String userId, @RequestBody IngredientCategoryCreateRequest request) { + return catalogService.createIngredientCategory(Long.valueOf(userId), request); + } } diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/entity/IngredientCategory.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/entity/IngredientCategory.java new file mode 100644 index 0000000..de4c70a --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/entity/IngredientCategory.java @@ -0,0 +1,36 @@ +package com.coachcoach.catalog.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Table(name = "tb_ingredient_category") +@Getter +@Entity +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class IngredientCategory { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ingredient_category_id") + private Long categoryId; + private Long userId; + private String categoryName; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public static IngredientCategory create(Long userId, String categoryName) { + IngredientCategory ic = new IngredientCategory(); + + ic.userId = userId; + ic.categoryName = categoryName; + ic.createdAt = LocalDateTime.now(); + ic.updatedAt = LocalDateTime.now(); + + return ic; + } +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/exception/CatalogErrorCode.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/exception/CatalogErrorCode.java index c2ef73a..7b4ca21 100644 --- a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/exception/CatalogErrorCode.java +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/global/exception/CatalogErrorCode.java @@ -8,7 +8,7 @@ @Getter @RequiredArgsConstructor public enum CatalogErrorCode implements ErrorCode { - UNAUTHORIZED("AUTH_001", "인증이 필요합니다", HttpStatus.UNAUTHORIZED), + DUPCATEGORY("CATALOG_001", "이미 등록된 카테고리입니다", HttpStatus.CONFLICT), ; private final String code; private final String message; diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/IngredientCategoryRepository.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/IngredientCategoryRepository.java new file mode 100644 index 0000000..d8b3def --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/IngredientCategoryRepository.java @@ -0,0 +1,10 @@ +package com.coachcoach.catalog.repository; + +import com.coachcoach.catalog.entity.IngredientCategory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface IngredientCategoryRepository extends JpaRepository { + boolean existsByUserIdAndCategoryName(Long userId, String categoryName); +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/CatalogService.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/CatalogService.java new file mode 100644 index 0000000..c194a28 --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/CatalogService.java @@ -0,0 +1,39 @@ +package com.coachcoach.catalog.service; + +import com.coachcoach.catalog.entity.IngredientCategory; +import com.coachcoach.catalog.global.exception.CatalogErrorCode; +import com.coachcoach.catalog.repository.IngredientCategoryRepository; +import com.coachcoach.catalog.service.request.IngredientCategoryCreateRequest; +import com.coachcoach.catalog.service.response.IngredientCategoryResponse; +import com.coachcoach.common.exception.BusinessException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CatalogService { + + private final IngredientCategoryRepository ingredientCategoryRepository; + + /** + * 재료 카테고리 생성 + */ + @Transactional + public IngredientCategoryResponse createIngredientCategory(Long userId, IngredientCategoryCreateRequest request) { + // 중복 여부 확인 + if(ingredientCategoryRepository.existsByUserIdAndCategoryName( + userId, request.getCategoryName() + )) { + throw new BusinessException(CatalogErrorCode.DUPCATEGORY); + } + + IngredientCategory ic = ingredientCategoryRepository.save( + IngredientCategory.create( + userId, + request.getCategoryName() + )); + + return IngredientCategoryResponse.from(ic); + } +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/request/IngredientCategoryCreateRequest.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/request/IngredientCategoryCreateRequest.java new file mode 100644 index 0000000..f29ee7b --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/request/IngredientCategoryCreateRequest.java @@ -0,0 +1,12 @@ +package com.coachcoach.catalog.service.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class IngredientCategoryCreateRequest { + @NotBlank(message = "카테고리는 필수입니다.") + private String categoryName; +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/response/IngredientCategoryResponse.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/response/IngredientCategoryResponse.java new file mode 100644 index 0000000..9d40df9 --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/response/IngredientCategoryResponse.java @@ -0,0 +1,25 @@ +package com.coachcoach.catalog.service.response; + +import com.coachcoach.catalog.entity.IngredientCategory; +import lombok.Getter; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Getter +@ToString +public class IngredientCategoryResponse { + private Long categoryId; + private Long userId; + private String categoryName; + + public static IngredientCategoryResponse from(IngredientCategory ic) { + IngredientCategoryResponse response = new IngredientCategoryResponse(); + + response.categoryId = ic.getCategoryId(); + response.userId = ic.getUserId(); + response.categoryName = ic.getCategoryName(); + + return response; + } +} From 35033d2f71ad5a7264dda627e1a75835edc3cf26 Mon Sep 17 00:00:00 2001 From: subin930 Date: Mon, 5 Jan 2026 22:17:09 +0900 Subject: [PATCH 15/19] =?UTF-8?q?[Feat]=20=EB=A9=94=EB=89=B4=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EB=93=B1=EB=A1=9D=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 메뉴 카테고리 등록 구현 & 재료 카테고리 등록 시 Valid 검사 로직 추가 --- .../catalog/controller/CatalogController.java | 23 +++++++++++- .../catalog/entity/MenuCategory.java | 35 +++++++++++++++++++ .../repository/MenuCategoryRepository.java | 10 ++++++ .../catalog/service/CatalogService.java | 33 +++++++++++++++-- .../request/MenuCategoryCreateRequest.java | 12 +++++++ .../response/MenuCategoryResponse.java | 26 ++++++++++++++ 6 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/entity/MenuCategory.java create mode 100644 coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/MenuCategoryRepository.java create mode 100644 coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/request/MenuCategoryCreateRequest.java create mode 100644 coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/response/MenuCategoryResponse.java diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java index 93c6b12..0630f4a 100644 --- a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java @@ -2,7 +2,12 @@ import com.coachcoach.catalog.service.CatalogService; import com.coachcoach.catalog.service.request.IngredientCategoryCreateRequest; +import com.coachcoach.catalog.service.request.MenuCategoryCreateRequest; import com.coachcoach.catalog.service.response.IngredientCategoryResponse; +import com.coachcoach.catalog.service.response.MenuCategoryResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -24,8 +29,24 @@ public class CatalogController { /** * 재료 카테고리 생성 */ + @Operation(summary = "재료 카테고리 생성", description = "📍인증 구현 X
📍유저가 중복 카테고리를 생성하려고 시도 시 CATALOG_001 에러 발생") @PostMapping("/ingredients/category") - public IngredientCategoryResponse createIngredientCategory(@RequestHeader(name = "userId", required = false) String userId, @RequestBody IngredientCategoryCreateRequest request) { + public IngredientCategoryResponse createIngredientCategory( + @RequestHeader(name = "userId", required = false) String userId, + @Valid @RequestBody IngredientCategoryCreateRequest request + ) { return catalogService.createIngredientCategory(Long.valueOf(userId), request); } + + /** + * 메뉴 카테고리 생성 + */ + @Operation(summary = "메뉴 카테고리 생성", description = "📍인증 구현 X
📍유저가 중복 카테고리를 생성하려고 시도 시 CATALOG_001 에러 발생") + @PostMapping("/menu/category") + public MenuCategoryResponse createMenuCategory( + @RequestHeader(name = "userId", required = false) String userId, + @Valid @RequestBody MenuCategoryCreateRequest request + ) { + return catalogService.createMenuCategory(Long.valueOf(userId), request); + } } diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/entity/MenuCategory.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/entity/MenuCategory.java new file mode 100644 index 0000000..a5faa69 --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/entity/MenuCategory.java @@ -0,0 +1,35 @@ +package com.coachcoach.catalog.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Table(name = "tb_menu_category") +@Getter +@Entity +@ToString +@NoArgsConstructor +public class MenuCategory { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "menu_category_id") + private Long categoryId; + private Long userId; + private String categoryName; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public static MenuCategory create(Long userId, String categoryName) { + MenuCategory mc = new MenuCategory(); + + mc.userId = userId; + mc.categoryName = categoryName; + mc.createdAt = LocalDateTime.now(); + mc.updatedAt = LocalDateTime.now(); + + return mc; + } +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/MenuCategoryRepository.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/MenuCategoryRepository.java new file mode 100644 index 0000000..a4c9d34 --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/MenuCategoryRepository.java @@ -0,0 +1,10 @@ +package com.coachcoach.catalog.repository; + +import com.coachcoach.catalog.entity.MenuCategory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MenuCategoryRepository extends JpaRepository { + boolean existsByUserIdAndCategoryName(Long userId, String categoryName); +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/CatalogService.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/CatalogService.java index c194a28..89c5a29 100644 --- a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/CatalogService.java +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/CatalogService.java @@ -1,10 +1,14 @@ package com.coachcoach.catalog.service; import com.coachcoach.catalog.entity.IngredientCategory; +import com.coachcoach.catalog.entity.MenuCategory; import com.coachcoach.catalog.global.exception.CatalogErrorCode; import com.coachcoach.catalog.repository.IngredientCategoryRepository; +import com.coachcoach.catalog.repository.MenuCategoryRepository; import com.coachcoach.catalog.service.request.IngredientCategoryCreateRequest; +import com.coachcoach.catalog.service.request.MenuCategoryCreateRequest; import com.coachcoach.catalog.service.response.IngredientCategoryResponse; +import com.coachcoach.catalog.service.response.MenuCategoryResponse; import com.coachcoach.common.exception.BusinessException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -15,6 +19,7 @@ public class CatalogService { private final IngredientCategoryRepository ingredientCategoryRepository; + private final MenuCategoryRepository menuCategoryRepository; /** * 재료 카테고리 생성 @@ -30,10 +35,32 @@ public IngredientCategoryResponse createIngredientCategory(Long userId, Ingredie IngredientCategory ic = ingredientCategoryRepository.save( IngredientCategory.create( - userId, - request.getCategoryName() - )); + userId, + request.getCategoryName() + )); return IngredientCategoryResponse.from(ic); } + + /** + * 메뉴 카테고리 생성 + */ + @Transactional + public MenuCategoryResponse createMenuCategory(Long userId, MenuCategoryCreateRequest request) { + // 중복 여부 확인 + if(menuCategoryRepository.existsByUserIdAndCategoryName( + userId, request.getCategoryName() + )) { + throw new BusinessException(CatalogErrorCode.DUPCATEGORY); + } + + MenuCategory mc = menuCategoryRepository.save( + MenuCategory.create( + userId, + request.getCategoryName() + )); + + return MenuCategoryResponse.from(mc); + } + } diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/request/MenuCategoryCreateRequest.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/request/MenuCategoryCreateRequest.java new file mode 100644 index 0000000..b3b1200 --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/request/MenuCategoryCreateRequest.java @@ -0,0 +1,12 @@ +package com.coachcoach.catalog.service.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class MenuCategoryCreateRequest { + @NotBlank(message = "카테고리는 필수입니다.") + private String categoryName; +} diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/response/MenuCategoryResponse.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/response/MenuCategoryResponse.java new file mode 100644 index 0000000..0e58b45 --- /dev/null +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/response/MenuCategoryResponse.java @@ -0,0 +1,26 @@ +package com.coachcoach.catalog.service.response; + +import com.coachcoach.catalog.entity.MenuCategory; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Getter +@ToString +public class MenuCategoryResponse { + private Long categoryId; + private Long userId; + private String categoryName; + + public static MenuCategoryResponse from(MenuCategory mc) { + MenuCategoryResponse response = new MenuCategoryResponse(); + + response.categoryId = mc.getCategoryId(); + response.userId = mc.getUserId(); + response.categoryName = mc.getCategoryName(); + + return response; + } +} From 2cad8e87d04f63a6837a596afe05dd3c192437c8 Mon Sep 17 00:00:00 2001 From: subin930 Date: Tue, 6 Jan 2026 12:10:56 +0900 Subject: [PATCH 16/19] =?UTF-8?q?[Feat]=20=EC=9E=AC=EB=A3=8C/=EB=A9=94?= =?UTF-8?q?=EB=89=B4=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../catalog/controller/CatalogController.java | 25 ++++++++++++++++--- .../IngredientCategoryRepository.java | 3 +++ .../repository/MenuCategoryRepository.java | 3 +++ .../catalog/service/CatalogService.java | 25 +++++++++++++++++++ 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java index 0630f4a..d6e4a79 100644 --- a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/controller/CatalogController.java @@ -9,10 +9,9 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.List; /** @@ -49,4 +48,22 @@ public MenuCategoryResponse createMenuCategory( ) { return catalogService.createMenuCategory(Long.valueOf(userId), request); } + + /** + * 재료 카테고리 목록 조회 + */ + @Operation(summary = "재료 카테고리 목록 조회", description = "📍인증 구현 X
📍유저 별 생성한 재료 카테고리 목록 조회(생성 시간 기준 오름차순)") + @GetMapping("/ingredients/category") + public List readIngredientCategory(@RequestHeader(name = "userId", required = false) String userId) { + return catalogService.readIngredientCategory(2L); + } + + /** + * 메뉴 카테고리 목록 조회 + */ + @Operation(summary = "메뉴 카테고리 목록 조회", description = "📍인증 구현 X
📍유저 별 생성한 메뉴 카테고리 목록 조회(생성 시간 기준 오름차순)") + @GetMapping("/menu/category") + public List readMenuCategory(@RequestHeader(name = "userId", required = false) String userId) { + return catalogService.readMenuCategory(2L); + } } diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/IngredientCategoryRepository.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/IngredientCategoryRepository.java index d8b3def..50f497c 100644 --- a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/IngredientCategoryRepository.java +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/IngredientCategoryRepository.java @@ -4,7 +4,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface IngredientCategoryRepository extends JpaRepository { boolean existsByUserIdAndCategoryName(Long userId, String categoryName); + List findByUserIdOrderByCreatedAtAsc(Long userId); } diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/MenuCategoryRepository.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/MenuCategoryRepository.java index a4c9d34..47044d9 100644 --- a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/MenuCategoryRepository.java +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/repository/MenuCategoryRepository.java @@ -4,7 +4,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface MenuCategoryRepository extends JpaRepository { boolean existsByUserIdAndCategoryName(Long userId, String categoryName); + List findByUserIdOrderByCreatedAtAsc(Long userId); } diff --git a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/CatalogService.java b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/CatalogService.java index 89c5a29..34f9e74 100644 --- a/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/CatalogService.java +++ b/coachcoach/service/catalog/src/main/java/com/coachcoach/catalog/service/CatalogService.java @@ -14,6 +14,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Service @RequiredArgsConstructor public class CatalogService { @@ -63,4 +65,27 @@ public MenuCategoryResponse createMenuCategory(Long userId, MenuCategoryCreateR return MenuCategoryResponse.from(mc); } + /** + * 재료 카테고리 목록 조회 + */ + public List readIngredientCategory(Long userId) { + //정렬 조건: 생성한 시점 순 + List result = ingredientCategoryRepository.findByUserIdOrderByCreatedAtAsc(userId); + + return result.stream() + .map(IngredientCategoryResponse::from) + .toList(); + } + + /** + * 메뉴 카테고리 목록 조회 + */ + public List readMenuCategory(Long userId) { + //정렬 조건: 생성한 시점 순 + List result = menuCategoryRepository.findByUserIdOrderByCreatedAtAsc(userId); + + return result.stream() + .map(MenuCategoryResponse::from) + .toList(); + } } From cfdbd382743633ca624731fb65200033734f86a3 Mon Sep 17 00:00:00 2001 From: subin930 Date: Wed, 7 Jan 2026 14:50:03 +0900 Subject: [PATCH 17/19] =?UTF-8?q?[Fix]=20catalog=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-prod.yml | 33 +------ .../src/main/resources/application.yml | 94 +++++++++++++++++++ 2 files changed, 99 insertions(+), 28 deletions(-) create mode 100644 coachcoach/service/catalog/src/main/resources/application.yml diff --git a/coachcoach/service/catalog/src/main/resources/application-prod.yml b/coachcoach/service/catalog/src/main/resources/application-prod.yml index cd56ece..dafab71 100644 --- a/coachcoach/service/catalog/src/main/resources/application-prod.yml +++ b/coachcoach/service/catalog/src/main/resources/application-prod.yml @@ -1,33 +1,10 @@ -server.port: 9002 spring: - application: - name: catalog-service + datasource: + url: ${DATASOURCE_URL} + username: ${DATASOURCE_USERNAME} + password: ${DATASOURCE_PASSWORD} eureka: - instance: - prefer-ip-address: true # 호스트명 대신 IP로 등록 - metadata-map: - prometheus.scrape: true # 수집 대상 client: - register-with-eureka: true - fetch-registry: true service-url: - defaultZone: http://10.0.0.107:8761/eureka/ -management: - endpoints: - web: - exposure: - include: health,info,prometheus,metrics - base-path: /actuator - endpoint: - health: - show-details: always - prometheus: - enabled: true - metrics: - tags: - application: ${spring.application.name} - prometheus: - metrics: - export: - enabled: true \ No newline at end of file + defaultZone: ${EUREKA_URL} \ No newline at end of file diff --git a/coachcoach/service/catalog/src/main/resources/application.yml b/coachcoach/service/catalog/src/main/resources/application.yml new file mode 100644 index 0000000..63af6b4 --- /dev/null +++ b/coachcoach/service/catalog/src/main/resources/application.yml @@ -0,0 +1,94 @@ +server: + port: 9002 + servlet: + encoding: + charset: UTF-8 + enabled: true + force: true + shutdown: graceful # 실행 중인 모은 요청을 수행한 후 종료 + error: + include-message: always # 에러 메시지 항상 포함 + include-binding-errors: always # Validation 에러 항상 포함 + include-stacktrace: on_param + +spring: + application: + name: catalog-service + jpa: + hibernate: + ddl-auto: none + open-in-view: false + show-sql: true + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 100 + jdbc: + batch_size: 20 + order_inserts: true + order_updates: true + database-platform: org.hibernate.dialect.PostgreSQLDialect + datasource: + driver-class-name: org.postgresql.Driver + hikari: + maximum-pool-size: 10 # 최대 커넥션 수 + minimum-idle: 5 # 최소 유휴 커넥션 + connection-timeout: 30000 # 30초 + idle-timeout: 600000 # 10분 + max-lifetime: 1800000 # 30분 + connection-test-query: SELECT 1 # 커넥션 검증 쿼리 + jackson: + serialization: + write-dates-as-timestamps: false # ISO-8601 형식 사용 + time-zone: Asia/Seoul + default-property-inclusion: non_null # null 필드 제외 + +eureka: + instance: + prefer-ip-address: true # 호스트명 대신 IP로 등록 + metadata-map: + prometheus.scrape: true # 수집 대상 + client: + register-with-eureka: true + fetch-registry: true + +management: + endpoints: + web: + exposure: + include: health,info,prometheus,metrics + base-path: /actuator + endpoint: + health: + show-details: always + prometheus: + enabled: true + metrics: + tags: + application: ${spring.application.name} + prometheus: + metrics: + export: + enabled: true + + +logging: + level: + chord.catalog: DEBUG # 프로젝트 로그 레벨 + org.hibernate.SQL: DEBUG # SQL 로그 + org.hibernate.type.descriptor.sql.BasicBinder: TRACE # 바인딩 파라미터 + org.springframework.web: INFO + org.springframework.cloud: INFO + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" + +springdoc: + api-docs: + enabled: true + path: /v3/api-docs + swagger-ui: + enabled: true + path: /swagger-ui.html + enable-spring-security: true + default-consumes-media-type: application/json + default-produces-media-type: application/json \ No newline at end of file From 78e5c10eafd1c8ea2d0caf96057744dde2128a80 Mon Sep 17 00:00:00 2001 From: subin930 Date: Wed, 7 Jan 2026 14:53:10 +0900 Subject: [PATCH 18/19] =?UTF-8?q?[Fix]=20gateway=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-prod.yml | 37 +----------------- .../src/main/resources/application.yml | 38 +++++++++++++++++-- 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/coachcoach/infra/gateway/src/main/resources/application-prod.yml b/coachcoach/infra/gateway/src/main/resources/application-prod.yml index b999e8e..e68aff4 100644 --- a/coachcoach/infra/gateway/src/main/resources/application-prod.yml +++ b/coachcoach/infra/gateway/src/main/resources/application-prod.yml @@ -1,39 +1,4 @@ -server.port: 8000 -spring: - application: - name: gateway-service - cloud: - gateway: - discovery: - locator: - enabled: true # Eureka 기반 라우팅 - lower-case-service-id: true eureka: - instance: - prefer-ip-address: true # 호스트명 대신 IP로 등록 - hostname: localhost - metadata-map: - prometheus.scrape: true # 수집 대상 client: - register-with-eureka: true - fetch-registry: true service-url: - defaultZone: http://10.0.0.107:8761/eureka/ -management: - endpoints: - web: - exposure: - include: health,info,prometheus,metrics - base-path: /actuator - endpoint: - health: - show-details: always - prometheus: - enabled: true - metrics: - tags: - application: ${spring.application.name} - prometheus: - metrics: - export: - enabled: true \ No newline at end of file + defaultZone: ${EUREKA_URL} \ No newline at end of file diff --git a/coachcoach/infra/gateway/src/main/resources/application.yml b/coachcoach/infra/gateway/src/main/resources/application.yml index 2220151..33acea9 100644 --- a/coachcoach/infra/gateway/src/main/resources/application.yml +++ b/coachcoach/infra/gateway/src/main/resources/application.yml @@ -11,9 +11,41 @@ spring: eureka: instance: prefer-ip-address: true # 호스트명 대신 IP로 등록 - hostname: localhost + metadata-map: + prometheus.scrape: true # 수집 대상 client: register-with-eureka: true fetch-registry: true - service-url: - defaultZone: http://localhost:8761/eureka/ \ No newline at end of file + +management: + endpoints: + web: + exposure: + include: health,info,prometheus,metrics + base-path: /actuator + endpoint: + health: + show-details: always + prometheus: + enabled: true + metrics: + tags: + application: ${spring.application.name} + prometheus: + metrics: + export: + enabled: true + +springdoc: + swagger-ui: + use-root-path: true + urls: + - name: auth-service + url: /api/auth/v3/api-docs + - name: user-store-service + url: /api/user-store/v3/api-docs + - name: catalog-service + url: /api/catalog/v3/api-docs + - name: insight-service + url: /api/insight/v3/api-docs + path: /swagger-ui.html \ No newline at end of file From 3bd748a3cc1fcce1f948f8f6125ac0070b31e897 Mon Sep 17 00:00:00 2001 From: subin930 Date: Wed, 7 Jan 2026 15:42:00 +0900 Subject: [PATCH 19/19] =?UTF-8?q?[Fix]=20env=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coachcoach/.env.example | 13 +++++++++++++ coachcoach/.gitignore | 3 +-- coachcoach/docker/private1-compose.yml | 6 ++++-- coachcoach/docker/private2-compose.yml | 6 ++++-- coachcoach/docker/public1-compose.yml | 3 ++- .../src/main/resources/application-prod.yml | 19 ------------------- coachcoach/infra/gateway/build.gradle | 1 + coachcoach/service/catalog/build.gradle | 1 + 8 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 coachcoach/.env.example delete mode 100644 coachcoach/infra/eureka/src/main/resources/application-prod.yml diff --git a/coachcoach/.env.example b/coachcoach/.env.example new file mode 100644 index 0000000..ea914c9 --- /dev/null +++ b/coachcoach/.env.example @@ -0,0 +1,13 @@ +PRIVATE_EC2_1_HOST= +PRIVATE_EC2_1_SSH_KEY= +PRIVATE_EC2_2_HOST= +PRIVATE_EC2_2_SSH_KEY= +PUBLIC_EC2_1_HOST= +PUBLIC_EC2_1_SSH_KEY= +QUAY_TOKEN= +EUREKA_URL= +MANAGEMENT_ENDPOINTS=health,info,prometheus,metrics +SPRING_PROFILES_ACTIVE=prod +DATASOURCE_URL= +DATASOURCE_USERNAME= +DATASOURCE_PASSWORD= \ No newline at end of file diff --git a/coachcoach/.gitignore b/coachcoach/.gitignore index 8b1032e..476f2ba 100644 --- a/coachcoach/.gitignore +++ b/coachcoach/.gitignore @@ -199,8 +199,7 @@ gradle-app.setting ### yml/yaml ### application-dev.yml -.env -.env.* +common.env !application.yml !application.properties diff --git a/coachcoach/docker/private1-compose.yml b/coachcoach/docker/private1-compose.yml index 6f9fdba..d0546fe 100644 --- a/coachcoach/docker/private1-compose.yml +++ b/coachcoach/docker/private1-compose.yml @@ -7,9 +7,10 @@ services: network_mode: "host" ports: - "9000:9000" + env_file: + - common.env environment: - SPRING_PROFILES_ACTIVE=prod - - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://10.0.0.107:8761/eureka/ restart: unless-stopped user-store: @@ -18,9 +19,10 @@ services: network_mode: "host" ports: - "9001:9001" + env_file: + - common.env environment: - SPRING_PROFILES_ACTIVE=prod - - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://10.0.0.107:8761/eureka/ restart: unless-stopped networks: diff --git a/coachcoach/docker/private2-compose.yml b/coachcoach/docker/private2-compose.yml index dd54e27..8dd277a 100644 --- a/coachcoach/docker/private2-compose.yml +++ b/coachcoach/docker/private2-compose.yml @@ -7,9 +7,10 @@ services: network_mode: "host" ports: - "9002:9002" + env_file: + - common.env environment: - SPRING_PROFILES_ACTIVE=prod - - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://10.0.0.107:8761/eureka/ restart: unless-stopped insight: @@ -18,9 +19,10 @@ services: network_mode: "host" ports: - "9003:9003" + env_file: + - common.env environment: - SPRING_PROFILES_ACTIVE=prod - - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://10.0.0.107:8761/eureka/ restart: unless-stopped networks: diff --git a/coachcoach/docker/public1-compose.yml b/coachcoach/docker/public1-compose.yml index 778b41b..1ac1a66 100644 --- a/coachcoach/docker/public1-compose.yml +++ b/coachcoach/docker/public1-compose.yml @@ -23,9 +23,10 @@ services: container_name: gateway ports: - "8080:8000" + env_file: + - common.env environment: - SPRING_PROFILES_ACTIVE=prod - - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka:8761/eureka/ - JAVA_OPTS=-Xmx128m -Xms64m networks: - coachcoach-network diff --git a/coachcoach/infra/eureka/src/main/resources/application-prod.yml b/coachcoach/infra/eureka/src/main/resources/application-prod.yml deleted file mode 100644 index adc2ab9..0000000 --- a/coachcoach/infra/eureka/src/main/resources/application-prod.yml +++ /dev/null @@ -1,19 +0,0 @@ -server.port: 8761 -spring: - application: - name: eureka-service - -eureka: - client: - register-with-eureka: false - fetch-registry: false - service-url: - defaultZone: http://localhost:8761/eureka/ - server: - wait-time-in-ms-when-sync-empty: 300000 #5분(deafult와 동일) - -management: - endpoints: - web: - exposure: - include: health, info #공개할 엔드포인트 \ No newline at end of file diff --git a/coachcoach/infra/gateway/build.gradle b/coachcoach/infra/gateway/build.gradle index 1872093..04f764b 100644 --- a/coachcoach/infra/gateway/build.gradle +++ b/coachcoach/infra/gateway/build.gradle @@ -17,6 +17,7 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.4.0' implementation project(':common') + developmentOnly 'org.springframework.boot:spring-boot-devtools' } dependencyManagement { diff --git a/coachcoach/service/catalog/build.gradle b/coachcoach/service/catalog/build.gradle index 4115a7e..386f496 100644 --- a/coachcoach/service/catalog/build.gradle +++ b/coachcoach/service/catalog/build.gradle @@ -24,6 +24,7 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0' implementation project(':common') + developmentOnly 'org.springframework.boot:spring-boot-devtools' } dependencyManagement {