From 2d3afcae35b0d80b881ab8c2e4b0bf90918e9e39 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 15 Apr 2025 10:13:44 +0900 Subject: [PATCH 001/361] Initial commit --- README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..f611469 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Back-STARS +SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. From 8e1bf21374a7722dac7a7a80ddcee2eb1b5062cb Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 16 Apr 2025 13:24:01 +0900 Subject: [PATCH 002/361] =?UTF-8?q?Init:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 + .idea/Back-STARS.iml | 9 + .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + auth-service/.gitattributes | 3 + auth-service/.gitignore | 37 +++ auth-service/build.gradle | 58 ++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43705 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + auth-service/gradlew | 251 ++++++++++++++++++ auth-service/gradlew.bat | 94 +++++++ auth-service/settings.gradle | 1 + .../authservice/AuthServiceApplication.java | 13 + .../src/main/resources/application.properties | 1 + .../AuthServiceApplicationTests.java | 13 + congestion-service/.gitattributes | 3 + congestion-service/.gitignore | 37 +++ congestion-service/build.gradle | 58 ++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43705 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + congestion-service/gradlew | 251 ++++++++++++++++++ congestion-service/gradlew.bat | 94 +++++++ congestion-service/settings.gradle | 1 + .../CongestionServiceApplication.java | 13 + .../src/main/resources/application.properties | 1 + .../CongestionServiceApplicationTests.java | 13 + external-info-service/.gitattributes | 3 + external-info-service/.gitignore | 37 +++ external-info-service/build.gradle | 57 ++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43705 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + external-info-service/gradlew | 251 ++++++++++++++++++ external-info-service/gradlew.bat | 94 +++++++ external-info-service/settings.gradle | 1 + .../ExternalInfoServiceApplication.java | 13 + .../src/main/resources/application.properties | 1 + .../ExternalInfoServiceApplicationTests.java | 13 + place-service/.gitattributes | 3 + place-service/.gitignore | 37 +++ place-service/build.gradle | 58 ++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43705 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + place-service/gradlew | 251 ++++++++++++++++++ place-service/gradlew.bat | 94 +++++++ place-service/settings.gradle | 1 + .../placeservice/PlaceServiceApplication.java | 13 + .../src/main/resources/application.properties | 1 + .../PlaceServiceApplicationTests.java | 13 + user-service/.gitattributes | 3 + user-service/.gitignore | 37 +++ user-service/build.gradle | 58 ++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43705 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + user-service/gradlew | 251 ++++++++++++++++++ user-service/gradlew.bat | 94 +++++++ user-service/settings.gradle | 1 + .../userservice/UserServiceApplication.java | 13 + .../src/main/resources/application.properties | 1 + .../UserServiceApplicationTests.java | 13 + 60 files changed, 2426 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/Back-STARS.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 auth-service/.gitattributes create mode 100644 auth-service/.gitignore create mode 100644 auth-service/build.gradle create mode 100644 auth-service/gradle/wrapper/gradle-wrapper.jar create mode 100644 auth-service/gradle/wrapper/gradle-wrapper.properties create mode 100755 auth-service/gradlew create mode 100644 auth-service/gradlew.bat create mode 100644 auth-service/settings.gradle create mode 100644 auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java create mode 100644 auth-service/src/main/resources/application.properties create mode 100644 auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java create mode 100644 congestion-service/.gitattributes create mode 100644 congestion-service/.gitignore create mode 100644 congestion-service/build.gradle create mode 100644 congestion-service/gradle/wrapper/gradle-wrapper.jar create mode 100644 congestion-service/gradle/wrapper/gradle-wrapper.properties create mode 100755 congestion-service/gradlew create mode 100644 congestion-service/gradlew.bat create mode 100644 congestion-service/settings.gradle create mode 100644 congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java create mode 100644 congestion-service/src/main/resources/application.properties create mode 100644 congestion-service/src/test/java/com/example/congestionservice/CongestionServiceApplicationTests.java create mode 100644 external-info-service/.gitattributes create mode 100644 external-info-service/.gitignore create mode 100644 external-info-service/build.gradle create mode 100644 external-info-service/gradle/wrapper/gradle-wrapper.jar create mode 100644 external-info-service/gradle/wrapper/gradle-wrapper.properties create mode 100755 external-info-service/gradlew create mode 100644 external-info-service/gradlew.bat create mode 100644 external-info-service/settings.gradle create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java create mode 100644 external-info-service/src/main/resources/application.properties create mode 100644 external-info-service/src/test/java/com/example/externalinfoservice/ExternalInfoServiceApplicationTests.java create mode 100644 place-service/.gitattributes create mode 100644 place-service/.gitignore create mode 100644 place-service/build.gradle create mode 100644 place-service/gradle/wrapper/gradle-wrapper.jar create mode 100644 place-service/gradle/wrapper/gradle-wrapper.properties create mode 100755 place-service/gradlew create mode 100644 place-service/gradlew.bat create mode 100644 place-service/settings.gradle create mode 100644 place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java create mode 100644 place-service/src/main/resources/application.properties create mode 100644 place-service/src/test/java/com/example/placeservice/PlaceServiceApplicationTests.java create mode 100644 user-service/.gitattributes create mode 100644 user-service/.gitignore create mode 100644 user-service/build.gradle create mode 100644 user-service/gradle/wrapper/gradle-wrapper.jar create mode 100644 user-service/gradle/wrapper/gradle-wrapper.properties create mode 100755 user-service/gradlew create mode 100644 user-service/gradlew.bat create mode 100644 user-service/settings.gradle create mode 100644 user-service/src/main/java/com/example/userservice/UserServiceApplication.java create mode 100644 user-service/src/main/resources/application.properties create mode 100644 user-service/src/test/java/com/example/userservice/UserServiceApplicationTests.java diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..c3f502a --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 디폴트 무시된 파일 +/shelf/ +/workspace.xml +# 에디터 기반 HTTP 클라이언트 요청 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/Back-STARS.iml b/.idea/Back-STARS.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/Back-STARS.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..07115cd --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e325687 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/auth-service/.gitattributes b/auth-service/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/auth-service/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/auth-service/.gitignore b/auth-service/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/auth-service/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/auth-service/build.gradle b/auth-service/build.gradle new file mode 100644 index 0000000..f0e2623 --- /dev/null +++ b/auth-service/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.4' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + // JWT 처리 + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.apache.kafka:kafka-streams' + implementation 'org.springframework.kafka:spring-kafka' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'org.postgresql:postgresql' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.kafka:spring-kafka-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('bootBuildImage') { + builder = 'paketobuildpacks/builder-jammy-base:latest' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/auth-service/gradle/wrapper/gradle-wrapper.jar b/auth-service/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9bbc975c742b298b441bfb90dbc124400a3751b9 GIT binary patch literal 43705 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 DHx0+F literal 0 HcmV?d00001 diff --git a/auth-service/gradle/wrapper/gradle-wrapper.properties b/auth-service/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/auth-service/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/auth-service/gradlew b/auth-service/gradlew new file mode 100755 index 0000000..faf9300 --- /dev/null +++ b/auth-service/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/auth-service/gradlew.bat b/auth-service/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/auth-service/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/auth-service/settings.gradle b/auth-service/settings.gradle new file mode 100644 index 0000000..d4c3eb3 --- /dev/null +++ b/auth-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'auth-service' diff --git a/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java b/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java new file mode 100644 index 0000000..59186b4 --- /dev/null +++ b/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java @@ -0,0 +1,13 @@ +package com.example.authservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AuthServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(AuthServiceApplication.class, args); + } + +} diff --git a/auth-service/src/main/resources/application.properties b/auth-service/src/main/resources/application.properties new file mode 100644 index 0000000..e536707 --- /dev/null +++ b/auth-service/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=auth-service diff --git a/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java b/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java new file mode 100644 index 0000000..2e763c4 --- /dev/null +++ b/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.authservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AuthServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/congestion-service/.gitattributes b/congestion-service/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/congestion-service/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/congestion-service/.gitignore b/congestion-service/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/congestion-service/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/congestion-service/build.gradle b/congestion-service/build.gradle new file mode 100644 index 0000000..6329d25 --- /dev/null +++ b/congestion-service/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.4' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + // JWT 처리 + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.apache.kafka:kafka-streams' + implementation 'org.springframework.kafka:spring-kafka' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'org.postgresql:postgresql' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.kafka:spring-kafka-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('bootBuildImage') { + builder = 'paketobuildpacks/builder-jammy-base:latest' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/congestion-service/gradle/wrapper/gradle-wrapper.jar b/congestion-service/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9bbc975c742b298b441bfb90dbc124400a3751b9 GIT binary patch literal 43705 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 DHx0+F literal 0 HcmV?d00001 diff --git a/congestion-service/gradle/wrapper/gradle-wrapper.properties b/congestion-service/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/congestion-service/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/congestion-service/gradlew b/congestion-service/gradlew new file mode 100755 index 0000000..faf9300 --- /dev/null +++ b/congestion-service/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/congestion-service/gradlew.bat b/congestion-service/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/congestion-service/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/congestion-service/settings.gradle b/congestion-service/settings.gradle new file mode 100644 index 0000000..5d18bb7 --- /dev/null +++ b/congestion-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'congestion-service' diff --git a/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java b/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java new file mode 100644 index 0000000..459ab5a --- /dev/null +++ b/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java @@ -0,0 +1,13 @@ +package com.example.congestionservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CongestionServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(CongestionServiceApplication.class, args); + } + +} diff --git a/congestion-service/src/main/resources/application.properties b/congestion-service/src/main/resources/application.properties new file mode 100644 index 0000000..a5977bd --- /dev/null +++ b/congestion-service/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=congestion-service diff --git a/congestion-service/src/test/java/com/example/congestionservice/CongestionServiceApplicationTests.java b/congestion-service/src/test/java/com/example/congestionservice/CongestionServiceApplicationTests.java new file mode 100644 index 0000000..47b2dc2 --- /dev/null +++ b/congestion-service/src/test/java/com/example/congestionservice/CongestionServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.congestionservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CongestionServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/external-info-service/.gitattributes b/external-info-service/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/external-info-service/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/external-info-service/.gitignore b/external-info-service/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/external-info-service/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/external-info-service/build.gradle b/external-info-service/build.gradle new file mode 100644 index 0000000..3fa973d --- /dev/null +++ b/external-info-service/build.gradle @@ -0,0 +1,57 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.4' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + // JWT 처리 + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.apache.kafka:kafka-streams' + implementation 'org.springframework.kafka:spring-kafka' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'org.postgresql:postgresql' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.kafka:spring-kafka-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('bootBuildImage') { + builder = 'paketobuildpacks/builder-jammy-base:latest' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/external-info-service/gradle/wrapper/gradle-wrapper.jar b/external-info-service/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9bbc975c742b298b441bfb90dbc124400a3751b9 GIT binary patch literal 43705 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 DHx0+F literal 0 HcmV?d00001 diff --git a/external-info-service/gradle/wrapper/gradle-wrapper.properties b/external-info-service/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/external-info-service/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/external-info-service/gradlew b/external-info-service/gradlew new file mode 100755 index 0000000..faf9300 --- /dev/null +++ b/external-info-service/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/external-info-service/gradlew.bat b/external-info-service/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/external-info-service/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/external-info-service/settings.gradle b/external-info-service/settings.gradle new file mode 100644 index 0000000..149d5d4 --- /dev/null +++ b/external-info-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'external-info-service' diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java new file mode 100644 index 0000000..8ad5cab --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java @@ -0,0 +1,13 @@ +package com.example.externalinfoservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ExternalInfoServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(ExternalInfoServiceApplication.class, args); + } + +} diff --git a/external-info-service/src/main/resources/application.properties b/external-info-service/src/main/resources/application.properties new file mode 100644 index 0000000..82efef9 --- /dev/null +++ b/external-info-service/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=external-info-service diff --git a/external-info-service/src/test/java/com/example/externalinfoservice/ExternalInfoServiceApplicationTests.java b/external-info-service/src/test/java/com/example/externalinfoservice/ExternalInfoServiceApplicationTests.java new file mode 100644 index 0000000..0e30f4c --- /dev/null +++ b/external-info-service/src/test/java/com/example/externalinfoservice/ExternalInfoServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.externalinfoservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ExternalInfoServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/place-service/.gitattributes b/place-service/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/place-service/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/place-service/.gitignore b/place-service/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/place-service/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/place-service/build.gradle b/place-service/build.gradle new file mode 100644 index 0000000..6329d25 --- /dev/null +++ b/place-service/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.4' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + // JWT 처리 + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.apache.kafka:kafka-streams' + implementation 'org.springframework.kafka:spring-kafka' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'org.postgresql:postgresql' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.kafka:spring-kafka-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('bootBuildImage') { + builder = 'paketobuildpacks/builder-jammy-base:latest' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/place-service/gradle/wrapper/gradle-wrapper.jar b/place-service/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9bbc975c742b298b441bfb90dbc124400a3751b9 GIT binary patch literal 43705 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 DHx0+F literal 0 HcmV?d00001 diff --git a/place-service/gradle/wrapper/gradle-wrapper.properties b/place-service/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/place-service/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/place-service/gradlew b/place-service/gradlew new file mode 100755 index 0000000..faf9300 --- /dev/null +++ b/place-service/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/place-service/gradlew.bat b/place-service/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/place-service/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/place-service/settings.gradle b/place-service/settings.gradle new file mode 100644 index 0000000..dad92db --- /dev/null +++ b/place-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'place-service' diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java new file mode 100644 index 0000000..7aaef28 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java @@ -0,0 +1,13 @@ +package com.example.placeservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class PlaceServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(PlaceServiceApplication.class, args); + } + +} diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties new file mode 100644 index 0000000..9071dcb --- /dev/null +++ b/place-service/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=place-service diff --git a/place-service/src/test/java/com/example/placeservice/PlaceServiceApplicationTests.java b/place-service/src/test/java/com/example/placeservice/PlaceServiceApplicationTests.java new file mode 100644 index 0000000..07f3276 --- /dev/null +++ b/place-service/src/test/java/com/example/placeservice/PlaceServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.placeservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class PlaceServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/user-service/.gitattributes b/user-service/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/user-service/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/user-service/.gitignore b/user-service/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/user-service/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/user-service/build.gradle b/user-service/build.gradle new file mode 100644 index 0000000..6329d25 --- /dev/null +++ b/user-service/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.4' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + // JWT 처리 + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.apache.kafka:kafka-streams' + implementation 'org.springframework.kafka:spring-kafka' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'org.postgresql:postgresql' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.kafka:spring-kafka-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('bootBuildImage') { + builder = 'paketobuildpacks/builder-jammy-base:latest' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/user-service/gradle/wrapper/gradle-wrapper.jar b/user-service/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9bbc975c742b298b441bfb90dbc124400a3751b9 GIT binary patch literal 43705 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 DHx0+F literal 0 HcmV?d00001 diff --git a/user-service/gradle/wrapper/gradle-wrapper.properties b/user-service/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/user-service/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/user-service/gradlew b/user-service/gradlew new file mode 100755 index 0000000..faf9300 --- /dev/null +++ b/user-service/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/user-service/gradlew.bat b/user-service/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/user-service/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/user-service/settings.gradle b/user-service/settings.gradle new file mode 100644 index 0000000..eb2f57d --- /dev/null +++ b/user-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'user-service' diff --git a/user-service/src/main/java/com/example/userservice/UserServiceApplication.java b/user-service/src/main/java/com/example/userservice/UserServiceApplication.java new file mode 100644 index 0000000..55a5c33 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/UserServiceApplication.java @@ -0,0 +1,13 @@ +package com.example.userservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UserServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(UserServiceApplication.class, args); + } + +} diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties new file mode 100644 index 0000000..79b5bda --- /dev/null +++ b/user-service/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=user-service diff --git a/user-service/src/test/java/com/example/userservice/UserServiceApplicationTests.java b/user-service/src/test/java/com/example/userservice/UserServiceApplicationTests.java new file mode 100644 index 0000000..0460494 --- /dev/null +++ b/user-service/src/test/java/com/example/userservice/UserServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.userservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UserServiceApplicationTests { + + @Test + void contextLoads() { + } + +} From f2d30da7e0b9e08caffda5e5bfc6b645794fc5ef Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 16 Apr 2025 13:24:01 +0900 Subject: [PATCH 003/361] =?UTF-8?q?Init:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 + .idea/Back-STARS.iml | 9 + .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + auth-service/.gitattributes | 3 + auth-service/.gitignore | 37 +++ auth-service/build.gradle | 58 ++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43705 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + auth-service/gradlew | 251 ++++++++++++++++++ auth-service/gradlew.bat | 94 +++++++ auth-service/settings.gradle | 1 + .../authservice/AuthServiceApplication.java | 13 + .../src/main/resources/application.properties | 1 + .../AuthServiceApplicationTests.java | 13 + congestion-service/.gitattributes | 3 + congestion-service/.gitignore | 37 +++ congestion-service/build.gradle | 58 ++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43705 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + congestion-service/gradlew | 251 ++++++++++++++++++ congestion-service/gradlew.bat | 94 +++++++ congestion-service/settings.gradle | 1 + .../CongestionServiceApplication.java | 13 + .../src/main/resources/application.properties | 1 + .../CongestionServiceApplicationTests.java | 13 + external-info-service/.gitattributes | 3 + external-info-service/.gitignore | 37 +++ external-info-service/build.gradle | 57 ++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43705 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + external-info-service/gradlew | 251 ++++++++++++++++++ external-info-service/gradlew.bat | 94 +++++++ external-info-service/settings.gradle | 1 + .../ExternalInfoServiceApplication.java | 13 + .../ExternalInfoServiceApplicationTests.java | 13 + place-service/.gitattributes | 3 + place-service/.gitignore | 37 +++ place-service/build.gradle | 58 ++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43705 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + place-service/gradlew | 251 ++++++++++++++++++ place-service/gradlew.bat | 94 +++++++ place-service/settings.gradle | 1 + .../placeservice/PlaceServiceApplication.java | 13 + .../src/main/resources/application.properties | 1 + .../PlaceServiceApplicationTests.java | 13 + user-service/.gitattributes | 3 + user-service/.gitignore | 37 +++ user-service/build.gradle | 58 ++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43705 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + user-service/gradlew | 251 ++++++++++++++++++ user-service/gradlew.bat | 94 +++++++ user-service/settings.gradle | 1 + .../userservice/UserServiceApplication.java | 13 + .../src/main/resources/application.properties | 1 + .../UserServiceApplicationTests.java | 13 + 59 files changed, 2425 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/Back-STARS.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 auth-service/.gitattributes create mode 100644 auth-service/.gitignore create mode 100644 auth-service/build.gradle create mode 100644 auth-service/gradle/wrapper/gradle-wrapper.jar create mode 100644 auth-service/gradle/wrapper/gradle-wrapper.properties create mode 100755 auth-service/gradlew create mode 100644 auth-service/gradlew.bat create mode 100644 auth-service/settings.gradle create mode 100644 auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java create mode 100644 auth-service/src/main/resources/application.properties create mode 100644 auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java create mode 100644 congestion-service/.gitattributes create mode 100644 congestion-service/.gitignore create mode 100644 congestion-service/build.gradle create mode 100644 congestion-service/gradle/wrapper/gradle-wrapper.jar create mode 100644 congestion-service/gradle/wrapper/gradle-wrapper.properties create mode 100755 congestion-service/gradlew create mode 100644 congestion-service/gradlew.bat create mode 100644 congestion-service/settings.gradle create mode 100644 congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java create mode 100644 congestion-service/src/main/resources/application.properties create mode 100644 congestion-service/src/test/java/com/example/congestionservice/CongestionServiceApplicationTests.java create mode 100644 external-info-service/.gitattributes create mode 100644 external-info-service/.gitignore create mode 100644 external-info-service/build.gradle create mode 100644 external-info-service/gradle/wrapper/gradle-wrapper.jar create mode 100644 external-info-service/gradle/wrapper/gradle-wrapper.properties create mode 100755 external-info-service/gradlew create mode 100644 external-info-service/gradlew.bat create mode 100644 external-info-service/settings.gradle create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java create mode 100644 external-info-service/src/test/java/com/example/externalinfoservice/ExternalInfoServiceApplicationTests.java create mode 100644 place-service/.gitattributes create mode 100644 place-service/.gitignore create mode 100644 place-service/build.gradle create mode 100644 place-service/gradle/wrapper/gradle-wrapper.jar create mode 100644 place-service/gradle/wrapper/gradle-wrapper.properties create mode 100755 place-service/gradlew create mode 100644 place-service/gradlew.bat create mode 100644 place-service/settings.gradle create mode 100644 place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java create mode 100644 place-service/src/main/resources/application.properties create mode 100644 place-service/src/test/java/com/example/placeservice/PlaceServiceApplicationTests.java create mode 100644 user-service/.gitattributes create mode 100644 user-service/.gitignore create mode 100644 user-service/build.gradle create mode 100644 user-service/gradle/wrapper/gradle-wrapper.jar create mode 100644 user-service/gradle/wrapper/gradle-wrapper.properties create mode 100755 user-service/gradlew create mode 100644 user-service/gradlew.bat create mode 100644 user-service/settings.gradle create mode 100644 user-service/src/main/java/com/example/userservice/UserServiceApplication.java create mode 100644 user-service/src/main/resources/application.properties create mode 100644 user-service/src/test/java/com/example/userservice/UserServiceApplicationTests.java diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..c3f502a --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 디폴트 무시된 파일 +/shelf/ +/workspace.xml +# 에디터 기반 HTTP 클라이언트 요청 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/Back-STARS.iml b/.idea/Back-STARS.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/Back-STARS.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..07115cd --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e325687 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/auth-service/.gitattributes b/auth-service/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/auth-service/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/auth-service/.gitignore b/auth-service/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/auth-service/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/auth-service/build.gradle b/auth-service/build.gradle new file mode 100644 index 0000000..f0e2623 --- /dev/null +++ b/auth-service/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.4' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + // JWT 처리 + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.apache.kafka:kafka-streams' + implementation 'org.springframework.kafka:spring-kafka' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'org.postgresql:postgresql' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.kafka:spring-kafka-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('bootBuildImage') { + builder = 'paketobuildpacks/builder-jammy-base:latest' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/auth-service/gradle/wrapper/gradle-wrapper.jar b/auth-service/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9bbc975c742b298b441bfb90dbc124400a3751b9 GIT binary patch literal 43705 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 DHx0+F literal 0 HcmV?d00001 diff --git a/auth-service/gradle/wrapper/gradle-wrapper.properties b/auth-service/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/auth-service/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/auth-service/gradlew b/auth-service/gradlew new file mode 100755 index 0000000..faf9300 --- /dev/null +++ b/auth-service/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/auth-service/gradlew.bat b/auth-service/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/auth-service/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/auth-service/settings.gradle b/auth-service/settings.gradle new file mode 100644 index 0000000..d4c3eb3 --- /dev/null +++ b/auth-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'auth-service' diff --git a/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java b/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java new file mode 100644 index 0000000..59186b4 --- /dev/null +++ b/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java @@ -0,0 +1,13 @@ +package com.example.authservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AuthServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(AuthServiceApplication.class, args); + } + +} diff --git a/auth-service/src/main/resources/application.properties b/auth-service/src/main/resources/application.properties new file mode 100644 index 0000000..e536707 --- /dev/null +++ b/auth-service/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=auth-service diff --git a/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java b/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java new file mode 100644 index 0000000..2e763c4 --- /dev/null +++ b/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.authservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AuthServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/congestion-service/.gitattributes b/congestion-service/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/congestion-service/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/congestion-service/.gitignore b/congestion-service/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/congestion-service/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/congestion-service/build.gradle b/congestion-service/build.gradle new file mode 100644 index 0000000..6329d25 --- /dev/null +++ b/congestion-service/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.4' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + // JWT 처리 + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.apache.kafka:kafka-streams' + implementation 'org.springframework.kafka:spring-kafka' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'org.postgresql:postgresql' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.kafka:spring-kafka-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('bootBuildImage') { + builder = 'paketobuildpacks/builder-jammy-base:latest' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/congestion-service/gradle/wrapper/gradle-wrapper.jar b/congestion-service/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9bbc975c742b298b441bfb90dbc124400a3751b9 GIT binary patch literal 43705 zcma&Obx`DOvL%eWOXJW;V64viP??$)@wHcsJ68)>bJS6*&iHnskXE8MjvIPVl|FrmV}Npeql07fCw6`pw`0s zGauF(<*@v{3t!qoUU*=j)6;|-(yg@jvDx&fV^trtZt27?4Tkn729qrItVh@PMwG5$ z+oXHSPM??iHZ!cVP~gYact-CwV`}~Q+R}PPNRy+T-geK+>fHrijpllon_F4N{@b-} z1M0=a!VbVmJM8Xk@NRv)m&aRYN}FSJ{LS;}2ArQ5baSjfy40l@T5)1r-^0fAU6f_} zzScst%$Nd-^ElV~H0TetQhMc%S{}Q4lssln=|;LG?Ulo}*mhg8YvBAUY7YFdXs~vv zv~{duzVw%C#GxkBwX=TYp1Dh*Uaum2?RmsvPaLlzO^fIJ`L?&OV?Y&kKj~^kWC`Ly zfL-}J^4a0Ojuz9O{jUbIS;^JatJ5+YNNHe}6nG9Yd6P-lJiK2ms)A^xq^H2fKrTF) zp!6=`Ece~57>^9(RA4OB9;f1FAhV%zVss%#rDq$9ZW3N2cXC7dMz;|UcRFecBm`DA z1pCO!#6zKp#@mx{2>Qcme8y$Qg_gnA%(`Vtg3ccwgb~D(&@y8#Jg8nNYW*-P{_M#E zZ|wCsQoO1(iIKd-2B9xzI}?l#Q@G5d$m1Lfh0q;iS5FDQ&9_2X-H)VDKA*fa{b(sV zL--krNCXibi1+*C2;4qVjb0KWUVGjjRT{A}Q*!cFmj0tRip2ra>WYJ>ZK4C|V~RYs z6;~+*)5F^x^aQqk9tjh)L;DOLlD8j+0<>kHc8MN|68PxQV`tJFbgxSfq-}b(_h`luA0&;Vk<@51i0 z_cu6{_*=vlvYbKjDawLw+t^H?OV00_73Cn3goU5?})UYFuoSX6Xqw;TKcrsc|r# z$sMWYl@cs#SVopO$hpHZ)cdU-+Ui%z&Sa#lMI~zWW@vE%QDh@bTe0&V9nL>4Et9`N zGT8(X{l@A~loDx}BDz`m6@tLv@$mTlVJ;4MGuj!;9Y=%;;_kj#o8n5tX%@M)2I@}u z_{I!^7N1BxW9`g&Z+K#lZ@7_dXdsqp{W9_`)zgZ=sD~%WS5s$`7z#XR!Lfy(4se(m zR@a3twgMs19!-c4jh`PfpJOSU;vShBKD|I0@rmv_x|+ogqslnLLOepJpPMOxhRb*i zGHkwf#?ylQ@k9QJL?!}MY4i7joSzMcEhrDKJH&?2v{-tgCqJe+Y0njl7HYff z{&~M;JUXVR$qM1FPucIEY(IBAuCHC@^~QG6O!dAjzQBxDOR~lJEr4KS9R*idQ^p{D zS#%NQADGbAH~6wAt}(1=Uff-1O#ITe)31zCL$e9~{w)gx)g>?zFE{Bc9nJT6xR!i8 z)l)~9&~zSZTHk{?iQL^MQo$wLi}`B*qnvUy+Y*jEraZMnEhuj`Fu+>b5xD1_Tp z)8|wedv42#3AZUL7x&G@p@&zcUvPkvg=YJS6?1B7ZEXr4b>M+9Gli$gK-Sgh{O@>q7TUg+H zNJj`6q#O@>4HpPJEHvNij`sYW&u%#=215HKNg;C!0#hH1vlO5+dFq9& zS)8{5_%hz?#D#wn&nm@aB?1_|@kpA@{%jYcs{K%$a4W{k@F zPyTav?jb;F(|GaZhm6&M#g|`ckO+|mCtAU)5_(hn&Ogd z9Ku}orOMu@K^Ac>eRh3+0-y^F`j^noa*OkS3p^tLV`TY$F$cPXZJ48!xz1d7%vfA( zUx2+sDPqHfiD-_wJDb38K^LtpN2B0w=$A10z%F9f_P2aDX63w7zDG5CekVQJGy18I zB!tI`6rZr7TK10L(8bpiaQ>S@b7r_u@lh^vakd0e6USWw7W%d_Ob%M!a`K>#I3r-w zo2^+9Y)Sb?P9)x0iA#^ns+Kp{JFF|$09jb6ZS2}_<-=$?^#IUo5;g`4ICZknr!_aJ zd73%QP^e-$%Xjt|28xM}ftD|V@76V_qvNu#?Mt*A-OV{E4_zC4Ymo|(cb+w^`Wv== z>)c%_U0w`d$^`lZQp@midD89ta_qTJW~5lRrIVwjRG_9aRiQGug%f3p@;*%Y@J5uQ|#dJ+P{Omc`d2VR)DXM*=ukjVqIpkb<9gn9{*+&#p)Ek zN=4zwNWHF~=GqcLkd!q0p(S2_K=Q`$whZ}r@ec_cb9hhg9a z6CE=1n8Q;hC?;ujo0numJBSYY6)GTq^=kB~`-qE*h%*V6-ip=c4+Yqs*7C@@b4YAi zuLjsmD!5M7r7d5ZPe>4$;iv|zq=9=;B$lI|xuAJwi~j~^Wuv!Qj2iEPWjh9Z&#+G>lZQpZ@(xfBrhc{rlLwOC;optJZDj4Xfu3$u6rt_=YY0~lxoy~fq=*L_&RmD7dZWBUmY&12S;(Ui^y zBpHR0?Gk|`U&CooNm_(kkO~pK+cC%uVh^cnNn)MZjF@l{_bvn4`Jc}8QwC5_)k$zs zM2qW1Zda%bIgY^3NcfL)9ug`05r5c%8ck)J6{fluBQhVE>h+IA&Kb}~$55m-^c1S3 zJMXGlOk+01qTQUFlh5Jc3xq|7McY$nCs$5=`8Y;|il#Ypb{O9}GJZD8!kYh{TKqs@ z-mQn1K4q$yGeyMcryHQgD6Ra<6^5V(>6_qg`3uxbl|T&cJVA*M_+OC#>w(xL`RoPQ zf1ZCI3G%;o-x>RzO!mc}K!XX{1rih0$~9XeczHgHdPfL}4IPi~5EV#ZcT9 zdgkB3+NPbybS-d;{8%bZW^U+x@Ak+uw;a5JrZH!WbNvl!b~r4*vs#he^bqz`W93PkZna2oYO9dBrKh2QCWt{dGOw)%Su%1bIjtp4dKjZ^ zWfhb$M0MQiDa4)9rkip9DaH0_tv=XxNm>6MKeWv>`KNk@QVkp$Lhq_~>M6S$oliq2 zU6i7bK;TY)m>-}X7hDTie>cc$J|`*}t=MAMfWIALRh2=O{L57{#fA_9LMnrV(HrN6 zG0K_P5^#$eKt{J|#l~U0WN_3)p^LLY(XEqes0OvI?3)GTNY&S13X+9`6PLVFRf8K) z9x@c|2T72+-KOm|kZ@j4EDDec>03FdgQlJ!&FbUQQH+nU^=U3Jyrgu97&#-W4C*;_ z(WacjhBDp@&Yon<9(BWPb;Q?Kc0gR5ZH~aRNkPAWbDY!FiYVSu!~Ss^9067|JCrZk z-{Rn2KEBR|Wti_iy) zXnh2wiU5Yz2L!W{{_#LwNWXeNPHkF=jjXmHC@n*oiz zIoM~Wvo^T@@t!QQW?Ujql-GBOlnB|HjN@x~K8z)c(X}%%5Zcux09vC8=@tvgY>czq z3D(U&FiETaN9aP}FDP3ZSIXIffq>M3{~eTB{uauL07oYiM=~K(XA{SN!rJLyXeC+Y zOdeebgHOc2aCIgC=8>-Q>zfuXV*=a&gp{l#E@K|{qft@YtO>xaF>O7sZz%8);e86? z+jJlFB{0fu6%8ew^_<+v>>%6eB8|t*_v7gb{x=vLLQYJKo;p7^o9!9A1)fZZ8i#ZU z<|E?bZakjkEV8xGi?n+{Xh3EgFKdM^;4D;5fHmc04PI>6oU>>WuLy6jgpPhf8$K4M zjJo*MbN0rZbZ!5DmoC^@hbqXiP^1l7I5;Wtp2i9Jkh+KtDJoXP0O8qmN;Sp(+%upX zAxXs*qlr(ck+-QG_mMx?hQNXVV~LT{$Q$ShX+&x?Q7v z@8t|UDylH6@RZ?WsMVd3B0z5zf50BP6U<&X_}+y3uJ0c5OD}+J&2T8}A%2Hu#Nt_4 zoOoTI$A!hQ<2pk5wfZDv+7Z{yo+Etqry=$!*pvYyS+kA4xnJ~3b~TBmA8Qd){w_bE zqDaLIjnU8m$wG#&T!}{e0qmHHipA{$j`%KN{&#_Kmjd&#X-hQN+ju$5Ms$iHj4r?) z&5m8tI}L$ih&95AjQ9EDfPKSmMj-@j?Q+h~C3<|Lg2zVtfKz=ft{YaQ1i6Om&EMll zzov%MsjSg=u^%EfnO+W}@)O6u0LwoX709h3Cxdc2Rwgjd%LLTChQvHZ+y<1q6kbJXj3_pq1&MBE{8 zd;aFotyW>4WHB{JSD8Z9M@jBitC1RF;!B8;Rf-B4nOiVbGlh9w51(8WjL&e{_iXN( zAvuMDIm_>L?rJPxc>S`bqC|W$njA0MKWa?V$u6mN@PLKYqak!bR!b%c^ze(M`ec(x zv500337YCT4gO3+9>oVIJLv$pkf`01S(DUM+4u!HQob|IFHJHm#>eb#eB1X5;bMc| z>QA4Zv}$S?fWg~31?Lr(C>MKhZg>gplRm`2WZ--iw%&&YlneQYY|PXl;_4*>vkp;I z$VYTZq|B*(3(y17#@ud@o)XUZPYN*rStQg5U1Sm2gM}7hf_G<>*T%6ebK*tF(kbJc zNPH4*xMnJNgw!ff{YXrhL&V$6`ylY={qT_xg9znQWw9>PlG~IbhnpsG_94Kk_(V-o&v7#F znra%uD-}KOX2dkak**hJnZZQyp#ERyyV^lNe!Qrg=VHiyr7*%j#PMvZMuYNE8o;JM zGrnDWmGGy)(UX{rLzJ*QEBd(VwMBXnJ@>*F8eOFy|FK*Vi0tYDw;#E zu#6eS;%Nm2KY+7dHGT3m{TM7sl=z8|V0e!DzEkY-RG8vTWDdSQFE|?+&FYA146@|y zV(JP>LWL;TSL6rao@W5fWqM1-xr$gRci#RQV2DX-x4@`w{uEUgoH4G|`J%H!N?*Qn zy~rjzuf(E7E!A9R2bSF|{{U(zO+;e29K_dGmC^p7MCP!=Bzq@}&AdF5=rtCwka zTT1A?5o}i*sXCsRXBt)`?nOL$zxuP3i*rm3Gmbmr6}9HCLvL*45d|(zP;q&(v%}S5yBmRVdYQQ24zh z6qL2<2>StU$_Ft29IyF!6=!@;tW=o8vNzVy*hh}XhZhUbxa&;9~woye<_YmkUZ)S?PW{7t; zmr%({tBlRLx=ffLd60`e{PQR3NUniWN2W^~7Sy~MPJ>A#!6PLnlw7O0(`=PgA}JLZ ztqhiNcKvobCcBel2 z-N82?4-()eGOisnWcQ9Wp23|ybG?*g!2j#>m3~0__IX1o%dG4b;VF@^B+mRgKx|ij zWr5G4jiRy}5n*(qu!W`y54Y*t8g`$YrjSunUmOsqykYB4-D(*(A~?QpuFWh;)A;5= zPl|=x+-w&H9B7EZGjUMqXT}MkcSfF}bHeRFLttu!vHD{Aq)3HVhvtZY^&-lxYb2%` zDXk7>V#WzPfJs6u{?ZhXpsMdm3kZscOc<^P&e&684Rc1-d=+=VOB)NR;{?0NjTl~D z1MXak$#X4{VNJyD$b;U~Q@;zlGoPc@ny!u7Pe;N2l4;i8Q=8>R3H{>HU(z z%hV2?rSinAg6&wuv1DmXok`5@a3@H0BrqsF~L$pRYHNEXXuRIWom0l zR9hrZpn1LoYc+G@q@VsFyMDNX;>_Vf%4>6$Y@j;KSK#g)TZRmjJxB!_NmUMTY(cAV zmewn7H{z`M3^Z& z2O$pWlDuZHAQJ{xjA}B;fuojAj8WxhO}_9>qd0|p0nBXS6IIRMX|8Qa!YDD{9NYYK z%JZrk2!Ss(Ra@NRW<7U#%8SZdWMFDU@;q<}%F{|6n#Y|?FaBgV$7!@|=NSVoxlJI4G-G(rn}bh|?mKkaBF$-Yr zA;t0r?^5Nz;u6gwxURapQ0$(-su(S+24Ffmx-aP(@8d>GhMtC5x*iEXIKthE*mk$` zOj!Uri|EAb4>03C1xaC#(q_I<;t}U7;1JqISVHz3tO{) zD(Yu@=>I9FDmDtUiWt81;BeaU{_=es^#QI7>uYl@e$$lGeZ~Q(f$?^3>$<<{n`Bn$ zn8bamZlL@6r^RZHV_c5WV7m2(G6X|OI!+04eAnNA5=0v1Z3lxml2#p~Zo57ri;4>;#16sSXXEK#QlH>=b$inEH0`G#<_ zvp;{+iY)BgX$R!`HmB{S&1TrS=V;*5SB$7*&%4rf_2wQS2ed2E%Wtz@y$4ecq4w<) z-?1vz_&u>s?BMrCQG6t9;t&gvYz;@K@$k!Zi=`tgpw*v-#U1Pxy%S9%52`uf$XMv~ zU}7FR5L4F<#9i%$P=t29nX9VBVv)-y7S$ZW;gmMVBvT$BT8d}B#XV^@;wXErJ-W2A zA=JftQRL>vNO(!n4mcd3O27bHYZD!a0kI)6b4hzzL9)l-OqWn)a~{VP;=Uo|D~?AY z#8grAAASNOkFMbRDdlqVUfB;GIS-B-_YXNlT_8~a|LvRMVXf!<^uy;)d$^OR(u)!) zHHH=FqJF-*BXif9uP~`SXlt0pYx|W&7jQnCbjy|8b-i>NWb@!6bx;1L&$v&+!%9BZ z0nN-l`&}xvv|wwxmC-ZmoFT_B#BzgQZxtm|4N+|;+(YW&Jtj^g!)iqPG++Z%x0LmqnF875%Ry&2QcCamx!T@FgE@H zN39P6e#I5y6Yl&K4eUP{^biV`u9{&CiCG#U6xgGRQr)zew;Z%x+ z-gC>y%gvx|dM=OrO`N@P+h2klPtbYvjS!mNnk4yE0+I&YrSRi?F^plh}hIp_+OKd#o7ID;b;%*c0ES z!J))9D&YufGIvNVwT|qsGWiZAwFODugFQ$VsNS%gMi8OJ#i${a4!E3<-4Jj<9SdSY z&xe|D0V1c`dZv+$8>(}RE|zL{E3 z-$5Anhp#7}oO(xm#}tF+W=KE*3(xxKxhBt-uuJP}`_K#0A< zE%rhMg?=b$ot^i@BhE3&)bNBpt1V*O`g?8hhcsV-n#=|9wGCOYt8`^#T&H7{U`yt2 z{l9Xl5CVsE=`)w4A^%PbIR6uG_5Ww9k`=q<@t9Bu662;o{8PTjDBzzbY#tL;$wrpjONqZ{^Ds4oanFm~uyPm#y1Ll3(H57YDWk9TlC zq;kebC!e=`FU&q2ojmz~GeLxaJHfs0#F%c(i+~gg$#$XOHIi@1mA72g2pFEdZSvp}m0zgQb5u2?tSRp#oo!bp`FP}< zaK4iuMpH+Jg{bb7n9N6eR*NZfgL7QiLxI zk6{uKr>xxJ42sR%bJ%m8QgrL|fzo9@?9eQiMW8O`j3teoO_R8cXPe_XiLnlYkE3U4 zN!^F)Z4ZWcA8gekEPLtFqX-Q~)te`LZnJK_pgdKs)Dp50 zdUq)JjlJeELskKg^6KY!sIou-HUnSFRsqG^lsHuRs`Z{f(Ti9eyd3cwu*Kxp?Ws7l z3cN>hGPXTnQK@qBgqz(n*qdJ2wbafELi?b90fK~+#XIkFGU4+HihnWq;{{)1J zv*Txl@GlnIMOjzjA1z%g?GsB2(6Zb-8fooT*8b0KF2CdsIw}~Hir$d3TdVHRx1m3c z4C3#h@1Xi@{t4zge-#B6jo*ChO%s-R%+9%-E|y<*4;L>$766RiygaLR?X%izyqMXA zb|N=Z-0PSFeH;W6aQ3(5VZWVC>5Ibgi&cj*c%_3=o#VyUJv* zM&bjyFOzlaFq;ZW(q?|yyi|_zS%oIuH^T*MZ6NNXBj;&yM3eQ7!CqXY?`7+*+GN47 zNR#%*ZH<^x{(0@hS8l{seisY~IE*)BD+R6^OJX}<2HRzo^fC$n>#yTOAZbk4%=Bei=JEe=o$jm`or0YDw*G?d> z=i$eEL7^}_?UI^9$;1Tn9b>$KOM@NAnvWrcru)r`?LodV%lz55O3y(%FqN;cKgj7t zlJ7BmLTQ*NDX#uelGbCY>k+&H*iSK?x-{w;f5G%%!^e4QT9z<_0vHbXW^MLR} zeC*jezrU|{*_F`I0mi)9=sUj^G03i@MjXx@ePv@(Udt2CCXVOJhRh4yp~fpn>ssHZ z?k(C>2uOMWKW5FVsBo#Nk!oqYbL`?#i~#!{3w^qmCto05uS|hKkT+iPrC-}hU_nbL zO622#mJupB21nChpime}&M1+whF2XM?prT-Vv)|EjWYK(yGYwJLRRMCkx;nMSpu?0 zNwa*{0n+Yg6=SR3-S&;vq=-lRqN`s9~#)OOaIcy3GZ&~l4g@2h| zThAN#=dh{3UN7Xil;nb8@%)wx5t!l z0RSe_yJQ+_y#qEYy$B)m2yDlul^|m9V2Ia$1CKi6Q19~GTbzqk*{y4;ew=_B4V8zw zScDH&QedBl&M*-S+bH}@IZUSkUfleyM45G>CnYY{hx8J9q}ME?Iv%XK`#DJRNmAYt zk2uY?A*uyBA=nlYjkcNPMGi*552=*Q>%l?gDK_XYh*Rya_c)ve{=ps`QYE0n!n!)_$TrGi_}J|>1v}(VE7I~aP-wns#?>Y zu+O7`5kq32zM4mAQpJ50vJsUDT_^s&^k-llQMy9!@wRnxw@~kXV6{;z_wLu3i=F3m z&eVsJmuauY)8(<=pNUM5!!fQ4uA6hBkJoElL1asWNkYE#qaP?a+biwWw~vB48PRS7 zY;DSHvgbIB$)!uJU)xA!yLE*kP0owzYo`v@wfdux#~f!dv#uNc_$SF@Qq9#3q5R zfuQnPPN_(z;#X#nRHTV>TWL_Q%}5N-a=PhkQ^GL+$=QYfoDr2JO-zo#j;mCsZVUQ) zJ96e^OqdLW6b-T@CW@eQg)EgIS9*k`xr$1yDa1NWqQ|gF^2pn#dP}3NjfRYx$pTrb zwGrf8=bQAjXx*8?du*?rlH2x~^pXjiEmj^XwQo{`NMonBN=Q@Y21!H)D( zA~%|VhiTjaRQ%|#Q9d*K4j~JDXOa4wmHb0L)hn*;Eq#*GI}@#ux4}bt+olS(M4$>c z=v8x74V_5~xH$sP+LZCTrMxi)VC%(Dg!2)KvW|Wwj@pwmH6%8zd*x0rUUe$e(Z%AW z@Q{4LL9#(A-9QaY2*+q8Yq2P`pbk3!V3mJkh3uH~uN)+p?67d(r|Vo0CebgR#u}i? zBxa^w%U|7QytN%L9bKaeYhwdg7(z=AoMeP0)M3XZA)NnyqL%D_x-(jXp&tp*`%Qsx z6}=lGr;^m1<{;e=QQZ!FNxvLcvJVGPkJ63at5%*`W?46!6|5FHYV0qhizSMT>Zoe8 zsJ48kb2@=*txGRe;?~KhZgr-ZZ&c0rNV7eK+h$I-UvQ=552@psVrvj#Ys@EU4p8`3 zsNqJu-o=#@9N!Pq`}<=|((u)>^r0k^*%r<{YTMm+mOPL>EoSREuQc-e2~C#ZQ&Xve zZ}OUzmE4{N-7cqhJiUoO_V#(nHX11fdfVZJT>|6CJGX5RQ+Ng$Nq9xs-C86-)~`>p zW--X53J`O~vS{WWjsAuGq{K#8f#2iz` zzSSNIf6;?5sXrHig%X(}0q^Y=eYwvh{TWK-fT>($8Ex>!vo_oGFw#ncr{vmERi^m7lRi%8Imph})ZopLoIWt*eFWSPuBK zu>;Pu2B#+e_W|IZ0_Q9E9(s@0>C*1ft`V{*UWz^K<0Ispxi@4umgGXW!j%7n+NC~* zBDhZ~k6sS44(G}*zg||X#9Weto;u*Ty;fP!+v*7be%cYG|yEOBomch#m8Np!Sw`L)q+T` zmrTMf2^}7j=RPwgpO9@eXfb{Q>GW#{X=+xt`AwTl!=TgYm)aS2x5*`FSUaaP_I{Xi zA#irF%G33Bw>t?^1YqX%czv|JF0+@Pzi%!KJ?z!u$A`Catug*tYPO`_Zho5iip0@! z;`rR0-|Ao!YUO3yaujlSQ+j-@*{m9dHLtve!sY1Xq_T2L3&=8N;n!!Eb8P0Z^p4PL zQDdZ?An2uzbIakOpC|d@=xEA}v-srucnX3Ym{~I#Ghl~JZU(a~Ppo9Gy1oZH&Wh%y zI=KH_s!Lm%lAY&`_KGm*Ht)j*C{-t}Nn71drvS!o|I|g>ZKjE3&Mq0TCs6}W;p>%M zQ(e!h*U~b;rsZ1OPigud>ej=&hRzs@b>>sq6@Yjhnw?M26YLnDH_Wt#*7S$-BtL08 zVyIKBm$}^vp?ILpIJetMkW1VtIc&7P3z0M|{y5gA!Yi5x4}UNz5C0Wdh02!h zNS>923}vrkzl07CX`hi)nj-B?#n?BJ2Vk0zOGsF<~{Fo7OMCN_85daxhk*pO}x_8;-h>}pcw26V6CqR-=x2vRL?GB#y%tYqi;J}kvxaz}*iFO6YO0ha6!fHU9#UI2Nv z_(`F#QU1B+P;E!t#Lb)^KaQYYSewj4L!_w$RH%@IL-M($?DV@lGj%3ZgVdHe^q>n(x zyd5PDpGbvR-&p*eU9$#e5#g3-W_Z@loCSz}f~{94>k6VRG`e5lI=SE0AJ7Z_+=nnE zTuHEW)W|a8{fJS>2TaX zuRoa=LCP~kP)kx4L+OqTjtJOtXiF=y;*eUFgCn^Y@`gtyp?n14PvWF=zhNGGsM{R- z^DsGxtoDtx+g^hZi@E2Y(msb-hm{dWiHdoQvdX88EdM>^DS#f}&kCGpPFDu*KjEpv$FZtLpeT>@)mf|z#ZWEsueeW~hF78Hu zfY9a+Gp?<)s{Poh_qdcSATV2oZJo$OH~K@QzE2kCADZ@xX(; z)0i=kcAi%nvlsYagvUp(z0>3`39iKG9WBDu3z)h38p|hLGdD+Khk394PF3qkX!02H z#rNE`T~P9vwNQ_pNe0toMCRCBHuJUmNUl)KFn6Gu2je+p>{<9^oZ4Gfb!)rLZ3CR3 z-o&b;Bh>51JOt=)$-9+Z!P}c@cKev_4F1ZZGs$I(A{*PoK!6j@ZJrAt zv2LxN#p1z2_0Ox|Q8PVblp9N${kXkpsNVa^tNWhof)8x8&VxywcJz#7&P&d8vvxn` zt75mu>yV=Dl#SuiV!^1BPh5R)`}k@Nr2+s8VGp?%Le>+fa{3&(XYi~{k{ z-u4#CgYIdhp~GxLC+_wT%I*)tm4=w;ErgmAt<5i6c~)7JD2olIaK8by{u-!tZWT#RQddptXRfEZxmfpt|@bs<*uh?Y_< zD>W09Iy4iM@@80&!e^~gj!N`3lZwosC!!ydvJtc0nH==K)v#ta_I}4Tar|;TLb|+) zSF(;=?$Z0?ZFdG6>Qz)6oPM}y1&zx_Mf`A&chb znSERvt9%wdPDBIU(07X+CY74u`J{@SSgesGy~)!Mqr#yV6$=w-dO;C`JDmv=YciTH zvcrN1kVvq|(3O)NNdth>X?ftc`W2X|FGnWV%s})+uV*bw>aoJ#0|$pIqK6K0Lw!@- z3pkPbzd`ljS=H2Bt0NYe)u+%kU%DWwWa>^vKo=lzDZHr>ruL5Ky&#q7davj-_$C6J z>V8D-XJ}0cL$8}Xud{T_{19#W5y}D9HT~$&YY-@=Th219U+#nT{tu=d|B)3K`pL53 zf7`I*|L@^dPEIDJkI3_oA9vsH7n7O}JaR{G~8 zfi$?kmKvu20(l`dV7=0S43VwVKvtF!7njv1Q{Ju#ysj=|dASq&iTE8ZTbd-iiu|2& zmll%Ee1|M?n9pf~?_tdQ<7%JA53!ulo1b^h#s|Su2S4r{TH7BRB3iIOiX5|vc^;5( zKfE1+ah18YA9o1EPT(AhBtve5(%GMbspXV)|1wf5VdvzeYt8GVGt0e*3|ELBhwRaO zE|yMhl;Bm?8Ju3-;DNnxM3Roelg`^!S%e({t)jvYtJCKPqN`LmMg^V&S z$9OIFLF$%Py~{l?#ReyMzpWixvm(n(Y^Am*#>atEZ8#YD&?>NUU=zLxOdSh0m6mL? z_twklB0SjM!3+7U^>-vV=KyQZI-6<(EZiwmNBzGy;Sjc#hQk%D;bay$v#zczt%mFCHL*817X4R;E$~N5(N$1Tv{VZh7d4mhu?HgkE>O+^-C*R@ zR0ima8PsEV*WFvz`NaB+lhX3&LUZcWWJJrG7ZjQrOWD%_jxv=)`cbCk zMgelcftZ%1-p9u!I-Zf_LLz{hcn5NRbxkWby@sj2XmYfAV?iw^0?hM<$&ZDctdC`; zsL|C-7d;w$z2Gt0@hsltNlytoPnK&$>ksr(=>!7}Vk#;)Hp)LuA7(2(Hh(y3LcxRY zim!`~j6`~B+sRBv4 z<#B{@38kH;sLB4eH2+8IPWklhd25r5j2VR}YK$lpZ%7eVF5CBr#~=kUp`i zlb+>Z%i%BJH}5dmfg1>h7U5Q(-F{1d=aHDbMv9TugohX5lq#szPAvPE|HaokMQIi_ zTcTNsO53(oX=hg2w!XA&+qP}nwr$(C)pgG8emS@Mf7m0&*kiA!wPLS`88c=aD$niJ zp?3j%NI^uy|5*MzF`k4hFbsyQZ@wu!*IY+U&&9PwumdmyfL(S0#!2RFfmtzD3m9V7 zsNOw9RQofl-XBfKBF^~~{oUVouka#r3EqRf=SnleD=r1Hm@~`y8U7R)w16fgHvK-6?-TFth)f3WlklbZh+}0 zx*}7oDF4U^1tX4^$qd%987I}g;+o0*$Gsd=J>~Uae~XY6UtbdF)J8TzJXoSrqHVC) zJ@pMgE#;zmuz?N2MIC+{&)tx=7A%$yq-{GAzyz zLzZLf=%2Jqy8wGHD;>^x57VG)sDZxU+EMfe0L{@1DtxrFOp)=zKY1i%HUf~Dro#8} zUw_Mj10K7iDsX}+fThqhb@&GI7PwONx!5z;`yLmB_92z0sBd#HiqTzDvAsTdx+%W{ z2YL#U=9r!@3pNXMp_nvximh+@HV3psUaVa-lOBekVuMf1RUd26~P*|MLouQrb}XM-bEw(UgQxMI6M&l3Nha z{MBcV=tl(b_4}oFdAo}WX$~$Mj-z70FowdoB{TN|h2BdYs?$imcj{IQpEf9q z)rzpttc0?iwopSmEoB&V!1aoZqEWEeO-MKMx(4iK7&Fhc(94c zdy}SOnSCOHX+A8q@i>gB@mQ~Anv|yiUsW!bO9hb&5JqTfDit9X6xDEz*mQEiNu$ay zwqkTV%WLat|Ar+xCOfYs0UQNM`sdsnn*zJr>5T=qOU4#Z(d90!IL76DaHIZeWKyE1 zqwN%9+~lPf2d7)vN2*Q?En?DEPcM+GQwvA<#;X3v=fqsxmjYtLJpc3)A8~*g(KqFx zZEnqqruFDnEagXUM>TC7ngwKMjc2Gx%#Ll#=N4qkOuK|;>4%=0Xl7k`E69@QJ-*Vq zk9p5!+Ek#bjuPa<@Xv7ku4uiWo|_wy)6tIr`aO!)h>m5zaMS-@{HGIXJ0UilA7*I} z?|NZ!Tp8@o-lnyde*H+@8IHME8VTQOGh96&XX3E+}OB zA>VLAGW+urF&J{H{9Gj3&u+Gyn?JAVW84_XBeGs1;mm?2SQm9^!3UE@(_FiMwgkJI zZ*caE={wMm`7>9R?z3Ewg!{PdFDrbzCmz=RF<@(yQJ_A6?PCd_MdUf5vv6G#9Mf)i#G z($OxDT~8RNZ>1R-vw|nN699a}MQN4gJE_9gA-0%>a?Q<9;f3ymgoi$OI!=aE6Elw z2I`l!qe-1J$T$X&x9Zz#;3!P$I);jdOgYY1nqny-k=4|Q4F!mkqACSN`blRji>z1` zc8M57`~1lgL+Ha%@V9_G($HFBXH%k;Swyr>EsQvg%6rNi){Tr&+NAMga2;@85531V z_h+h{jdB&-l+%aY{$oy2hQfx`d{&?#psJ78iXrhrO)McOFt-o80(W^LKM{Zw93O}m z;}G!51qE?hi=Gk2VRUL2kYOBRuAzktql%_KYF4>944&lJKfbr+uo@)hklCHkC=i)E zE*%WbWr@9zoNjumq|kT<9Hm*%&ahcQ)|TCjp@uymEU!&mqqgS;d|v)QlBsE0Jw|+^ zFi9xty2hOk?rlGYT3)Q7i4k65@$RJ-d<38o<`}3KsOR}t8sAShiVWevR8z^Si4>dS z)$&ILfZ9?H#H&lumngpj7`|rKQQ`|tmMmFR+y-9PP`;-425w+#PRKKnx7o-Rw8;}*Ctyw zKh~1oJ5+0hNZ79!1fb(t7IqD8*O1I_hM;o*V~vd_LKqu7c_thyLalEF8Y3oAV=ODv z$F_m(Z>ucO(@?+g_vZ`S9+=~Msu6W-V5I-V6h7->50nQ@+TELlpl{SIfYYNvS6T6D z`9cq=at#zEZUmTfTiM3*vUamr!OB~g$#?9$&QiwDMbSaEmciWf3O2E8?oE0ApScg38hb&iN%K+kvRt#d))-tr^ zD+%!d`i!OOE3in0Q_HzNXE!JcZ<0;cu6P_@;_TIyMZ@Wv!J z)HSXAYKE%-oBk`Ye@W3ShYu-bfCAZ}1|J16hFnLy z?Bmg2_kLhlZ*?`5R8(1%Y?{O?xT)IMv{-)VWa9#1pKH|oVRm4!lLmls=u}Lxs44@g^Zwa0Z_h>Rk<(_mHN47=Id4oba zQ-=qXGz^cNX(b*=NT0<^23+hpS&#OXzzVO@$Z2)D`@oS=#(s+eQ@+FSQcpXD@9npp zlxNC&q-PFU6|!;RiM`?o&Sj&)<4xG3#ozRyQxcW4=EE;E)wcZ&zUG*5elg;{9!j}I z9slay#_bb<)N!IKO16`n3^@w=Y%duKA-{8q``*!w9SW|SRbxcNl50{k&CsV@b`5Xg zWGZ1lX)zs_M65Yt&lO%mG0^IFxzE_CL_6$rDFc&#xX5EXEKbV8E2FOAt>Ka@e0aHQ zMBf>J$FLrCGL@$VgPKSbRkkqo>sOXmU!Yx+Dp7E3SRfT`v~!mjU3qj-*!!YjgI*^) z+*05x78FVnVwSGKr^A|FW*0B|HYgc{c;e3Ld}z4rMI7hVBKaiJRL_e$rxDW^8!nGLdJ<7ex9dFoyj|EkODflJ#Xl`j&bTO%=$v)c+gJsLK_%H3}A_} z6%rfG?a7+k7Bl(HW;wQ7BwY=YFMSR3J43?!;#~E&)-RV_L!|S%XEPYl&#`s!LcF>l zn&K8eemu&CJp2hOHJKaYU#hxEutr+O161ze&=j3w12)UKS%+LAwbjqR8sDoZHnD=m0(p62!zg zxt!Sj65S?6WPmm zL&U9c`6G}T`irf=NcOiZ!V)qhnvMNOPjVkyO2^CGJ+dKTnNAPa?!AxZEpO7yL_LkB zWpolpaDfSaO-&Uv=dj7`03^BT3_HJOAjn~X;wz-}03kNs@D^()_{*BD|0mII!J>5p z1h06PTyM#3BWzAz1FPewjtrQfvecWhkRR=^gKeFDe$rmaYAo!np6iuio3>$w?az$E zwGH|zy@OgvuXok}C)o1_&N6B3P7ZX&-yimXc1hAbXr!K&vclCL%hjVF$yHpK6i_Wa z*CMg1RAH1(EuuA01@lA$sMfe*s@9- z$jNWqM;a%d3?(>Hzp*MiOUM*?8eJ$=(0fYFis!YA;0m8s^Q=M0Hx4ai3eLn%CBm14 zOb8lfI!^UAu_RkuHmKA-8gx8Z;##oCpZV{{NlNSe<i;9!MfIN!&;JI-{|n{(A19|s z9oiGesENcLf@NN^9R0uIrgg(46r%kjR{0SbnjBqPq()wDJ@LC2{kUu_j$VR=l`#RdaRe zxx;b7bu+@IntWaV$si1_nrQpo*IWGLBhhMS13qH zTy4NpK<-3aVc;M)5v(8JeksSAGQJ%6(PXGnQ-g^GQPh|xCop?zVXlFz>42%rbP@jg z)n)% zM9anq5(R=uo4tq~W7wES$g|Ko z1iNIw@-{x@xKxSXAuTx@SEcw(%E49+JJCpT(y=d+n9PO0Gv1SmHkYbcxPgDHF}4iY zkXU4rkqkwVBz<{mcv~A0K|{zpX}aJcty9s(u-$je2&=1u(e#Q~UA{gA!f;0EAaDzdQ=}x7g(9gWrWYe~ zV98=VkHbI!5Rr;+SM;*#tOgYNlfr7;nLU~MD^jSdSpn@gYOa$TQPv+e8DyJ&>aInB zDk>JmjH=}<4H4N4z&QeFx>1VPY8GU&^1c&71T*@2#dINft%ibtY(bAm%<2YwPL?J0Mt{ z7l7BR718o5=v|jB!<7PDBafdL>?cCdVmKC;)MCOobo5edt%RTWiReAMaIU5X9h`@El0sR&Z z7Ed+FiyA+QAyWn zf7=%(8XpcS*C4^-L24TBUu%0;@s!Nzy{e95qjgkzElf0#ou`sYng<}wG1M|L? zKl6ITA1X9mt6o@S(#R3B{uwJI8O$&<3{+A?T~t>Kapx6#QJDol6%?i-{b1aRu?&9B z*W@$T*o&IQ&5Kc*4LK_)MK-f&Ys^OJ9FfE?0SDbAPd(RB)Oju#S(LK)?EVandS1qb#KR;OP|86J?;TqI%E8`vszd&-kS%&~;1Als=NaLzRNnj4q=+ zu5H#z)BDKHo1EJTC?Cd_oq0qEqNAF8PwU7fK!-WwVEp4~4g z3SEmE3-$ddli))xY9KN$lxEIfyLzup@utHn=Q{OCoz9?>u%L^JjClW$M8OB`txg4r6Q-6UlVx3tR%%Z!VMb6#|BKRL`I))#g zij8#9gk|p&Iwv+4s+=XRDW7VQrI(+9>DikEq!_6vIX8$>poDjSYIPcju%=qluSS&j zI-~+ztl1f71O-B+s7Hf>AZ#}DNSf`7C7*)%(Xzf|ps6Dr7IOGSR417xsU=Rxb z1pgk9vv${17h7mZ{)*R{mc%R=!i}8EFV9pl8V=nXCZruBff`$cqN3tpB&RK^$yH!A8RL zJ5KltH$&5%xC7pLZD}6wjD2-uq3&XL8CM$@V9jqalF{mvZ)c4Vn?xXbvkB(q%xbSdjoXJXanVN@I;8I`)XlBX@6BjuQKD28Jrg05} z^ImmK-Ux*QMn_A|1ionE#AurP8Vi?x)7jG?v#YyVe_9^up@6^t_Zy^T1yKW*t* z&Z0+0Eo(==98ig=^`he&G^K$I!F~1l~gq}%o5#pR6?T+ zLmZu&_ekx%^nys<^tC@)s$kD`^r8)1^tUazRkWEYPw0P)=%cqnyeFo3nW zyV$^0DXPKn5^QiOtOi4MIX^#3wBPJjenU#2OIAgCHPKXv$OY=e;yf7+_vI7KcjKq% z?RVzC24ekYp2lEhIE^J$l&wNX0<}1Poir8PjM`m#zwk-AL0w6WvltT}*JN8WFmtP_ z6#rK7$6S!nS!}PSFTG6AF7giGJw5%A%14ECde3x95(%>&W3zUF!8x5%*h-zk8b@Bz zh`7@ixoCVCZ&$$*YUJpur90Yg0X-P82>c~NMzDy7@Ed|6(#`;{)%t7#Yb>*DBiXC3 zUFq(UDFjrgOsc%0KJ_L;WQKF0q!MINpQzSsqwv?#Wg+-NO; z84#4nk$+3C{2f#}TrRhin=Erdfs77TqBSvmxm0P?01Tn@V(}gI_ltHRzQKPyvQ2=M zX#i1-a(>FPaESNx+wZ6J{^m_q3i})1n~JG80c<%-Ky!ZdTs8cn{qWY%x%X^27-Or_ z`KjiUE$OG9K4lWS16+?aak__C*)XA{ z6HmS*8#t_3dl}4;7ZZgn4|Tyy1lOEM1~6Qgl(|BgfQF{Mfjktch zB5kc~4NeehRYO%)3Z!FFHhUVVcV@uEX$eft5Qn&V3g;}hScW_d)K_h5i)vxjKCxcf zL>XlZ^*pQNuX*RJQn)b6;blT3<7@Ap)55)aK3n-H08GIx65W zO9B%gE%`!fyT`)hKjm-&=on)l&!i-QH+mXQ&lbXg0d|F{Ac#U;6b$pqQcpqWSgAPo zmr$gOoE*0r#7J=cu1$5YZE%uylM!i3L{;GW{ae9uy)+EaV>GqW6QJ)*B2)-W`|kLL z)EeeBtpgm;79U_1;Ni5!c^0RbG8yZ0W98JiG~TC8rjFRjGc6Zi8BtoC);q1@8h7UV zFa&LRzYsq%6d!o5-yrqyjXi>jg&c8bu}{Bz9F2D(B%nnuVAz74zmBGv)PAdFXS2(A z=Z?uupM2f-ar0!A)C6l2o8a|+uT*~huH)!h3i!&$ zr>76mt|lwexD(W_+5R{e@2SwR15lGxsnEy|gbS-s5?U}l*kcfQlfnQKo5=LZXizrL zM=0ty+$#f_qGGri-*t@LfGS?%7&LigUIU#JXvwEdJZvIgPCWFBTPT`@Re5z%%tRDO zkMlJCoqf2A=hkU7Ih=IxmPF~fEL90)u76nfFRQwe{m7b&Ww$pnk~$4Lx#s9|($Cvt ze|p{Xozhb^g1MNh-PqS_dLY|Fex4|rhM#lmzq&mhebD$5P>M$eqLoV|z=VQY{)7&sR#tW zl(S1i!!Rrg7kv+V@EL51PGpm511he%MbX2-Jl+DtyYA(0gZyZQjPZP@`SAH{n&25@ zd)emg(p2T3$A!Nmzo|%=z%AhLX)W4hsZNFhmd4<1l6?b3&Fg)G(Zh%J{Cf8Q;?_++ zgO7O<(-)H|Es@QqUgcXNJEfC-BCB~#dhi6ADVZtL!)Mx|u7>ukD052z!QZ5UC-+rd zYXWNRpCmdM{&?M9OMa;OiN{Y#0+F>lBQ=W@M;OXq;-7v3niC$pM8p!agNmq7F04;| z@s-_98JJB&s`Pr6o$KZ=8}qO*7m6SMp7kVmmh$jfnG{r@O(auI7Z^jj!x}NTLS9>k zdo}&Qc2m4Ws3)5qFw#<$h=g%+QUKiYog33bE)e4*H~6tfd42q+|FT5+vmr6Y$6HGC zV!!q>B`1Ho|6E|D<2tYE;4`8WRfm2#AVBBn%_W)mi(~x@g;uyQV3_)~!#A6kmFy0p zY~#!R1%h5E{5;rehP%-#kjMLt*{g((o@0-9*8lKVu+t~CtnOxuaMgo2ssI6@kX09{ zkn~q8Gx<6T)l}7tWYS#q0&~x|-3ho@l}qIr79qOJQcm&Kfr7H54=BQto0)vd1A_*V z)8b2{xa5O^u95~TS=HcJF5b9gMV%&M6uaj<>E zPNM~qGjJ~xbg%QTy#(hPtfc46^nN=Y_GmPYY_hTL{q`W3NedZyRL^kgU@Q$_KMAjEzz*eip`3u6AhPDcWXzR=Io5EtZRPme>#K9 z4lN&87i%YYjoCKN_z9YK+{fJu{yrriba#oGM|2l$ir017UH86Eoig3x+;bz32R*;n zt)Eyg#PhQbbGr^naCv0?H<=@+Poz)Xw*3Gn00qdSL|zGiyYKOA0CP%qk=rBAlt~hr zEvd3Z4nfW%g|c`_sfK$z8fWsXTQm@@eI-FpLGrW<^PIjYw)XC-xFk+M<6>MfG;WJr zuN}7b;p^`uc0j(73^=XJcw;|D4B(`)Flm|qEbB?>qBBv2V?`mWA?Q3yRdLkK7b}y& z+!3!JBI{+&`~;%Pj#n&&y+<;IQzw5SvqlbC+V=kLZLAHOQb zS{{8E&JXy1p|B&$K!T*GKtSV^{|Uk;`oE*F;?@q1dX|>|KWb@|Dy*lbGV0Gx;gpA$ z*N16`v*gQ?6Skw(f^|SL;;^ox6jf2AQ$Zl?gvEV&H|-ep*hIS@0TmGu1X1ZmEPY&f zKCrV{UgRAiNU*=+Uw%gjIQhTAC@67m)6(_D+N>)(^gK74F%M2NUpWpho}aq|Kxh$3 zz#DWOmQV4Lg&}`XTU41Z|P~5;wN2c?2L{a=)Xi~!m#*=22c~&AW zgG#yc!_p##fI&E{xQD9l#^x|9`wSyCMxXe<3^kDIkS0N>=oAz7b`@M>aT?e$IGZR; zS;I{gnr4cS^u$#>D(sjkh^T6_$s=*o%vNLC5+6J=HA$&0v6(Y1lm|RDn&v|^CTV{= zjVrg_S}WZ|k=zzp>DX08AtfT@LhW&}!rv^);ds7|mKc5^zge_Li>FTNFoA8dbk@K$ zuuzmDQRL1leikp%m}2_`A7*7=1p2!HBlj0KjPC|WT?5{_aa%}rQ+9MqcfXI0NtjvXz1U)|H>0{6^JpHspI4MfXjV%1Tc1O!tdvd{!IpO+@ z!nh()i-J3`AXow^MP!oVLVhVW&!CDaQxlD9b|Zsc%IzsZ@d~OfMvTFXoEQg9Nj|_L zI+^=(GK9!FGck+y8!KF!nzw8ZCX>?kQr=p@7EL_^;2Mlu1e7@ixfZQ#pqpyCJ```(m;la2NpJNoLQR};i4E;hd+|QBL@GdQy(Cc zTSgZ)4O~hXj86x<7&ho5ePzDrVD`XL7{7PjjNM1|6d5>*1hFPY!E(XDMA+AS;_%E~ z(dOs)vy29&I`5_yEw0x{8Adg%wvmoW&Q;x?5`HJFB@KtmS+o0ZFkE@f)v>YYh-z&m z#>ze?@JK4oE7kFRFD%MPC@x$^p{aW}*CH9Y_(oJ~St#(2)4e-b34D>VG6giMGFA83 zpZTHM2I*c8HE}5G;?Y7RXMA2k{Y?RxHb2 zZFQv?!*Kr_q;jt3`{?B5Wf}_a7`roT&m1BN9{;5Vqo6JPh*gnN(gj}#=A$-F(SRJj zUih_ce0f%K19VLXi5(VBGOFbc(YF zLvvOJl+W<}>_6_4O?LhD>MRGlrk;~J{S#Q;Q9F^;Cu@>EgZAH=-5fp02(VND(v#7n zK-`CfxEdonk!!65?3Ry(s$=|CvNV}u$5YpUf?9kZl8h@M!AMR7RG<9#=`_@qF@})d ztJDH>=F!5I+h!4#^DN6C$pd6^)_;0Bz7|#^edb9_qFg&eI}x{Roovml5^Yf5;=ehZ zGqz-x{I`J$ejkmGTFipKrUbv-+1S_Yga=)I2ZsO16_ye@!%&Op^6;#*Bm;=I^#F;? z27Sz-pXm4x-ykSW*3`)y4$89wy6dNOP$(@VYuPfb97XPDTY2FE{Z+{6=}LLA23mAc zskjZJ05>b)I7^SfVc)LnKW(&*(kP*jBnj>jtph`ZD@&30362cnQpZW8juUWcDnghc zy|tN1T6m?R7E8iyrL%)53`ymXX~_;#r${G`4Q(&7=m7b#jN%wdLlS0lb~r9RMdSuU zJ{~>>zGA5N`^QmrzaqDJ(=9y*?@HZyE!yLFONJO!8q5Up#2v>fR6CkquE$PEcvw5q zC8FZX!15JgSn{Gqft&>A9r0e#be^C<%)psE*nyW^e>tsc8s4Q}OIm})rOhuc{3o)g1r>Q^w5mas) zDlZQyjQefhl0PmH%cK05*&v{-M1QCiK=rAP%c#pdCq_StgDW}mmw$S&K6ASE=`u4+ z5wcmtrP27nAlQCc4qazffZoFV7*l2=Va}SVJD6CgRY^=5Ul=VYLGqR7H^LHA;H^1g}ekn=4K8SPRCT+pel*@jUXnLz+AIePjz@mUsslCN2 z({jl?BWf&DS+FlE5Xwp%5zXC7{!C=k9oQLP5B;sLQxd`pg+B@qPRqZ6FU(k~QkQu{ zF~5P=kLhs+D}8qqa|CQo2=cv$wkqAzBRmz_HL9(HRBj&73T@+B{(zZahlkkJ>EQmQ zenp59dy+L;sSWYde!z_W+I~-+2Xnm;c;wI_wH=RTgxpMlCW@;Us*0}L74J#E z8XbDWJGpBscw?W$&ZxZNxUq(*DKDwNzW7_}AIw$HF6Ix|;AJ3t6lN=v(c9=?n9;Y0 zK9A0uW4Ib9|Mp-itnzS#5in=Ny+XhGO8#(1_H4%Z6yEBciBiHfn*h;^r9gWb^$UB4 zJtN8^++GfT`1!WfQt#3sXGi-p<~gIVdMM<#ZZ0e_kdPG%Q5s20NNt3Jj^t$(?5cJ$ zGZ#FT(Lt>-0fP4b5V3az4_byF12k%}Spc$WsRydi&H|9H5u1RbfPC#lq=z#a9W(r1 z!*}KST!Yhsem0tO#r!z`znSL-=NnP~f(pw-sE+Z$e7i7t9nBP^5ts1~WFmW+j+<@7 zIh@^zKO{1%Lpx^$w8-S+T_59v;%N;EZtJzcfN%&@(Ux5 z@YzX^MwbbXESD*d(&qT7-eOHD6iaH-^N>p2sVdq&(`C$;?#mgBANIc5$r| z^A$r)@c{Z}N%sbfo?T`tTHz9-YpiMW?6>kr&W9t$Cuk{q^g1<$I~L zo++o2!!$;|U93cI#p4hyc!_Mv2QKXxv419}Ej#w#%N+YIBDdnn8;35!f2QZkUG?8O zpP47Wf9rnoI^^!9!dy~XsZ&!DU4bVTAi3Fc<9$_krGR&3TI=Az9uMgYU5dd~ksx+} zP+bs9y+NgEL>c@l>H1R%@>5SWg2k&@QZL(qNUI4XwDl6(=!Q^U%o984{|0e|mR$p+ z9BcwttR#7?As?@Q{+j?K6H7R71PuiA^Dl$=f47nUKL|koCwutc_P<-m{|Al3C~o7w z=4S=}s5LcJFT1zjS)+10X_r$74`K78pz!nGGH%JV%w75!YSIt#hT7}}K>+@{{a+Im z5p#6%^X*txY?}|T17xWW*sa^?G2QHt#@tlcw0GIcy;|NR2vaCBDvn=`h)1il7E5Rx z%)mA4$`$OZx)NF5vXZnaJ1)*cA6ryx6Ll~t!LzhxvcTedxT;>JS&e=?-&DXUPaQ2~ zH*69ezE`hgV{K-|0z|m~ld}=X^-Ob={wpex&}*+Rz{gx)G}gn!C_VN{UN=>^EV=Xc zr$-HO09cW&p4^M}V3yBjTP_xrVcc8iU_^Y-JD~(bgw*@GXGB1gYKz5DWO+O`>})|N zWrC)MR93yA)3{&27-M)TJB6Ml3~?zZg#mYsF=#OSTaw&K z@hBftpt+2l@)YK@|3DvTjl(8wZtpLp9Ik!6G$CSL_idZ$Ti?R)4toe8bb)l|)lNb}?K;O2K9vyn1QG zd=v#y-Ld49UVkmfRU>Egc+(Y$^-;6vW;3Lcu*6~etz}0|@+b|+!UCal)DEYGLbHWJ zll5Wi^$Y<6@S%^y%hdjRh6&{!z1Py|lZ|q&Wub3l41uN2zEF8E&5H5?PL*&V}?*a}Lp% zCYi{ghjpRNT^^B+_U59No50Ghih5qn(W5`RkrsDWr{~A1dgtv{sRkH4RU2^A{jb&0 zxVRnrm|u<;$iI;M6A>$POP)TWGU-gSjAERk*EGmVT(aw$!XUSe~7Ql-oRA54^4V(JWS6Q1mG?!vZ zx+pE!FEtvqr|Xrcb3oR`%LHFLmU_&{=p%mGy6MRe2Yz_5WJ8p@IgU2 zdVvvhhQtiQkChK%*&PsiPCBL9oDOoJX8!$S(V>R}+1M}wzK*U*A{KJ`r=lM;mPrKU zQDqqN(W*u-5-?$(SIk<6A0E}34y&@-IVC%S!a1F4kz<3bIKjlyD)ooO_7ftl%S_(6w`!vX&1PZ!K`@D@L6JR)6zO@Dl!YF{RY}d3HZ7?Q5E>w=$ ze)H_)48Ds*Ov4?zoGb2fe3}{!5Ooc|KCIni1o)(Gj+CO?`*7jsV`hIv@8J(22o4Q? zu?Bvi)zDG(me?7XKeL|iF9ZRgZdT*}Ffsl62Cu;{Gv9j6dO zPt*H2GqC)-C`V`ceuu=tM{7!2yTEj=*5+T~5DYiZ)Hy)*PARYI6R2lZXoOj;v8M4W z*O-NX(7_~Q&A3>Oaw&1lBH_H%SwmISX-i3)HfHvBOeVwTT{LUM3}ZuZmg<(>)KE;d zbs2!0v6>J;1nQ0UJkUxnkE@Ibi~Q}M=-=Rk;hcOnxO$luOKEVxZc|!XECgex(2`}T z3Y;Q_6rL)e+SrOZhQj5_e}Lv>w7n*Pep$yWZNQl>ubBgb_NIWWDn3kNpn+MPQXV;8 zV|_Ba5jsQ(w&Ey^IM|@|y!AqcJ#3m0#Q6_qvgCG~eoF#mnGmbO(;DP+bW%_aOs1R_ z@9p#7X2UA^--#Nwx_Hvk2l1`eO{P*#j@q2UELtH|Uh6hxR`h_847wIJo0=5CQQ`6it|%a-I$^&a@we1rc&*;QIu5Ck^?) zx*5eSd*mG#=6Hi(5!;5uUi&{HfnT1S8X-)?gE5CZ6KWoqM5|CyrULmuFBKOU8SOp* z{IB1$OCcq`S-k*xs;4fmhKsIGZ;GYAY*%(@875NxhMq|j*m4CNLI(Vho|N|F);!E0cS5y^$H^Izje?z}oTgyr`9x9G&rlJZw&uqIoBMtz zzhU0(9;w02?m#0!)cFi*r+8YvooQ;(s2lLVvyLqAE%Xqe!vtWbIs!l1Bpp(FIht-Z zPn#CN-2C|J*GhA2fuHqYQ2mJiXlGTzD}mkr2;ia8Wp}h^;OS7+N^Mw|en!1${vN6 z-x{8N*4UekA~`IV2&K-GzhAqau|}d*pEQ$1MH$cFi03OG^1NetZ_jW^STaEzr&Xho zB452St%v3ez2#TFm~`gZh$vi=in+y2d!z<{OZ~Kty-5bQ;0O=k_ESi8Nx9{*T`LJy6jqR>&|+>OZ;+=0hA04 zE25t^sE9HG)3^KKR_A5WDkqispweP9!I-@dCO&N!JrD@i{WBHnfQ z95o8;d$`AFnca3;N-0iX-CmbbAp5yQ!GoH;h7Cn?m{ammZJI8igP{U73lFnl2&gCs zqJ4(Vo~^j`{zOAzScL5B_Sm?Mjtek1d(A6X5ObcZi$;aOYy|g$}BY z$GEP3#i60Ju_&3SHzryH!gUFwC9-295u??cf+aYRQ1$+!rc#42YNattd6mZEFI@?C zqFM>6+zxEunIHDZ>{Z15u##>N(28Dw!>G(k*dB{NHvip@aP}f`@=Q;!o;zRMWo{Cx zo?kyzh8n7#f1g0&g>Cd>O-2g?uPwy8sy8hZbHSsXPmU;@l=HL=zm7mN(=@*|D$i+u zs~TllkCTvD$f&-#b9B?}#Lg*-ibK13R_a$RyoN3m5`10tdhAq{+VW)K#Bht-ra1*J z+n$N%V>u0rVtx`aKJDwXXrxaD7nS<>$=c82v7@KVx^S@vT;h=SZE37K>iahpx3;VDzEr9GY=2(%uaqM;^76eSP0QLzo4sI z>p_Eei*T$K;|qK`sq;?Hesp}(@VvX2Q4sAMYAJ}b&d$htDMC{FG-$o4k9ApECi1$a zXdamjiOGKHBh(4M<3(2x6n-CrmZMCknkQxdSS!qlis#I}btfX;J`JU3RlvtLdrymP zG0ZzrsGXVFiq+Wk1=BFay&9ZiCE#(`h~CL+c-Hs@iGTU@YxM%vlg;)`Tf~IknA^02 zXkN#Txo6aR{j$wP5T#|UH#5AP2{rSY8p?jKFv zG3kn3y`FaV!*Jq%m39_TQEhD>M@l*bhEPGe1{ft3q#K5AknT=F2_=T^l#ou5ln@D# z5Tzs(kRG@qNDa~HLNvfv7Z0g=bSlb?`QAx|Gfoni|iHJ%K0cy z;~Nsaa+{8HP_qrb{nj+xzkdYhSI@W4N_1`z(eSGIkbDP)!Ko|M%}Rqp(~KI2hl~eE zvJ!j4m6iwMgKy>fkCLC)`M$z9EV}B+sq1}}kVf$(ig0pWTY?rHz1Sm=4srTGNb^JG z=2$9wz-C@aZZZ2!HY#HNejqZRmE=pN(D$Kui$NpfhU`!y_s{@MIxiJdHb1|{6xb`> zE74_@QtgtG{4=3P1$^vn&m}7Aw8!1DnT$2thO#~44wl(N#ao8S0@t@m+Z!KD2CfK; z)n5DAPKV_etmH1aLDK$?`;sL91iVt$D z*SG}=-LIAg(*+JON!-5ivqOMQ1S!OQUgHglDsKik&Mwg;vva523`JwQH6SRz9eTY# zTIi23145~kc3r1mSWC_RzD%hs$S#!pkI9!BU80jJCJcwo*FZolQG$q`8C1d9pP@ND zG^&-ZraIvhg_FDVSfKGwkcI=avIan%2sK4coUs~Nr8jC*&!G0#?}_^s3r-c}-uAqi zM-Lw>Y}I``T;IS%Y|qH;s{F*ZefM!4{I5awr!K+T@uPd*Vu*iPWI}>(-D{zxsN>LG z=@747a_Rb2>q?y8xYf?dq2HM5tFO8Y5e4N;Y=xy8yAhI zsm>oy%R5;7)7T3V_b2%`aH^tNlsQpFxIFW#iV#8?{6{^cGr{A0@1bA)|K z>MMTuZD(pd2t|7vmHtywGXb%%=)S<`OG~}U+jm#xd%H8 z$v8-C%F?ah3$;hn?{G3(LT!SgvCVi$vwsZssAQvUwT`Q%qSw!LSd!(I!64w1=%Sc1Mck)q1@pZ@)=SY zoX}d+L3-RA|c?G3_BQNm&( z!i$AZ7cI(z7q|e9VM##6T3Xorj1JG(9os$;(I$y%mBy(#8{|3l4|x*oBAQL^XhZ0g zy1FR1teRrpKq{uLAibTLx#n({qwjlkOvR{OdSAeT5ah4-sNN)n4Clg1T9lzF)&yj; zyal1%+s4n1IG;^VPWJ;#olpk8Z42Gj-tjFeQ&PlxB)`oCNoUYKj4U$AeG8rYiD{pK zndDf&2;2;)D|KvOZP+e7fcPU9k4M2sfhr@vC~Ly0?S-4dz)ZGAYpCsAhChgbxLd4g zhTrbIPkO5SEp_kD>Ha0m12h5n3s;mE8kn515&nzSf+^D= zyE{JnJ;43l&BH55CL<=W%CF;6iUI)V5C*6!`**KqvzR2=Fj*3Y4`HYwx}TYD445(K z-QtXwtL?m*(F=LVH*H4oM>dXHBW=38q_dZ-_Vr&qpEPxd9Fs95P5W~@Z|Rt+WZP6l zPSQ}~Dh4V?Pp1g&Hk*Px?lm16C@X6M29Vrk%Rw@E||E-v~$ zb_E~{z<}#8i`Mx9mkqtd#Z1lZ-E_J8I+2oumc#x1)jdvh{W76NKm6x-RYpM~v!P8$ zw3e|YVf|}Hse9~oC@N7^j}Fi$hNpyaYnu1}bdXsD=^oI*%WKvbme|BI}$G3>smu#6y)ls|j? zF7Bhu9Z)j)C;3cZb+I>0stSK^WLOYV^U{pUYkgv>?+Nt^5j*CUB=eGw-CvU&40>y~ zGoHLXxY^7k5Xgv62{iQy|5jJQuq0|LU`}lE@flQ2Z*Zn*VWcQjm4FTb>LSVox^S4q zLn`LfS@mrjKCmg$nb^af?d?0&$aX6#2u(JyzIJvuJ*lwPrh|0~aEnSACCTezSdG%h zmSQg`17j@$Iq)r1&?+eR@1nlX|H`<}_!?BQSF&N+QQnvEAqZe+mIFui!0V49R?|9*$ zv!K1A01{8xq;L()Tv*Qk0-$Oj6+vCT*TUD{HvxO@3JjxBwM!4g3ydy&eaJw4CoQBF zJtULJ!YxgNR7_Ls%LmogyI7uIs=!B&?=MYY^yX+v;j@D_xGeZg>eZk0C;4e|HRNSi z6KlD9>q=3v-$4Zik&^ZDhNm1X)+7LCH1k!s+T3tn zUn@={1U&NJLq@K?~w|(=Y<4W{ucX}FdRr6pLw(l2$iK)At%t3gYBMlJz#(K0Nqm;=KAML!&MMSNz=%k=j*zh77r34Rs37iCY` z=_kva_41bdrj(b=4Wc5MO0~q^z#pIWJ>)vDSgIQF=3JVJe1iDy%h)8oNy{s_r&;m` zL{DYKSB_5xRb9xKNOS{qAY3qv5sSXVrrf%~*q5HO|CQ&lbKMePa$M5D{vlJcoGrCZ zD?fKbZN$6rWwz)w7`9h4DAmh1ij2}EO|bO#A9L0_RW6l*$sPPUJrUbhLC75L9%W5iO$Iw5~Yut-qBeu~hF|xD7-eQ%l z412vpq_;t%^F*pYDk%Q35c-erK|6Ve=FxQbAv~ikZ4c9$Y4;ee#ciOD9{yRqf55Qk zumv}#+JciT|Gj$uFOxBUze)=?l{B}qaC0_7m`t82<$K53!4Xvi9Tr)ADp3Off?O8o zVDG0Yx|tfn@r((m?Nxrh(b0DGjg)$;DfO&$6uY;4&F!4jnxkhP}Y3x zS?WFFt>=HWzqlQhffVfvM$Ta8Sg*r3j!Eo&rUOW7SCL2~lG7<+XZ;+{&8h5g8ElI+P>>yR2U%S93NN!Xhm|C682t6ysH-=o1=Bd*N*VlnG%l+KZFtjG`UkL;%65qn0UYQ`h zh0{9jDQx(`aBe7J0Aj3Z)4}`A|4OMM0a;?{j}qkYwi)~O8$9D}ITiMH2buiU>ixYp zhL${nwj6X($*OwmpVG`y5b6v45tX*J8?og}Qju6eJ9H}`X87iEd%BUo7<`2q(HJx+ zMR}d-J4oAf{V1W^a2~`M-YAdZ81dd4o6NPO{cmZaAS@RS4ir#Sr zfFZO-VIL|VN<%nEXr2` z$0FK2L#8O_f1w~c@G70JrB@N}r(gJ!Vmkk6{r68w!o$qO?HrFcjeU0_3F5;*!E2%( zTx>4?gP8w z1B?3UVZmz^%d_dIps>>0{cB~mp3{9UoPR6uQFecVq&} zY{ebB?AlPAD_}(ll{fK99;Wh1cgRbnw)maD^F>*J!R}eHM*W0VYN1TADWMy9H=$00 z5bHY${oDgwX7(W9LZw?}{!8(_{JB~Xkje6{0x4fgC4kUmpfJ+LT1DYD*TWu4#h{Y7 zFLronmc=hS=W=j1ar3r1JNjQoWo2hMWsqW*e?TF%#&{GpsaLp}iN~$)ar+7Ti}E&X z-nq~+Gkp(`qF0F_4A22>VZn-x>I$?PDZSeG8h_ifoWf^DxIb5%T7UytYo3}F|4#RC zUHpg$=)qVqD~=m(!~?XwocuxU1u}9qhhM7d^eqmJPi_e-!IO`*{u7A zbu*?L$Mbj-X9n3G2>+Kc#l`@d8}Xb9{l*IN{#M*d;s+3Pdr8FO$EBELR=8{ zd?LJbSv9fI`{OqTH)5{b?WulgMb)psp+W|@cSp=jtl-&5C}9lw@*0H+gEW(}mAWNz zf{~U;;N}|wdSaphgqnH{FWUy!{y3^=AC*c?RJ5Eb<^ zCgH_v7^axIUVmHSFL^zlj2R$zow$|y#7>%#U7d#Vp_ezcp3lefMyd5ES=q$>4pWyA zp_Zso^^NP~lu2=S6nD(3Z5u=Uy&B&F1i$J*3;3KhEkD_lgscHGR*;T;U!9vgQa(hI}oh9IzEf_PU_8F+i77t-~gDX z490Sb)LyVZmf18N6w{+37$aO<2!Av0 ztLaPOv^J<2@p{WnMiDudoghX_`luFZt_4eNU}*~cF5i%eEcNLs;D>QVIwr8mH;=dc z09`}JV;aaF;13@&iS(w>Jc=k~|d_1hcpM(l|O zu>!@}me%isTT$xT#hNUvh(ATd0wT4fbv=6htcHNEZIw9%E6wlYmwfu2{j0kh1y=$;Yf!|NldgB9ul zB{dbE&LfRnr8ITm@;-68wo#VV?8lG3ed&9k1}QBS3}WGV9%26?A1rBkkDR9Z3o+g+ z)eQg8BY3y(Dh5&z?VLLNdDV`C=muUvCPpGg!oYxIgOI3^%4>5d7jTh~ni!Fg2;fhx z(*c%H6Je84kmQh;5tC3*l~7khLxK-e|Cz?FLh!yYe7g|*LwqU?2wv^_ZyKT$fYVkGJo@AK0$+ml?}zJeB~deT2WL1vz}dxB z)y??t!}%M@)u$_IyW~)6u1SttJ!awd6N5lx|xBrmyrBh>tb&D*=C+Z3nPfq$1%WgY0bY*?PZ#Hk|=xn zGM#0*w4CaB^y0G(J4q=;5NeM@m-P}#mv7QZNF)M!dK^w{mk_!n0`+Y3PQutu-%NBt zzgPXug?JLEbUL{e_dk;Vd896&yPe(hliVK!lj%5+@BKdcrEZ2Nc_*i@ve*2lB>u~{ zFozd2FM|_0+nAGR4TLNHanQn_Oeb!JrUcvzJ?7p9TTNB}ocO3j$7ij!li8#k6 z@2tSd1>K03K9A#_-MIq)S;T#oE^;>U$)&}okIvDf3lm?kI{d80$>~xKUoS!%q1Pi?WpsUUt(tI ztjNjY*y&Rm9(S(DC2GuPHBJs@5M{RGm`c1z<6nwyN^)rMo-AS{M2$oM9|y%fM|}G~ DHx0+F literal 0 HcmV?d00001 diff --git a/congestion-service/gradle/wrapper/gradle-wrapper.properties b/congestion-service/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/congestion-service/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/congestion-service/gradlew b/congestion-service/gradlew new file mode 100755 index 0000000..faf9300 --- /dev/null +++ b/congestion-service/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/congestion-service/gradlew.bat b/congestion-service/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/congestion-service/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/congestion-service/settings.gradle b/congestion-service/settings.gradle new file mode 100644 index 0000000..5d18bb7 --- /dev/null +++ b/congestion-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'congestion-service' diff --git a/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java b/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java new file mode 100644 index 0000000..459ab5a --- /dev/null +++ b/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java @@ -0,0 +1,13 @@ +package com.example.congestionservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CongestionServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(CongestionServiceApplication.class, args); + } + +} diff --git a/congestion-service/src/main/resources/application.properties b/congestion-service/src/main/resources/application.properties new file mode 100644 index 0000000..a5977bd --- /dev/null +++ b/congestion-service/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=congestion-service diff --git a/congestion-service/src/test/java/com/example/congestionservice/CongestionServiceApplicationTests.java b/congestion-service/src/test/java/com/example/congestionservice/CongestionServiceApplicationTests.java new file mode 100644 index 0000000..47b2dc2 --- /dev/null +++ b/congestion-service/src/test/java/com/example/congestionservice/CongestionServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.congestionservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CongestionServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/external-info-service/.gitattributes b/external-info-service/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/external-info-service/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/external-info-service/.gitignore b/external-info-service/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/external-info-service/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/external-info-service/build.gradle b/external-info-service/build.gradle new file mode 100644 index 0000000..3fa973d --- /dev/null +++ b/external-info-service/build.gradle @@ -0,0 +1,57 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.4' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + // JWT 처리 + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.apache.kafka:kafka-streams' + implementation 'org.springframework.kafka:spring-kafka' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'org.postgresql:postgresql' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.kafka:spring-kafka-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('bootBuildImage') { + builder = 'paketobuildpacks/builder-jammy-base:latest' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/external-info-service/gradle/wrapper/gradle-wrapper.jar b/external-info-service/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9bbc975c742b298b441bfb90dbc124400a3751b9 GIT binary patch literal 43705 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 DHx0+F literal 0 HcmV?d00001 diff --git a/external-info-service/gradle/wrapper/gradle-wrapper.properties b/external-info-service/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/external-info-service/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/external-info-service/gradlew b/external-info-service/gradlew new file mode 100755 index 0000000..faf9300 --- /dev/null +++ b/external-info-service/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/external-info-service/gradlew.bat b/external-info-service/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/external-info-service/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/external-info-service/settings.gradle b/external-info-service/settings.gradle new file mode 100644 index 0000000..149d5d4 --- /dev/null +++ b/external-info-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'external-info-service' diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java new file mode 100644 index 0000000..8ad5cab --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java @@ -0,0 +1,13 @@ +package com.example.externalinfoservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ExternalInfoServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(ExternalInfoServiceApplication.class, args); + } + +} diff --git a/external-info-service/src/test/java/com/example/externalinfoservice/ExternalInfoServiceApplicationTests.java b/external-info-service/src/test/java/com/example/externalinfoservice/ExternalInfoServiceApplicationTests.java new file mode 100644 index 0000000..0e30f4c --- /dev/null +++ b/external-info-service/src/test/java/com/example/externalinfoservice/ExternalInfoServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.externalinfoservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ExternalInfoServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/place-service/.gitattributes b/place-service/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/place-service/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/place-service/.gitignore b/place-service/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/place-service/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/place-service/build.gradle b/place-service/build.gradle new file mode 100644 index 0000000..6329d25 --- /dev/null +++ b/place-service/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.4' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + // JWT 처리 + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.apache.kafka:kafka-streams' + implementation 'org.springframework.kafka:spring-kafka' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'org.postgresql:postgresql' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.kafka:spring-kafka-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('bootBuildImage') { + builder = 'paketobuildpacks/builder-jammy-base:latest' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/place-service/gradle/wrapper/gradle-wrapper.jar b/place-service/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9bbc975c742b298b441bfb90dbc124400a3751b9 GIT binary patch literal 43705 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 DHx0+F literal 0 HcmV?d00001 diff --git a/place-service/gradle/wrapper/gradle-wrapper.properties b/place-service/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/place-service/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/place-service/gradlew b/place-service/gradlew new file mode 100755 index 0000000..faf9300 --- /dev/null +++ b/place-service/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/place-service/gradlew.bat b/place-service/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/place-service/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/place-service/settings.gradle b/place-service/settings.gradle new file mode 100644 index 0000000..dad92db --- /dev/null +++ b/place-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'place-service' diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java new file mode 100644 index 0000000..7aaef28 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java @@ -0,0 +1,13 @@ +package com.example.placeservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class PlaceServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(PlaceServiceApplication.class, args); + } + +} diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties new file mode 100644 index 0000000..9071dcb --- /dev/null +++ b/place-service/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=place-service diff --git a/place-service/src/test/java/com/example/placeservice/PlaceServiceApplicationTests.java b/place-service/src/test/java/com/example/placeservice/PlaceServiceApplicationTests.java new file mode 100644 index 0000000..07f3276 --- /dev/null +++ b/place-service/src/test/java/com/example/placeservice/PlaceServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.placeservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class PlaceServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/user-service/.gitattributes b/user-service/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/user-service/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/user-service/.gitignore b/user-service/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/user-service/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/user-service/build.gradle b/user-service/build.gradle new file mode 100644 index 0000000..6329d25 --- /dev/null +++ b/user-service/build.gradle @@ -0,0 +1,58 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.4' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + // JWT 처리 + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.apache.kafka:kafka-streams' + implementation 'org.springframework.kafka:spring-kafka' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + runtimeOnly 'org.postgresql:postgresql' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.kafka:spring-kafka-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('bootBuildImage') { + builder = 'paketobuildpacks/builder-jammy-base:latest' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/user-service/gradle/wrapper/gradle-wrapper.jar b/user-service/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..9bbc975c742b298b441bfb90dbc124400a3751b9 GIT binary patch literal 43705 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 DHx0+F literal 0 HcmV?d00001 diff --git a/user-service/gradle/wrapper/gradle-wrapper.properties b/user-service/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/user-service/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/user-service/gradlew b/user-service/gradlew new file mode 100755 index 0000000..faf9300 --- /dev/null +++ b/user-service/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/user-service/gradlew.bat b/user-service/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/user-service/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/user-service/settings.gradle b/user-service/settings.gradle new file mode 100644 index 0000000..eb2f57d --- /dev/null +++ b/user-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'user-service' diff --git a/user-service/src/main/java/com/example/userservice/UserServiceApplication.java b/user-service/src/main/java/com/example/userservice/UserServiceApplication.java new file mode 100644 index 0000000..55a5c33 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/UserServiceApplication.java @@ -0,0 +1,13 @@ +package com.example.userservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UserServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(UserServiceApplication.class, args); + } + +} diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties new file mode 100644 index 0000000..79b5bda --- /dev/null +++ b/user-service/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=user-service diff --git a/user-service/src/test/java/com/example/userservice/UserServiceApplicationTests.java b/user-service/src/test/java/com/example/userservice/UserServiceApplicationTests.java new file mode 100644 index 0000000..0460494 --- /dev/null +++ b/user-service/src/test/java/com/example/userservice/UserServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.userservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class UserServiceApplicationTests { + + @Test + void contextLoads() { + } + +} From 17d214b96ede05eaac77ec980fba0f8726f3f789 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 16 Apr 2025 14:33:57 +0900 Subject: [PATCH 004/361] =?UTF-8?q?Feat:=EA=B4=80=EA=B4=91=EC=A7=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A7=80=EC=97=AD=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 19 +++++++++++++ .../com/example/placeservice/entity/Area.java | 27 +++++++++++++++++++ .../repository/AreaRepository.java | 7 +++++ .../src/main/resources/application.properties | 10 +++++++ 4 files changed, 63 insertions(+) create mode 100644 place-service/src/main/java/com/example/placeservice/entity/Area.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java diff --git a/README.md b/README.md index f611469..4a98003 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,21 @@ # Back-STARS SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. + + +#### 로컬에서 postgreSQL 테스트 +1. Docker 이미지 다운로드 및 컨테이너 실행 + `docker pull postgres:latest` + +2. PostgreSQL 컨테이너 실행 + `docker run --name my-postgres -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin -e POSTGRES_DB=star -p 5432:5432 -d postgres:latest` + +3. Spring Boot 애플리케이션 설정 + - 이제, Spring Boot 애플리케이션에서 PostgreSQL과 연결 설정. application.properties에 PostgreSQL 데이터베이스 설정 추가 + ``` + spring.datasource.url=jdbc:postgresql://localhost:5432/star + spring.datasource.username=admin + spring.datasource.password=admin + spring.datasource.driver-class-name=org.postgresql.Driver + spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect + spring.jpa.hibernate.ddl-auto=update + ``` \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java new file mode 100644 index 0000000..faa2087 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -0,0 +1,27 @@ +package com.example.placeservice.entity; + +import jakarta.persistence.*; + +import java.math.BigDecimal; + +@Entity +public class Area { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "area_id") + private Long area_id; + + @Column(name = "name", length = 50, nullable = false) + private String name; // 지역명 + + @Column(name = "category", length = 20, nullable = false) + private String category; // 구분 + + @Column(name = "x", precision = 10, scale = 6, nullable = false) + private BigDecimal x; // 위도 + + @Column(name = "y", precision = 10, scale = 6, nullable = false) + private BigDecimal y; // 경도 + + +} diff --git a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java new file mode 100644 index 0000000..13d7a2b --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java @@ -0,0 +1,7 @@ +package com.example.placeservice.repository; + +import com.example.placeservice.entity.Area; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AreaRepository extends JpaRepository { +} diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 9071dcb..d5cbebf 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -1 +1,11 @@ spring.application.name=place-service + + +# postgreSQL ?? +spring.datasource.url=jdbc:postgresql://localhost:5432/star +spring.datasource.username=admin +spring.datasource.password=admin +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update From f1311746b71d28cd04cfe7d4d18c42c42b703768 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 16 Apr 2025 14:33:57 +0900 Subject: [PATCH 005/361] =?UTF-8?q?Feat:=EA=B4=80=EA=B4=91=EC=A7=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A7=80=EC=97=AD=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 19 +++++++++++++ .../com/example/placeservice/entity/Area.java | 27 +++++++++++++++++++ .../repository/AreaRepository.java | 7 +++++ .../src/main/resources/application.properties | 10 +++++++ 4 files changed, 63 insertions(+) create mode 100644 place-service/src/main/java/com/example/placeservice/entity/Area.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java diff --git a/README.md b/README.md index f611469..4a98003 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,21 @@ # Back-STARS SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. + + +#### 로컬에서 postgreSQL 테스트 +1. Docker 이미지 다운로드 및 컨테이너 실행 + `docker pull postgres:latest` + +2. PostgreSQL 컨테이너 실행 + `docker run --name my-postgres -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin -e POSTGRES_DB=star -p 5432:5432 -d postgres:latest` + +3. Spring Boot 애플리케이션 설정 + - 이제, Spring Boot 애플리케이션에서 PostgreSQL과 연결 설정. application.properties에 PostgreSQL 데이터베이스 설정 추가 + ``` + spring.datasource.url=jdbc:postgresql://localhost:5432/star + spring.datasource.username=admin + spring.datasource.password=admin + spring.datasource.driver-class-name=org.postgresql.Driver + spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect + spring.jpa.hibernate.ddl-auto=update + ``` \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java new file mode 100644 index 0000000..faa2087 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -0,0 +1,27 @@ +package com.example.placeservice.entity; + +import jakarta.persistence.*; + +import java.math.BigDecimal; + +@Entity +public class Area { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "area_id") + private Long area_id; + + @Column(name = "name", length = 50, nullable = false) + private String name; // 지역명 + + @Column(name = "category", length = 20, nullable = false) + private String category; // 구분 + + @Column(name = "x", precision = 10, scale = 6, nullable = false) + private BigDecimal x; // 위도 + + @Column(name = "y", precision = 10, scale = 6, nullable = false) + private BigDecimal y; // 경도 + + +} diff --git a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java new file mode 100644 index 0000000..13d7a2b --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java @@ -0,0 +1,7 @@ +package com.example.placeservice.repository; + +import com.example.placeservice.entity.Area; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AreaRepository extends JpaRepository { +} diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 9071dcb..d5cbebf 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -1 +1,11 @@ spring.application.name=place-service + + +# postgreSQL ?? +spring.datasource.url=jdbc:postgresql://localhost:5432/star +spring.datasource.username=admin +spring.datasource.password=admin +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update From 4e50c2752f83fdb447ab381082b9a28a2997cf36 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 16 Apr 2025 17:15:30 +0900 Subject: [PATCH 006/361] =?UTF-8?q?Feat:=20API=20=EC=B6=94=EA=B0=80--?= =?UTF-8?q?=EB=8F=84=EB=A1=9C=EC=86=8C=ED=86=B5=20=ED=98=84=ED=99=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auth-service/.gitattributes | 3 - auth-service/.gitignore | 37 --- auth-service/build.gradle | 58 ---- .../gradle/wrapper/gradle-wrapper.jar | Bin 43705 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - auth-service/gradlew | 251 ------------------ auth-service/gradlew.bat | 94 ------- auth-service/settings.gradle | 1 - .../authservice/AuthServiceApplication.java | 13 - .../src/main/resources/application.properties | 1 - .../AuthServiceApplicationTests.java | 13 - external-info-service/build.gradle | 5 + .../externalinfoservice/config/AppConfig.java | 13 + .../config/SecurityConfig.java | 22 ++ .../controller/RoadController.java | 21 ++ .../externalinfoservice/dto/RoadDto.java | 115 ++++++++ .../service/RoadService.java | 48 ++++ .../src/main/resources/application.properties | 10 + 18 files changed, 234 insertions(+), 478 deletions(-) delete mode 100644 auth-service/.gitattributes delete mode 100644 auth-service/.gitignore delete mode 100644 auth-service/build.gradle delete mode 100644 auth-service/gradle/wrapper/gradle-wrapper.jar delete mode 100644 auth-service/gradle/wrapper/gradle-wrapper.properties delete mode 100755 auth-service/gradlew delete mode 100644 auth-service/gradlew.bat delete mode 100644 auth-service/settings.gradle delete mode 100644 auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java delete mode 100644 auth-service/src/main/resources/application.properties delete mode 100644 auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java diff --git a/auth-service/.gitattributes b/auth-service/.gitattributes deleted file mode 100644 index 8af972c..0000000 --- a/auth-service/.gitattributes +++ /dev/null @@ -1,3 +0,0 @@ -/gradlew text eol=lf -*.bat text eol=crlf -*.jar binary diff --git a/auth-service/.gitignore b/auth-service/.gitignore deleted file mode 100644 index c2065bc..0000000 --- a/auth-service/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -HELP.md -.gradle -build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ - -### STS ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache -bin/ -!**/src/main/**/bin/ -!**/src/test/**/bin/ - -### IntelliJ IDEA ### -.idea -*.iws -*.iml -*.ipr -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ - -### VS Code ### -.vscode/ diff --git a/auth-service/build.gradle b/auth-service/build.gradle deleted file mode 100644 index f0e2623..0000000 --- a/auth-service/build.gradle +++ /dev/null @@ -1,58 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '3.4.4' - id 'io.spring.dependency-management' version '1.1.7' -} - -group = 'com.example' -version = '0.0.1-SNAPSHOT' - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } -} - -configurations { - compileOnly { - extendsFrom annotationProcessor - } -} - -repositories { - mavenCentral() -} - -dependencies { - // JWT 처리 - implementation 'io.jsonwebtoken:jjwt-api:0.12.6' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' - - - implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.apache.kafka:kafka-streams' - implementation 'org.springframework.kafka:spring-kafka' - implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' - compileOnly 'org.projectlombok:lombok' - developmentOnly 'org.springframework.boot:spring-boot-devtools' - runtimeOnly 'org.postgresql:postgresql' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.kafka:spring-kafka-test' - testImplementation 'org.springframework.security:spring-security-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' -} - -tasks.named('bootBuildImage') { - builder = 'paketobuildpacks/builder-jammy-base:latest' -} - -tasks.named('test') { - useJUnitPlatform() -} diff --git a/auth-service/gradle/wrapper/gradle-wrapper.jar b/auth-service/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 9bbc975c742b298b441bfb90dbc124400a3751b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43705 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 DHx0+F diff --git a/auth-service/gradle/wrapper/gradle-wrapper.properties b/auth-service/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 37f853b..0000000 --- a/auth-service/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/auth-service/gradlew b/auth-service/gradlew deleted file mode 100755 index faf9300..0000000 --- a/auth-service/gradlew +++ /dev/null @@ -1,251 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/auth-service/gradlew.bat b/auth-service/gradlew.bat deleted file mode 100644 index 9d21a21..0000000 --- a/auth-service/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/auth-service/settings.gradle b/auth-service/settings.gradle deleted file mode 100644 index d4c3eb3..0000000 --- a/auth-service/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'auth-service' diff --git a/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java b/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java deleted file mode 100644 index 59186b4..0000000 --- a/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.authservice; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class AuthServiceApplication { - - public static void main(String[] args) { - SpringApplication.run(AuthServiceApplication.class, args); - } - -} diff --git a/auth-service/src/main/resources/application.properties b/auth-service/src/main/resources/application.properties deleted file mode 100644 index e536707..0000000 --- a/auth-service/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=auth-service diff --git a/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java b/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java deleted file mode 100644 index 2e763c4..0000000 --- a/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.authservice; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class AuthServiceApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/external-info-service/build.gradle b/external-info-service/build.gradle index 3fa973d..3b8f4df 100644 --- a/external-info-service/build.gradle +++ b/external-info-service/build.gradle @@ -24,6 +24,11 @@ repositories { } dependencies { + // JAXB (XML 파싱) + implementation 'javax.xml.bind:jaxb-api:2.3.1' + // JAXB Runtime (XML 파싱을 위해 필요) + implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3' + // JWT 처리 implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java b/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java new file mode 100644 index 0000000..8e423a6 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java @@ -0,0 +1,13 @@ +package com.example.externalinfoservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java b/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java new file mode 100644 index 0000000..f77354b --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java @@ -0,0 +1,22 @@ +package com.example.externalinfoservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() // 모든 요청 허용 + ); + + return http.build(); + } + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java new file mode 100644 index 0000000..44bfd26 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java @@ -0,0 +1,21 @@ +package com.example.externalinfoservice.controller; + +import com.example.externalinfoservice.service.RoadService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/traffic") +@RequiredArgsConstructor +public class RoadController { + private final RoadService roadService; + + @GetMapping("/{area-id}") + public String getRoadData(@PathVariable("area-id") String id) { + // TrafficService 호출하여 OpenAPI 데이터를 JSON 형식으로 반환 + return roadService.getTrafficData(id); + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java new file mode 100644 index 0000000..ae24bad --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java @@ -0,0 +1,115 @@ +package com.example.externalinfoservice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import lombok.Data; + +import java.util.List; + +@Data +@XmlRootElement(name = "SeoulRtd.citydata") +@XmlAccessorType(XmlAccessType.FIELD) +public class RoadDto { + + @XmlElement(name = "CITYDATA") + @JsonProperty("city_data") + private CityData cityData; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class CityData { + @XmlElement(name = "ROAD_TRAFFIC_STTS") + @JsonProperty("road_traffic_stts") + private List roadTrafficSttsList; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class RoadTrafficStts { + @XmlElement(name = "AVG_ROAD_DATA") + @JsonProperty("avg_road_data") + private AvgRoadData avgRoadData; + + @XmlElement(name = "ROAD_TRAFFIC_STTS") + @JsonProperty("road_traffic_stts") + private RoadTrafficDetail roadTrafficDetail; + } + + + @XmlAccessorType(XmlAccessType.FIELD) + public static class AvgRoadData { + @XmlElement(name = "ROAD_MSG") + @JsonProperty("road_msg") + private String roadMsg; + + @XmlElement(name = "ROAD_TRAFFIC_IDX") + @JsonProperty("road_traffic_idx") + private String trafficIdx; + + @XmlElement(name = "ROAD_TRAFFIC_SPD") + @JsonProperty("road_traffic_spd") + private int trafficSpeed; + + @XmlElement(name = "ROAD_TRAFFIC_TIME") + @JsonProperty("road_traffic_time") + private String trafficTime; + + // Getters & Setters + } + + @XmlAccessorType(XmlAccessType.FIELD) + public static class RoadTrafficDetail { + @XmlElement(name = "LINK_ID") + @JsonProperty("link_id") + private String linkId; + + @XmlElement(name = "ROAD_NM") + @JsonProperty("road_nm") + private String roadName; + + @XmlElement(name = "START_ND_CD") + @JsonProperty("start_nd_cd") + private String startNodeCode; + + @XmlElement(name = "START_ND_NM") + @JsonProperty("start_nd_nm") + private String startNodeName; + + @XmlElement(name = "START_ND_XY") + @JsonProperty("start_nd_xy") + private String startCoordinates; + + @XmlElement(name = "END_ND_CD") + @JsonProperty("end_nd_cd") + private String endNodeCode; + + @XmlElement(name = "END_ND_NM") + @JsonProperty("end_nd_nm") + private String endNodeName; + + @XmlElement(name = "END_ND_XY") + @JsonProperty("end_nd_xy") + private String endCoordinates; + + @XmlElement(name = "DIST") + @JsonProperty("dist") + private double distance; + + @XmlElement(name = "SPD") + @JsonProperty("spd") + private double speed; + + @XmlElement(name = "IDX") + @JsonProperty("idx") + private String congestionIndex; + + @XmlElement(name = "XYLIST") + @JsonProperty("xylist") + private String xyList; + + // Getters & Setters + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java new file mode 100644 index 0000000..40ea77f --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java @@ -0,0 +1,48 @@ +package com.example.externalinfoservice.service; + +import com.example.externalinfoservice.dto.RoadDto; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Unmarshaller; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.io.StringReader; + +@Service +public class RoadService { + + @Value("http://openapi.seoul.go.kr:8088/sample/xml/citydata/1/5/") + private String openApiUrl; + + private final RestTemplate restTemplate; + + public RoadService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public String getTrafficData(String id) { + // 1. 외부 OpenAPI에서 XML 데이터 받아오기 + String xmlResponse = restTemplate.getForObject(openApiUrl + id, String.class); + + // 2. XML 데이터를 DTO로 변환 + try { + // DTO -> JSON + ObjectMapper objectMapper = new ObjectMapper(); + String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); + + return json; + } catch (Exception e) { + throw new RuntimeException("XML 파싱 실패", e); + } + } + + private RoadDto xmlToDto(String xml) throws Exception { + // JAXB를 사용해서 XML을 DTO로 변환 + JAXBContext context = JAXBContext.newInstance(RoadDto.class); + Unmarshaller unmarshaller = context.createUnmarshaller(); + return (RoadDto) unmarshaller.unmarshal(new StringReader(xml)); + } +} diff --git a/external-info-service/src/main/resources/application.properties b/external-info-service/src/main/resources/application.properties index 82efef9..381e81a 100644 --- a/external-info-service/src/main/resources/application.properties +++ b/external-info-service/src/main/resources/application.properties @@ -1 +1,11 @@ spring.application.name=external-info-service + + +# postgreSQL ?? +spring.datasource.url=jdbc:postgresql://localhost:5432/star +spring.datasource.username=admin +spring.datasource.password=admin +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update From 77c00d94bc26b4896161030bf5f1e6e795ad06ac Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 16 Apr 2025 17:15:30 +0900 Subject: [PATCH 007/361] =?UTF-8?q?Feat:=20API=20=EC=B6=94=EA=B0=80--?= =?UTF-8?q?=EB=8F=84=EB=A1=9C=EC=86=8C=ED=86=B5=20=ED=98=84=ED=99=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auth-service/.gitattributes | 3 - auth-service/.gitignore | 37 --- auth-service/build.gradle | 58 ---- .../gradle/wrapper/gradle-wrapper.jar | Bin 43705 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - auth-service/gradlew | 251 ------------------ auth-service/gradlew.bat | 94 ------- auth-service/settings.gradle | 1 - .../authservice/AuthServiceApplication.java | 13 - .../src/main/resources/application.properties | 1 - .../AuthServiceApplicationTests.java | 13 - external-info-service/build.gradle | 5 + .../externalinfoservice/config/AppConfig.java | 13 + .../config/SecurityConfig.java | 22 ++ .../controller/RoadController.java | 21 ++ .../externalinfoservice/dto/RoadDto.java | 115 ++++++++ .../service/RoadService.java | 48 ++++ 17 files changed, 224 insertions(+), 478 deletions(-) delete mode 100644 auth-service/.gitattributes delete mode 100644 auth-service/.gitignore delete mode 100644 auth-service/build.gradle delete mode 100644 auth-service/gradle/wrapper/gradle-wrapper.jar delete mode 100644 auth-service/gradle/wrapper/gradle-wrapper.properties delete mode 100755 auth-service/gradlew delete mode 100644 auth-service/gradlew.bat delete mode 100644 auth-service/settings.gradle delete mode 100644 auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java delete mode 100644 auth-service/src/main/resources/application.properties delete mode 100644 auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java diff --git a/auth-service/.gitattributes b/auth-service/.gitattributes deleted file mode 100644 index 8af972c..0000000 --- a/auth-service/.gitattributes +++ /dev/null @@ -1,3 +0,0 @@ -/gradlew text eol=lf -*.bat text eol=crlf -*.jar binary diff --git a/auth-service/.gitignore b/auth-service/.gitignore deleted file mode 100644 index c2065bc..0000000 --- a/auth-service/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -HELP.md -.gradle -build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/**/build/ -!**/src/test/**/build/ - -### STS ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache -bin/ -!**/src/main/**/bin/ -!**/src/test/**/bin/ - -### IntelliJ IDEA ### -.idea -*.iws -*.iml -*.ipr -out/ -!**/src/main/**/out/ -!**/src/test/**/out/ - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ - -### VS Code ### -.vscode/ diff --git a/auth-service/build.gradle b/auth-service/build.gradle deleted file mode 100644 index f0e2623..0000000 --- a/auth-service/build.gradle +++ /dev/null @@ -1,58 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '3.4.4' - id 'io.spring.dependency-management' version '1.1.7' -} - -group = 'com.example' -version = '0.0.1-SNAPSHOT' - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } -} - -configurations { - compileOnly { - extendsFrom annotationProcessor - } -} - -repositories { - mavenCentral() -} - -dependencies { - // JWT 처리 - implementation 'io.jsonwebtoken:jjwt-api:0.12.6' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' - - - implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.apache.kafka:kafka-streams' - implementation 'org.springframework.kafka:spring-kafka' - implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' - compileOnly 'org.projectlombok:lombok' - developmentOnly 'org.springframework.boot:spring-boot-devtools' - runtimeOnly 'org.postgresql:postgresql' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.kafka:spring-kafka-test' - testImplementation 'org.springframework.security:spring-security-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' -} - -tasks.named('bootBuildImage') { - builder = 'paketobuildpacks/builder-jammy-base:latest' -} - -tasks.named('test') { - useJUnitPlatform() -} diff --git a/auth-service/gradle/wrapper/gradle-wrapper.jar b/auth-service/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 9bbc975c742b298b441bfb90dbc124400a3751b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43705 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 DHx0+F diff --git a/auth-service/gradle/wrapper/gradle-wrapper.properties b/auth-service/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 37f853b..0000000 --- a/auth-service/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/auth-service/gradlew b/auth-service/gradlew deleted file mode 100755 index faf9300..0000000 --- a/auth-service/gradlew +++ /dev/null @@ -1,251 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/auth-service/gradlew.bat b/auth-service/gradlew.bat deleted file mode 100644 index 9d21a21..0000000 --- a/auth-service/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/auth-service/settings.gradle b/auth-service/settings.gradle deleted file mode 100644 index d4c3eb3..0000000 --- a/auth-service/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'auth-service' diff --git a/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java b/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java deleted file mode 100644 index 59186b4..0000000 --- a/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.authservice; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class AuthServiceApplication { - - public static void main(String[] args) { - SpringApplication.run(AuthServiceApplication.class, args); - } - -} diff --git a/auth-service/src/main/resources/application.properties b/auth-service/src/main/resources/application.properties deleted file mode 100644 index e536707..0000000 --- a/auth-service/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=auth-service diff --git a/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java b/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java deleted file mode 100644 index 2e763c4..0000000 --- a/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.authservice; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class AuthServiceApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/external-info-service/build.gradle b/external-info-service/build.gradle index 3fa973d..3b8f4df 100644 --- a/external-info-service/build.gradle +++ b/external-info-service/build.gradle @@ -24,6 +24,11 @@ repositories { } dependencies { + // JAXB (XML 파싱) + implementation 'javax.xml.bind:jaxb-api:2.3.1' + // JAXB Runtime (XML 파싱을 위해 필요) + implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3' + // JWT 처리 implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java b/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java new file mode 100644 index 0000000..8e423a6 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java @@ -0,0 +1,13 @@ +package com.example.externalinfoservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java b/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java new file mode 100644 index 0000000..f77354b --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java @@ -0,0 +1,22 @@ +package com.example.externalinfoservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() // 모든 요청 허용 + ); + + return http.build(); + } + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java new file mode 100644 index 0000000..44bfd26 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java @@ -0,0 +1,21 @@ +package com.example.externalinfoservice.controller; + +import com.example.externalinfoservice.service.RoadService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/traffic") +@RequiredArgsConstructor +public class RoadController { + private final RoadService roadService; + + @GetMapping("/{area-id}") + public String getRoadData(@PathVariable("area-id") String id) { + // TrafficService 호출하여 OpenAPI 데이터를 JSON 형식으로 반환 + return roadService.getTrafficData(id); + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java new file mode 100644 index 0000000..ae24bad --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java @@ -0,0 +1,115 @@ +package com.example.externalinfoservice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import lombok.Data; + +import java.util.List; + +@Data +@XmlRootElement(name = "SeoulRtd.citydata") +@XmlAccessorType(XmlAccessType.FIELD) +public class RoadDto { + + @XmlElement(name = "CITYDATA") + @JsonProperty("city_data") + private CityData cityData; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class CityData { + @XmlElement(name = "ROAD_TRAFFIC_STTS") + @JsonProperty("road_traffic_stts") + private List roadTrafficSttsList; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class RoadTrafficStts { + @XmlElement(name = "AVG_ROAD_DATA") + @JsonProperty("avg_road_data") + private AvgRoadData avgRoadData; + + @XmlElement(name = "ROAD_TRAFFIC_STTS") + @JsonProperty("road_traffic_stts") + private RoadTrafficDetail roadTrafficDetail; + } + + + @XmlAccessorType(XmlAccessType.FIELD) + public static class AvgRoadData { + @XmlElement(name = "ROAD_MSG") + @JsonProperty("road_msg") + private String roadMsg; + + @XmlElement(name = "ROAD_TRAFFIC_IDX") + @JsonProperty("road_traffic_idx") + private String trafficIdx; + + @XmlElement(name = "ROAD_TRAFFIC_SPD") + @JsonProperty("road_traffic_spd") + private int trafficSpeed; + + @XmlElement(name = "ROAD_TRAFFIC_TIME") + @JsonProperty("road_traffic_time") + private String trafficTime; + + // Getters & Setters + } + + @XmlAccessorType(XmlAccessType.FIELD) + public static class RoadTrafficDetail { + @XmlElement(name = "LINK_ID") + @JsonProperty("link_id") + private String linkId; + + @XmlElement(name = "ROAD_NM") + @JsonProperty("road_nm") + private String roadName; + + @XmlElement(name = "START_ND_CD") + @JsonProperty("start_nd_cd") + private String startNodeCode; + + @XmlElement(name = "START_ND_NM") + @JsonProperty("start_nd_nm") + private String startNodeName; + + @XmlElement(name = "START_ND_XY") + @JsonProperty("start_nd_xy") + private String startCoordinates; + + @XmlElement(name = "END_ND_CD") + @JsonProperty("end_nd_cd") + private String endNodeCode; + + @XmlElement(name = "END_ND_NM") + @JsonProperty("end_nd_nm") + private String endNodeName; + + @XmlElement(name = "END_ND_XY") + @JsonProperty("end_nd_xy") + private String endCoordinates; + + @XmlElement(name = "DIST") + @JsonProperty("dist") + private double distance; + + @XmlElement(name = "SPD") + @JsonProperty("spd") + private double speed; + + @XmlElement(name = "IDX") + @JsonProperty("idx") + private String congestionIndex; + + @XmlElement(name = "XYLIST") + @JsonProperty("xylist") + private String xyList; + + // Getters & Setters + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java new file mode 100644 index 0000000..40ea77f --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java @@ -0,0 +1,48 @@ +package com.example.externalinfoservice.service; + +import com.example.externalinfoservice.dto.RoadDto; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Unmarshaller; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.io.StringReader; + +@Service +public class RoadService { + + @Value("http://openapi.seoul.go.kr:8088/sample/xml/citydata/1/5/") + private String openApiUrl; + + private final RestTemplate restTemplate; + + public RoadService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public String getTrafficData(String id) { + // 1. 외부 OpenAPI에서 XML 데이터 받아오기 + String xmlResponse = restTemplate.getForObject(openApiUrl + id, String.class); + + // 2. XML 데이터를 DTO로 변환 + try { + // DTO -> JSON + ObjectMapper objectMapper = new ObjectMapper(); + String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); + + return json; + } catch (Exception e) { + throw new RuntimeException("XML 파싱 실패", e); + } + } + + private RoadDto xmlToDto(String xml) throws Exception { + // JAXB를 사용해서 XML을 DTO로 변환 + JAXBContext context = JAXBContext.newInstance(RoadDto.class); + Unmarshaller unmarshaller = context.createUnmarshaller(); + return (RoadDto) unmarshaller.unmarshal(new StringReader(xml)); + } +} From f7a0eb598b552668b132a62b604d5257273ccb20 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 17 Apr 2025 09:19:05 +0900 Subject: [PATCH 008/361] =?UTF-8?q?Docs:=20README=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4a98003..4ef1b76 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # Back-STARS SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. +
+로컬에서 postgreSQL 테스트 -#### 로컬에서 postgreSQL 테스트 1. Docker 이미지 다운로드 및 컨테이너 실행 `docker pull postgres:latest` @@ -18,4 +19,6 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update - ``` \ No newline at end of file + ``` + +
\ No newline at end of file From 8da5c527bcc20f0353f4bbda5b61d62d0bed252b Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 17 Apr 2025 09:19:05 +0900 Subject: [PATCH 009/361] =?UTF-8?q?Docs:=20README=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4a98003..4ef1b76 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # Back-STARS SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. +
+로컬에서 postgreSQL 테스트 -#### 로컬에서 postgreSQL 테스트 1. Docker 이미지 다운로드 및 컨테이너 실행 `docker pull postgres:latest` @@ -18,4 +19,6 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update - ``` \ No newline at end of file + ``` + +
\ No newline at end of file From a5a7f99e257ec863cca2a1bc35376abed8d7ea9f Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 17 Apr 2025 09:19:05 +0900 Subject: [PATCH 010/361] =?UTF-8?q?Docs:=20README=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4a98003..4ef1b76 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # Back-STARS SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. +
+로컬에서 postgreSQL 테스트 -#### 로컬에서 postgreSQL 테스트 1. Docker 이미지 다운로드 및 컨테이너 실행 `docker pull postgres:latest` @@ -18,4 +19,6 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update - ``` \ No newline at end of file + ``` + +
\ No newline at end of file From 68ba7a75bd03c558d13cd39b66d1b7b69c131d26 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Thu, 17 Apr 2025 10:09:06 +0900 Subject: [PATCH 011/361] =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- external-info-service/build.gradle | 13 ++++ .../externalinfoservice/config/AppConfig.java | 13 ++++ .../config/SecurityConfig.java | 22 +++++++ .../controller/ParkingController.java | 21 ++++++ .../externalinfoservice/dto/ParkDto.java | 65 ++++++++++++++++++ .../service/ParkService.java | 66 +++++++++++++++++++ .../src/main/resources/application.properties | 11 ++++ place-service/build.gradle | 1 + .../src/main/resources/application.properties | 7 +- 9 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java diff --git a/external-info-service/build.gradle b/external-info-service/build.gradle index 3fa973d..ff16947 100644 --- a/external-info-service/build.gradle +++ b/external-info-service/build.gradle @@ -29,6 +29,19 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-configuration-processor' + implementation 'org.springframework.boot:spring-boot-starter-json' + implementation 'org.springframework.boot:spring-boot-starter-security' + + implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0' + implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.2' + + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' + + implementation 'org.postgresql:postgresql:42.7.2' implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java b/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java new file mode 100644 index 0000000..8d3e0c9 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java @@ -0,0 +1,13 @@ +package com.example.externalinfoservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java b/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java new file mode 100644 index 0000000..9ebabc7 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java @@ -0,0 +1,22 @@ +package com.example.externalinfoservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() // 모든 요청 허용 + ); + + return http.build(); + } + +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java new file mode 100644 index 0000000..b74d48e --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java @@ -0,0 +1,21 @@ +package com.example.externalinfoservice.controller; + +import com.example.externalinfoservice.service.ParkService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/parking") +@RequiredArgsConstructor +public class ParkingController { + + private final ParkService parkService; + + @GetMapping(value = "/{areaId}", produces = MediaType.APPLICATION_JSON_VALUE) + public Map getParking(@PathVariable String areaId) { + return parkService.getParkingData(areaId); + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java new file mode 100644 index 0000000..0a28d3f --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java @@ -0,0 +1,65 @@ +package com.example.externalinfoservice.dto; + +import jakarta.xml.bind.annotation.*; +import lombok.Data; + +import java.util.List; + +@Data +@XmlRootElement(name = "SeoulRtd.citydata") +@XmlAccessorType(XmlAccessType.FIELD) +public class ParkDto { + + @XmlElement(name = "CITYDATA") + private CityData cityData; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class CityData { + + @XmlElement(name = "PRK_STTS") + private List prkStts; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class ParkingStatus { + + @XmlElement(name = "PRK_NM") + private String PRK_NM; + + @XmlElement(name = "PRK_CD") + private String PRK_CD; + + @XmlElement(name = "PRK_TYPE") + private String PRK_TYPE; + + @XmlElement(name = "CPCTY") + private int CPCTY; + + @XmlElement(name = "CUR_PRK_CNT") + private Integer CUR_PRK_CNT; + + @XmlElement(name = "CUR_PRK_TIME") + private String CUR_PRK_TIME; + + @XmlElement(name = "CUR_PRK_YN") + private String CUR_PRK_YN; + + @XmlElement(name = "PAY_YN") + private String PAY_YN; + + @XmlElement(name = "RATES") + private int RATES; + + @XmlElement(name = "TIME_RATES") + private int TIME_RATES; + + @XmlElement(name = "ADD_RATES") + private int ADD_RATES; + + @XmlElement(name = "ADD_TIME_RATES") + private int ADD_TIME_RATES; + + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java new file mode 100644 index 0000000..1e44fa5 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java @@ -0,0 +1,66 @@ +package com.example.externalinfoservice.service; + +import com.example.externalinfoservice.dto.ParkDto; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +@Service +public class ParkService { + + @Value("${seoul.api.key}") + private String apiKey; + + private final RestTemplate restTemplate; + + public ParkService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public Map getParkingData(String areaId) { + try { + String url = String.format( + "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", + apiKey, areaId + ); + + String xmlResponse = restTemplate.getForObject(url, String.class); + + XmlMapper xmlMapper = new XmlMapper(); + JsonNode root = xmlMapper.readTree(xmlResponse); + + // 실제 PRK_STTS 배열에 접근 + JsonNode prkArray = root + .path("CITYDATA") + .path("PRK_STTS") + .path("PRK_STTS"); + + if (!prkArray.isArray()) { + throw new RuntimeException("PRK_STTS는 배열이 아닙니다."); + } + + // 제거할 필드들: LNG, LAT, ADDRESS, ROAD_ADDR + for (JsonNode node : prkArray) { + if (node.isObject()) { + ((com.fasterxml.jackson.databind.node.ObjectNode) node).remove("LNG"); + ((com.fasterxml.jackson.databind.node.ObjectNode) node).remove("LAT"); + ((com.fasterxml.jackson.databind.node.ObjectNode) node).remove("ADDRESS"); + ((com.fasterxml.jackson.databind.node.ObjectNode) node).remove("ROAD_ADDR"); + } + } + + ObjectMapper objectMapper = new ObjectMapper(); + Object prkListJson = objectMapper.treeToValue(prkArray, Object.class); + + return Map.of("data", Map.of("PRK_STTS", prkListJson)); + + } catch (Exception e) { + throw new RuntimeException("주차장 데이터 파싱 실패: " + e.getMessage(), e); + } + } +} diff --git a/external-info-service/src/main/resources/application.properties b/external-info-service/src/main/resources/application.properties index 82efef9..b45722c 100644 --- a/external-info-service/src/main/resources/application.properties +++ b/external-info-service/src/main/resources/application.properties @@ -1 +1,12 @@ spring.application.name=external-info-service + + +# postgreSQL ?? +spring.datasource.url=jdbc:postgresql://localhost:5432/star +spring.datasource.username=admin +spring.datasource.password=admin +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update +seoul.api.key=505a6255746b696d34344b46566174 diff --git a/place-service/build.gradle b/place-service/build.gradle index 6329d25..e094fb2 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -30,6 +30,7 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + implementation 'org.postgresql:postgresql:42.7.2' implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 9071dcb..4d28859 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -1 +1,6 @@ -spring.application.name=place-service +spring.datasource.url=jdbc:postgresql://localhost:5432/star +spring.datasource.username=admin +spring.datasource.password=admin +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update \ No newline at end of file From 95de034ad23ccfa75f7ac03273648e6aaf4e8094 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Thu, 17 Apr 2025 10:09:06 +0900 Subject: [PATCH 012/361] =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- external-info-service/build.gradle | 13 ++++ .../externalinfoservice/config/AppConfig.java | 13 ++++ .../config/SecurityConfig.java | 22 +++++++ .../controller/ParkingController.java | 21 ++++++ .../externalinfoservice/dto/ParkDto.java | 65 ++++++++++++++++++ .../service/ParkService.java | 66 +++++++++++++++++++ place-service/build.gradle | 1 + .../src/main/resources/application.properties | 7 +- 8 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java diff --git a/external-info-service/build.gradle b/external-info-service/build.gradle index 3fa973d..ff16947 100644 --- a/external-info-service/build.gradle +++ b/external-info-service/build.gradle @@ -29,6 +29,19 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-configuration-processor' + implementation 'org.springframework.boot:spring-boot-starter-json' + implementation 'org.springframework.boot:spring-boot-starter-security' + + implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0' + implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.2' + + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' + + implementation 'org.postgresql:postgresql:42.7.2' implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java b/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java new file mode 100644 index 0000000..8d3e0c9 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java @@ -0,0 +1,13 @@ +package com.example.externalinfoservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java b/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java new file mode 100644 index 0000000..9ebabc7 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java @@ -0,0 +1,22 @@ +package com.example.externalinfoservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() // 모든 요청 허용 + ); + + return http.build(); + } + +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java new file mode 100644 index 0000000..b74d48e --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java @@ -0,0 +1,21 @@ +package com.example.externalinfoservice.controller; + +import com.example.externalinfoservice.service.ParkService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/parking") +@RequiredArgsConstructor +public class ParkingController { + + private final ParkService parkService; + + @GetMapping(value = "/{areaId}", produces = MediaType.APPLICATION_JSON_VALUE) + public Map getParking(@PathVariable String areaId) { + return parkService.getParkingData(areaId); + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java new file mode 100644 index 0000000..0a28d3f --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java @@ -0,0 +1,65 @@ +package com.example.externalinfoservice.dto; + +import jakarta.xml.bind.annotation.*; +import lombok.Data; + +import java.util.List; + +@Data +@XmlRootElement(name = "SeoulRtd.citydata") +@XmlAccessorType(XmlAccessType.FIELD) +public class ParkDto { + + @XmlElement(name = "CITYDATA") + private CityData cityData; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class CityData { + + @XmlElement(name = "PRK_STTS") + private List prkStts; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class ParkingStatus { + + @XmlElement(name = "PRK_NM") + private String PRK_NM; + + @XmlElement(name = "PRK_CD") + private String PRK_CD; + + @XmlElement(name = "PRK_TYPE") + private String PRK_TYPE; + + @XmlElement(name = "CPCTY") + private int CPCTY; + + @XmlElement(name = "CUR_PRK_CNT") + private Integer CUR_PRK_CNT; + + @XmlElement(name = "CUR_PRK_TIME") + private String CUR_PRK_TIME; + + @XmlElement(name = "CUR_PRK_YN") + private String CUR_PRK_YN; + + @XmlElement(name = "PAY_YN") + private String PAY_YN; + + @XmlElement(name = "RATES") + private int RATES; + + @XmlElement(name = "TIME_RATES") + private int TIME_RATES; + + @XmlElement(name = "ADD_RATES") + private int ADD_RATES; + + @XmlElement(name = "ADD_TIME_RATES") + private int ADD_TIME_RATES; + + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java new file mode 100644 index 0000000..1e44fa5 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java @@ -0,0 +1,66 @@ +package com.example.externalinfoservice.service; + +import com.example.externalinfoservice.dto.ParkDto; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +@Service +public class ParkService { + + @Value("${seoul.api.key}") + private String apiKey; + + private final RestTemplate restTemplate; + + public ParkService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public Map getParkingData(String areaId) { + try { + String url = String.format( + "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", + apiKey, areaId + ); + + String xmlResponse = restTemplate.getForObject(url, String.class); + + XmlMapper xmlMapper = new XmlMapper(); + JsonNode root = xmlMapper.readTree(xmlResponse); + + // 실제 PRK_STTS 배열에 접근 + JsonNode prkArray = root + .path("CITYDATA") + .path("PRK_STTS") + .path("PRK_STTS"); + + if (!prkArray.isArray()) { + throw new RuntimeException("PRK_STTS는 배열이 아닙니다."); + } + + // 제거할 필드들: LNG, LAT, ADDRESS, ROAD_ADDR + for (JsonNode node : prkArray) { + if (node.isObject()) { + ((com.fasterxml.jackson.databind.node.ObjectNode) node).remove("LNG"); + ((com.fasterxml.jackson.databind.node.ObjectNode) node).remove("LAT"); + ((com.fasterxml.jackson.databind.node.ObjectNode) node).remove("ADDRESS"); + ((com.fasterxml.jackson.databind.node.ObjectNode) node).remove("ROAD_ADDR"); + } + } + + ObjectMapper objectMapper = new ObjectMapper(); + Object prkListJson = objectMapper.treeToValue(prkArray, Object.class); + + return Map.of("data", Map.of("PRK_STTS", prkListJson)); + + } catch (Exception e) { + throw new RuntimeException("주차장 데이터 파싱 실패: " + e.getMessage(), e); + } + } +} diff --git a/place-service/build.gradle b/place-service/build.gradle index 6329d25..e094fb2 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -30,6 +30,7 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + implementation 'org.postgresql:postgresql:42.7.2' implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 9071dcb..4d28859 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -1 +1,6 @@ -spring.application.name=place-service +spring.datasource.url=jdbc:postgresql://localhost:5432/star +spring.datasource.username=admin +spring.datasource.password=admin +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update \ No newline at end of file From 8aa25d1facf443b42971d2eb399e3c238fc565b8 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Thu, 17 Apr 2025 11:05:46 +0900 Subject: [PATCH 013/361] =?UTF-8?q?Feat:API=EC=B6=94=EA=B0=80--=EB=82=A0?= =?UTF-8?q?=EC=94=A8=20=ED=98=84=ED=99=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WeatherController.java | 28 +++ .../externalinfoservice/dto/WeatherDto.java | 46 +++++ .../service/WeatherService.java | 160 ++++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java new file mode 100644 index 0000000..5b43eb9 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java @@ -0,0 +1,28 @@ +package com.example.externalinfoservice.controller; + +import com.example.externalinfoservice.service.WeatherService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/weather") +@RequiredArgsConstructor +public class WeatherController { + private final WeatherService weatherService; + + @GetMapping("/{place-code}") + public String getWeatherData(@PathVariable("place-code") String placeCode) { + return weatherService.getWeatherData(placeCode); + } + + @GetMapping(value = "/api/{place-code}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getWeatherDataApi(@PathVariable("place-code") String placeCode) { + String jsonData = weatherService.getWeatherData(placeCode); + return ResponseEntity.ok(jsonData); + } +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java new file mode 100644 index 0000000..3570ae6 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java @@ -0,0 +1,46 @@ +package com.example.externalinfoservice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import lombok.Data; + +@Data +@XmlRootElement(name = "SeoulRtd.citydata") +@XmlAccessorType(XmlAccessType.FIELD) +public class WeatherDto { + @XmlElement(name = "WEATHER_STTS") + @JsonProperty("weather_status") + private WeatherStatus weatherStatus; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class WeatherStatus { + @XmlElement(name = "TEMP") + @JsonProperty("temperature") + private Double temperature; + + + @XmlElement(name = "PRECIPITATION") + @JsonProperty("precipitation") + private Double precipitation; + + @XmlElement(name = "PRECPT_TYPE") + @JsonProperty("precipitation_type") + private String precipitationType; + + @XmlElement(name = "PM10") + @JsonProperty("pm10") + private Integer pm10; + + @XmlElement(name = "FCST24HOURS") + @JsonProperty("forecast24hours") + private String forecast24hours; + + @XmlElement(name = "SKY_STTS") + @JsonProperty("sky_status") + private String skyStatus; + } +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java new file mode 100644 index 0000000..7686729 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java @@ -0,0 +1,160 @@ +package com.example.externalinfoservice.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.ByteArrayInputStream; +import java.util.Set; +import java.util.HashSet; + +@Service +public class WeatherService { + + private final String weatherApiUrl; + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + + // 날씨 관련 필드 목록 + private static final Set WEATHER_FIELDS = new HashSet<>() {{ + add("TEMP"); // 기온 + add("PRECIPITATION"); // 강수량 + add("PRECPT_TYPE"); // 강수 형태 + add("PM10"); // 미세먼지 + add("FCST24HOURS"); // 24시간 예보 + add("SKY_STTS"); // 하늘 상태 + }}; + + public WeatherService(RestTemplate restTemplate, ObjectMapper objectMapper) { + this.weatherApiUrl = "http://openapi.seoul.go.kr:8088/594b4a6559796f683930466466666d/xml/citydata/1/5/"; + this.restTemplate = restTemplate; + this.objectMapper = objectMapper; + } + + public String getWeatherData(String placeCode) { + // 1. 외부 OpenAPI에서 XML 데이터 받아오기 + String xmlResponse = restTemplate.getForObject(weatherApiUrl + placeCode, String.class); + + // 전체 XML 응답 출력 + System.out.println("XML Response: " + xmlResponse); + + try { + // XML 파싱을 위한 DocumentBuilder 생성 + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes())); + + // 모든 노드 이름 출력 + NodeList allNodes = document.getElementsByTagName("*"); + System.out.println("Total nodes: " + allNodes.getLength()); + for (int i = 0; i < allNodes.getLength(); i++) { + System.out.println("Node " + i + ": " + allNodes.item(i).getNodeName()); + } + + // JSON 객체 생성 + ObjectNode weatherJson = objectMapper.createObjectNode(); + ObjectNode weatherSttsJson = objectMapper.createObjectNode(); + + // 원하는 필드들 + String[] desiredFields = { + "TEMP", "PRECIPITATION", "PRECPT_TYPE", + "PM10", "FCST24HOURS", "SKY_STTS" + }; + + // 모든 WEATHER_STTS 하위 노드 탐색 + NodeList weatherSttsNodes = document.getElementsByTagName("WEATHER_STTS"); + System.out.println("WEATHER_STTS nodes count: " + weatherSttsNodes.getLength()); + + // 각 필드별로 특별 처리 + for (String field : desiredFields) { + NodeList fieldNodes = document.getElementsByTagName(field); + System.out.println(field + " nodes count: " + fieldNodes.getLength()); + + // 각 필드에 대해 값 추출 + if (fieldNodes.getLength() > 0) { + Node fieldNode = fieldNodes.item(0); + String nodeValue = fieldNode.getTextContent().trim(); + System.out.println(field + " value: " + nodeValue); + + if (field.equals("FCST24HOURS")) { + // FCST24HOURS는 배열로 처리 + ArrayNode forecastArray = objectMapper.createArrayNode(); + NodeList fcstChildren = fieldNode.getChildNodes(); + + for (int j = 0; j < fcstChildren.getLength(); j++) { + Node fcstChild = fcstChildren.item(j); + if (fcstChild.getNodeType() == Node.ELEMENT_NODE) { + Element fcstElement = (Element) fcstChild; + ObjectNode forecastItem = objectMapper.createObjectNode(); + NodeList itemChildren = fcstElement.getChildNodes(); + + for (int k = 0; k < itemChildren.getLength(); k++) { + Node itemChild = itemChildren.item(k); + if (itemChild.getNodeType() == Node.ELEMENT_NODE) { + Element itemElement = (Element) itemChild; + String itemNodeName = itemElement.getNodeName(); + String itemNodeValue = itemElement.getTextContent().trim(); + + if (!itemNodeValue.isEmpty()) { + forecastItem.put(convertToCamelCase(itemNodeName), itemNodeValue); + } + } + } + + if (!forecastItem.isEmpty()) { + forecastArray.add(forecastItem); + } + } + } + + weatherSttsJson.set(convertToCamelCase(field), forecastArray); + } else if (!nodeValue.isEmpty()) { + weatherSttsJson.put(convertToCamelCase(field), nodeValue); + } + } + } + + // weatherStts 필드에 추출된 정보 저장 + weatherJson.set("weatherStts", weatherSttsJson); + + // JSON을 예쁘게 출력 (들여쓰기 2칸) + String jsonString = objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(weatherJson); + // JSON 문자열에 줄바꿈 문자가 제대로 유지되는지 확인 + System.out.println("Formatted JSON: " + jsonString); + return jsonString; + + } catch (Exception e) { + e.printStackTrace(); // 전체 스택 트레이스 출력 + throw new RuntimeException("날씨 데이터 변환 실패", e); + } + } + + // 언더스코어 이름을 camelCase로 변환 + private String convertToCamelCase(String input) { + if (input == null || input.isEmpty()) return input; + + StringBuilder result = new StringBuilder(); + boolean capitalizeNext = false; + + for (char c : input.toLowerCase().toCharArray()) { + if (c == '_') { + capitalizeNext = true; + } else if (capitalizeNext) { + result.append(Character.toUpperCase(c)); + capitalizeNext = false; + } else { + result.append(c); + } + } + + return result.toString(); + } +} \ No newline at end of file From 50c3ea283599506383529c48c14fce3dab80c2e0 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Thu, 17 Apr 2025 11:05:46 +0900 Subject: [PATCH 014/361] =?UTF-8?q?Feat:API=EC=B6=94=EA=B0=80--=EB=82=A0?= =?UTF-8?q?=EC=94=A8=20=ED=98=84=ED=99=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WeatherController.java | 28 +++ .../externalinfoservice/dto/WeatherDto.java | 46 +++++ .../service/WeatherService.java | 160 ++++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java new file mode 100644 index 0000000..5b43eb9 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java @@ -0,0 +1,28 @@ +package com.example.externalinfoservice.controller; + +import com.example.externalinfoservice.service.WeatherService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/weather") +@RequiredArgsConstructor +public class WeatherController { + private final WeatherService weatherService; + + @GetMapping("/{place-code}") + public String getWeatherData(@PathVariable("place-code") String placeCode) { + return weatherService.getWeatherData(placeCode); + } + + @GetMapping(value = "/api/{place-code}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getWeatherDataApi(@PathVariable("place-code") String placeCode) { + String jsonData = weatherService.getWeatherData(placeCode); + return ResponseEntity.ok(jsonData); + } +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java new file mode 100644 index 0000000..3570ae6 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java @@ -0,0 +1,46 @@ +package com.example.externalinfoservice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import lombok.Data; + +@Data +@XmlRootElement(name = "SeoulRtd.citydata") +@XmlAccessorType(XmlAccessType.FIELD) +public class WeatherDto { + @XmlElement(name = "WEATHER_STTS") + @JsonProperty("weather_status") + private WeatherStatus weatherStatus; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class WeatherStatus { + @XmlElement(name = "TEMP") + @JsonProperty("temperature") + private Double temperature; + + + @XmlElement(name = "PRECIPITATION") + @JsonProperty("precipitation") + private Double precipitation; + + @XmlElement(name = "PRECPT_TYPE") + @JsonProperty("precipitation_type") + private String precipitationType; + + @XmlElement(name = "PM10") + @JsonProperty("pm10") + private Integer pm10; + + @XmlElement(name = "FCST24HOURS") + @JsonProperty("forecast24hours") + private String forecast24hours; + + @XmlElement(name = "SKY_STTS") + @JsonProperty("sky_status") + private String skyStatus; + } +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java new file mode 100644 index 0000000..7686729 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java @@ -0,0 +1,160 @@ +package com.example.externalinfoservice.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.ByteArrayInputStream; +import java.util.Set; +import java.util.HashSet; + +@Service +public class WeatherService { + + private final String weatherApiUrl; + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + + // 날씨 관련 필드 목록 + private static final Set WEATHER_FIELDS = new HashSet<>() {{ + add("TEMP"); // 기온 + add("PRECIPITATION"); // 강수량 + add("PRECPT_TYPE"); // 강수 형태 + add("PM10"); // 미세먼지 + add("FCST24HOURS"); // 24시간 예보 + add("SKY_STTS"); // 하늘 상태 + }}; + + public WeatherService(RestTemplate restTemplate, ObjectMapper objectMapper) { + this.weatherApiUrl = "http://openapi.seoul.go.kr:8088/594b4a6559796f683930466466666d/xml/citydata/1/5/"; + this.restTemplate = restTemplate; + this.objectMapper = objectMapper; + } + + public String getWeatherData(String placeCode) { + // 1. 외부 OpenAPI에서 XML 데이터 받아오기 + String xmlResponse = restTemplate.getForObject(weatherApiUrl + placeCode, String.class); + + // 전체 XML 응답 출력 + System.out.println("XML Response: " + xmlResponse); + + try { + // XML 파싱을 위한 DocumentBuilder 생성 + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes())); + + // 모든 노드 이름 출력 + NodeList allNodes = document.getElementsByTagName("*"); + System.out.println("Total nodes: " + allNodes.getLength()); + for (int i = 0; i < allNodes.getLength(); i++) { + System.out.println("Node " + i + ": " + allNodes.item(i).getNodeName()); + } + + // JSON 객체 생성 + ObjectNode weatherJson = objectMapper.createObjectNode(); + ObjectNode weatherSttsJson = objectMapper.createObjectNode(); + + // 원하는 필드들 + String[] desiredFields = { + "TEMP", "PRECIPITATION", "PRECPT_TYPE", + "PM10", "FCST24HOURS", "SKY_STTS" + }; + + // 모든 WEATHER_STTS 하위 노드 탐색 + NodeList weatherSttsNodes = document.getElementsByTagName("WEATHER_STTS"); + System.out.println("WEATHER_STTS nodes count: " + weatherSttsNodes.getLength()); + + // 각 필드별로 특별 처리 + for (String field : desiredFields) { + NodeList fieldNodes = document.getElementsByTagName(field); + System.out.println(field + " nodes count: " + fieldNodes.getLength()); + + // 각 필드에 대해 값 추출 + if (fieldNodes.getLength() > 0) { + Node fieldNode = fieldNodes.item(0); + String nodeValue = fieldNode.getTextContent().trim(); + System.out.println(field + " value: " + nodeValue); + + if (field.equals("FCST24HOURS")) { + // FCST24HOURS는 배열로 처리 + ArrayNode forecastArray = objectMapper.createArrayNode(); + NodeList fcstChildren = fieldNode.getChildNodes(); + + for (int j = 0; j < fcstChildren.getLength(); j++) { + Node fcstChild = fcstChildren.item(j); + if (fcstChild.getNodeType() == Node.ELEMENT_NODE) { + Element fcstElement = (Element) fcstChild; + ObjectNode forecastItem = objectMapper.createObjectNode(); + NodeList itemChildren = fcstElement.getChildNodes(); + + for (int k = 0; k < itemChildren.getLength(); k++) { + Node itemChild = itemChildren.item(k); + if (itemChild.getNodeType() == Node.ELEMENT_NODE) { + Element itemElement = (Element) itemChild; + String itemNodeName = itemElement.getNodeName(); + String itemNodeValue = itemElement.getTextContent().trim(); + + if (!itemNodeValue.isEmpty()) { + forecastItem.put(convertToCamelCase(itemNodeName), itemNodeValue); + } + } + } + + if (!forecastItem.isEmpty()) { + forecastArray.add(forecastItem); + } + } + } + + weatherSttsJson.set(convertToCamelCase(field), forecastArray); + } else if (!nodeValue.isEmpty()) { + weatherSttsJson.put(convertToCamelCase(field), nodeValue); + } + } + } + + // weatherStts 필드에 추출된 정보 저장 + weatherJson.set("weatherStts", weatherSttsJson); + + // JSON을 예쁘게 출력 (들여쓰기 2칸) + String jsonString = objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(weatherJson); + // JSON 문자열에 줄바꿈 문자가 제대로 유지되는지 확인 + System.out.println("Formatted JSON: " + jsonString); + return jsonString; + + } catch (Exception e) { + e.printStackTrace(); // 전체 스택 트레이스 출력 + throw new RuntimeException("날씨 데이터 변환 실패", e); + } + } + + // 언더스코어 이름을 camelCase로 변환 + private String convertToCamelCase(String input) { + if (input == null || input.isEmpty()) return input; + + StringBuilder result = new StringBuilder(); + boolean capitalizeNext = false; + + for (char c : input.toLowerCase().toCharArray()) { + if (c == '_') { + capitalizeNext = true; + } else if (capitalizeNext) { + result.append(Character.toUpperCase(c)); + capitalizeNext = false; + } else { + result.append(c); + } + } + + return result.toString(); + } +} \ No newline at end of file From 7853dd2bdcf8066c49835ee4f71fd8957cb51887 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Thu, 17 Apr 2025 13:10:41 +0900 Subject: [PATCH 015/361] =?UTF-8?q?=EB=82=A0=EC=94=A8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ParkingController.java | 4 +- .../externalinfoservice/dto/ParkDto.java | 35 +++++++++------ .../service/ParkService.java | 44 +++++++------------ .../src/main/resources/application.properties | 9 ++-- 4 files changed, 41 insertions(+), 51 deletions(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java index b74d48e..1ec6d57 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java @@ -5,8 +5,6 @@ import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; -import java.util.Map; - @RestController @RequestMapping("/parking") @RequiredArgsConstructor @@ -15,7 +13,7 @@ public class ParkingController { private final ParkService parkService; @GetMapping(value = "/{areaId}", produces = MediaType.APPLICATION_JSON_VALUE) - public Map getParking(@PathVariable String areaId) { + public String getParking(@PathVariable String areaId) { return parkService.getParkingData(areaId); } } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java index 0a28d3f..1deb47b 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java @@ -1,5 +1,6 @@ package com.example.externalinfoservice.dto; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.xml.bind.annotation.*; import lombok.Data; @@ -8,6 +9,7 @@ @Data @XmlRootElement(name = "SeoulRtd.citydata") @XmlAccessorType(XmlAccessType.FIELD) +@JsonIgnoreProperties(ignoreUnknown = true) public class ParkDto { @XmlElement(name = "CITYDATA") @@ -17,6 +19,14 @@ public class ParkDto { @XmlAccessorType(XmlAccessType.FIELD) public static class CityData { + @XmlElement(name = "PRK_STTS") + private PrkSttsWrapper prktts; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class PrkSttsWrapper { + @XmlElement(name = "PRK_STTS") private List prkStts; } @@ -26,40 +36,39 @@ public static class CityData { public static class ParkingStatus { @XmlElement(name = "PRK_NM") - private String PRK_NM; + private String prk_nm; @XmlElement(name = "PRK_CD") - private String PRK_CD; + private String prk_cd; @XmlElement(name = "PRK_TYPE") - private String PRK_TYPE; + private String prk_type; @XmlElement(name = "CPCTY") - private int CPCTY; + private int cpcty; @XmlElement(name = "CUR_PRK_CNT") - private Integer CUR_PRK_CNT; + private String cur_prk_cnt; @XmlElement(name = "CUR_PRK_TIME") - private String CUR_PRK_TIME; + private String cur_prk_time; @XmlElement(name = "CUR_PRK_YN") - private String CUR_PRK_YN; + private String cur_prk_yn; @XmlElement(name = "PAY_YN") - private String PAY_YN; + private String pay_yn; @XmlElement(name = "RATES") - private int RATES; + private int rates; @XmlElement(name = "TIME_RATES") - private int TIME_RATES; + private int time_rates; @XmlElement(name = "ADD_RATES") - private int ADD_RATES; + private int add_rates; @XmlElement(name = "ADD_TIME_RATES") - private int ADD_TIME_RATES; - + private int add_time_rates; } } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java index 1e44fa5..8507da0 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java @@ -1,14 +1,14 @@ package com.example.externalinfoservice.service; import com.example.externalinfoservice.dto.ParkDto; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.Unmarshaller; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; -import java.util.Map; +import java.io.StringReader; @Service public class ParkService { @@ -22,45 +22,31 @@ public ParkService(RestTemplate restTemplate) { this.restTemplate = restTemplate; } - public Map getParkingData(String areaId) { + public String getParkingData(String areaId) { try { String url = String.format( "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", apiKey, areaId ); + // XML 요청 String xmlResponse = restTemplate.getForObject(url, String.class); - XmlMapper xmlMapper = new XmlMapper(); - JsonNode root = xmlMapper.readTree(xmlResponse); - - // 실제 PRK_STTS 배열에 접근 - JsonNode prkArray = root - .path("CITYDATA") - .path("PRK_STTS") - .path("PRK_STTS"); - - if (!prkArray.isArray()) { - throw new RuntimeException("PRK_STTS는 배열이 아닙니다."); - } - - // 제거할 필드들: LNG, LAT, ADDRESS, ROAD_ADDR - for (JsonNode node : prkArray) { - if (node.isObject()) { - ((com.fasterxml.jackson.databind.node.ObjectNode) node).remove("LNG"); - ((com.fasterxml.jackson.databind.node.ObjectNode) node).remove("LAT"); - ((com.fasterxml.jackson.databind.node.ObjectNode) node).remove("ADDRESS"); - ((com.fasterxml.jackson.databind.node.ObjectNode) node).remove("ROAD_ADDR"); - } - } + // XML -> DTO + ParkDto parkDto = xmlToDto(xmlResponse); + // DTO -> JSON ObjectMapper objectMapper = new ObjectMapper(); - Object prkListJson = objectMapper.treeToValue(prkArray, Object.class); - - return Map.of("data", Map.of("PRK_STTS", prkListJson)); + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(parkDto); } catch (Exception e) { throw new RuntimeException("주차장 데이터 파싱 실패: " + e.getMessage(), e); } } + + private ParkDto xmlToDto(String xml) throws Exception { + JAXBContext context = JAXBContext.newInstance(ParkDto.class); + Unmarshaller unmarshaller = context.createUnmarshaller(); + return (ParkDto) unmarshaller.unmarshal(new StringReader(xml)); + } } diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 1cba9b3..0c9366c 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -1,19 +1,16 @@ -<<<<<<< HEAD -======= spring.application.name=place-service # postgreSQL ?? ->>>>>>> fbe5868b2ab98cf2b9a2818f38d0419f23f72e50 + spring.datasource.url=jdbc:postgresql://localhost:5432/star spring.datasource.username=admin spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver -<<<<<<< HEAD + spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update -======= + spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update ->>>>>>> fbe5868b2ab98cf2b9a2818f38d0419f23f72e50 From 3f13bfd582ff55e870a7ed05b894f73ff50830cb Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Thu, 17 Apr 2025 13:10:41 +0900 Subject: [PATCH 016/361] =?UTF-8?q?=EB=82=A0=EC=94=A8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ParkingController.java | 4 +- .../externalinfoservice/dto/ParkDto.java | 35 +++++++++------ .../service/ParkService.java | 44 +++++++------------ .../src/main/resources/application.properties | 9 ++-- 4 files changed, 41 insertions(+), 51 deletions(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java index b74d48e..1ec6d57 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java @@ -5,8 +5,6 @@ import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; -import java.util.Map; - @RestController @RequestMapping("/parking") @RequiredArgsConstructor @@ -15,7 +13,7 @@ public class ParkingController { private final ParkService parkService; @GetMapping(value = "/{areaId}", produces = MediaType.APPLICATION_JSON_VALUE) - public Map getParking(@PathVariable String areaId) { + public String getParking(@PathVariable String areaId) { return parkService.getParkingData(areaId); } } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java index 0a28d3f..1deb47b 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java @@ -1,5 +1,6 @@ package com.example.externalinfoservice.dto; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.xml.bind.annotation.*; import lombok.Data; @@ -8,6 +9,7 @@ @Data @XmlRootElement(name = "SeoulRtd.citydata") @XmlAccessorType(XmlAccessType.FIELD) +@JsonIgnoreProperties(ignoreUnknown = true) public class ParkDto { @XmlElement(name = "CITYDATA") @@ -17,6 +19,14 @@ public class ParkDto { @XmlAccessorType(XmlAccessType.FIELD) public static class CityData { + @XmlElement(name = "PRK_STTS") + private PrkSttsWrapper prktts; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class PrkSttsWrapper { + @XmlElement(name = "PRK_STTS") private List prkStts; } @@ -26,40 +36,39 @@ public static class CityData { public static class ParkingStatus { @XmlElement(name = "PRK_NM") - private String PRK_NM; + private String prk_nm; @XmlElement(name = "PRK_CD") - private String PRK_CD; + private String prk_cd; @XmlElement(name = "PRK_TYPE") - private String PRK_TYPE; + private String prk_type; @XmlElement(name = "CPCTY") - private int CPCTY; + private int cpcty; @XmlElement(name = "CUR_PRK_CNT") - private Integer CUR_PRK_CNT; + private String cur_prk_cnt; @XmlElement(name = "CUR_PRK_TIME") - private String CUR_PRK_TIME; + private String cur_prk_time; @XmlElement(name = "CUR_PRK_YN") - private String CUR_PRK_YN; + private String cur_prk_yn; @XmlElement(name = "PAY_YN") - private String PAY_YN; + private String pay_yn; @XmlElement(name = "RATES") - private int RATES; + private int rates; @XmlElement(name = "TIME_RATES") - private int TIME_RATES; + private int time_rates; @XmlElement(name = "ADD_RATES") - private int ADD_RATES; + private int add_rates; @XmlElement(name = "ADD_TIME_RATES") - private int ADD_TIME_RATES; - + private int add_time_rates; } } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java index 1e44fa5..8507da0 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java @@ -1,14 +1,14 @@ package com.example.externalinfoservice.service; import com.example.externalinfoservice.dto.ParkDto; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.Unmarshaller; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; -import java.util.Map; +import java.io.StringReader; @Service public class ParkService { @@ -22,45 +22,31 @@ public ParkService(RestTemplate restTemplate) { this.restTemplate = restTemplate; } - public Map getParkingData(String areaId) { + public String getParkingData(String areaId) { try { String url = String.format( "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", apiKey, areaId ); + // XML 요청 String xmlResponse = restTemplate.getForObject(url, String.class); - XmlMapper xmlMapper = new XmlMapper(); - JsonNode root = xmlMapper.readTree(xmlResponse); - - // 실제 PRK_STTS 배열에 접근 - JsonNode prkArray = root - .path("CITYDATA") - .path("PRK_STTS") - .path("PRK_STTS"); - - if (!prkArray.isArray()) { - throw new RuntimeException("PRK_STTS는 배열이 아닙니다."); - } - - // 제거할 필드들: LNG, LAT, ADDRESS, ROAD_ADDR - for (JsonNode node : prkArray) { - if (node.isObject()) { - ((com.fasterxml.jackson.databind.node.ObjectNode) node).remove("LNG"); - ((com.fasterxml.jackson.databind.node.ObjectNode) node).remove("LAT"); - ((com.fasterxml.jackson.databind.node.ObjectNode) node).remove("ADDRESS"); - ((com.fasterxml.jackson.databind.node.ObjectNode) node).remove("ROAD_ADDR"); - } - } + // XML -> DTO + ParkDto parkDto = xmlToDto(xmlResponse); + // DTO -> JSON ObjectMapper objectMapper = new ObjectMapper(); - Object prkListJson = objectMapper.treeToValue(prkArray, Object.class); - - return Map.of("data", Map.of("PRK_STTS", prkListJson)); + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(parkDto); } catch (Exception e) { throw new RuntimeException("주차장 데이터 파싱 실패: " + e.getMessage(), e); } } + + private ParkDto xmlToDto(String xml) throws Exception { + JAXBContext context = JAXBContext.newInstance(ParkDto.class); + Unmarshaller unmarshaller = context.createUnmarshaller(); + return (ParkDto) unmarshaller.unmarshal(new StringReader(xml)); + } } diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 1cba9b3..0c9366c 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -1,19 +1,16 @@ -<<<<<<< HEAD -======= spring.application.name=place-service # postgreSQL ?? ->>>>>>> fbe5868b2ab98cf2b9a2818f38d0419f23f72e50 + spring.datasource.url=jdbc:postgresql://localhost:5432/star spring.datasource.username=admin spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver -<<<<<<< HEAD + spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update -======= + spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update ->>>>>>> fbe5868b2ab98cf2b9a2818f38d0419f23f72e50 From a1b6953cc859bd72767797b9ef379c137f6401f3 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Thu, 17 Apr 2025 13:26:27 +0900 Subject: [PATCH 017/361] =?UTF-8?q?area-db=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=20=EC=82=BD=EC=9E=85=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- place-service/Dockerfile | 11 ++ place-service/build.gradle | 2 +- place-service/docker-compose.yml | 26 ++++ .../com/example/placeservice/entity/Area.java | 6 + .../src/main/resources/application.properties | 9 +- place-service/src/main/resources/data.sql | 117 ++++++++++++++++++ 6 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 place-service/Dockerfile create mode 100644 place-service/docker-compose.yml create mode 100644 place-service/src/main/resources/data.sql diff --git a/place-service/Dockerfile b/place-service/Dockerfile new file mode 100644 index 0000000..2e9e322 --- /dev/null +++ b/place-service/Dockerfile @@ -0,0 +1,11 @@ +# Gradle +FROM gradle:8.13.0-jdk17 AS build +WORKDIR /app +COPY . . +RUN gradle build -x test + +FROM eclipse-temurin:17-jdk +WORKDIR /app +COPY --from=build /app/build/libs/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/place-service/build.gradle b/place-service/build.gradle index 6329d25..201fa02 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -55,4 +55,4 @@ tasks.named('bootBuildImage') { tasks.named('test') { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/place-service/docker-compose.yml b/place-service/docker-compose.yml new file mode 100644 index 0000000..31e1bab --- /dev/null +++ b/place-service/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3' +services: + db: + image: postgres:16.8 + container_name: my_postgres + environment: + POSTGRES_DB: stars_db + POSTGRES_USER: root + POSTGRES_PASSWORD: admin + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + app: + build: . + container_name: my_spring_app + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/stars_db + SPRING_DATASOURCE_USERNAME: root + SPRING_DATASOURCE_PASSWORD: admin + ports: + - "8080:8080" + depends_on: + - db +volumes: + pgdata: diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index faa2087..6c14791 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -11,9 +11,15 @@ public class Area { @Column(name = "area_id") private Long area_id; + @Column(name = "seoul_id", length = 10, nullable = false) + private String seoul_id; + @Column(name = "name", length = 50, nullable = false) private String name; // 지역명 + @Column(name = "name_eng", length = 100, nullable = false) + private String name_eng; + @Column(name = "category", length = 20, nullable = false) private String category; // 구분 diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index d5cbebf..5734ed4 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -2,10 +2,13 @@ spring.application.name=place-service # postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/star -spring.datasource.username=admin +spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db +spring.datasource.username=root spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update +spring.jpa.defer-datasource-initialization=true +spring.jpa.hibernate.ddl-auto=create +spring.jpa.show-sql=true +spring.sql.init.mode=always diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql new file mode 100644 index 0000000..5ad3979 --- /dev/null +++ b/place-service/src/main/resources/data.sql @@ -0,0 +1,117 @@ +INSERT INTO area (seoul_id, name, name_eng, category, x, y) VALUES +('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), +('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), +('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), +('POI004', '이태원 관광특구', 'Itaewon Special Tourist Zone', '관광특구', 37.53475718815058, 126.99591956660342), +('POI005', '잠실 관광특구', 'Jamsil Special Tourist Zone', '관광특구', 37.51868923685722, 127.11694379878114), +('POI006', '종로·청계 관광특구', 'Jongno Cheonggye Special Toruist Zone', '관광특구', 37.56984229024498, 126.99866678943675), +('POI007', '홍대 관광특구', 'HongDae Culture & Arts Special Tourist Zone', '관광특구', 37.55506170187393, 126.92159115672915), +('POI008', '경복궁', 'Gyeongbokgung Palace', '고궁·문화유산', 37.5809150911806, 126.97764726625209), +('POI009', '광화문·덕수궁', 'Gwanghwamun & Deoksugung Palace', '고궁·문화유산', 37.56847081447724, 126.97620594802834), +('POI010', '보신각', 'Bosingak', '고궁·문화유산', 37.57089797616037, 126.98337607511172), +('POI011', '서울 암사동 유적', 'Amsa Prehistoric Settlement Site', '고궁·문화유산', 37.56102809827398, 127.13078491934499), +('POI012', '창덕궁·종묘', 'Changdeokgung Palace & Jongmyo Shrine', '고궁·문화유산', 37.5795304952381, 126.99416802285715), +('POI013', '가산디지털단지역', 'Gasan Digital Complex station', '인구밀집지역', 37.48267268376068, 126.88014526210826), +('POI014', '강남역', 'Gangnam station', '인구밀집지역', 37.50019794543511, 127.02822145618981), +('POI015', '건대입구역', 'Konkuk University station', '인구밀집지역', 37.540005312302846, 127.06718185804415), +('POI016', '고덕역', 'Godeok station', '인구밀집지역', 37.553377141111774, 127.15503341723486), +('POI017', '고속터미널역', 'Express Bus Terminal station', '인구밀집지역', 37.50472316731823, 127.00542276570697), +('POI018', '교대역', 'Seoul National University of Education station', '인구밀집지역', 37.49185444557083, 127.01456630812736), +('POI019', '구로디지털단지역', 'Guro Digital Complex station', '인구밀집지역', 37.48349491938897, 126.89557915984915), +('POI020', '구로역', 'Guro station', '인구밀집지역', 37.501517014484925, 126.88285531403136), +('POI021', '군자역', 'Gunja station', '인구밀집지역', 37.555916439038526, 127.08008933356267), +('POI022', '남구로역', 'Namguro station', '인구밀집지역', 37.484184300878496, 126.88539683418863), +('POI023', '대림역', 'Daerim station', '인구밀집지역', 37.492818237753816, 126.89567307890546), +('POI024', '동대문역', 'Dongdaemun station', '인구밀집지역', 37.57127393968462, 127.0096854515538), +('POI025', '뚝섬역', 'Ttukseom station', '인구밀집지역', 37.548249670448335, 127.04603079475764), +('POI026', '미아사거리역', 'Miasageori station', '인구밀집지역', 37.61244898963818, 127.0305835226811), +('POI027', '발산역', 'Balsan station', '인구밀집지역', 37.55796811527854, 126.84179723439551), +('POI028', '북한산우이역', 'Bukhansan Ui station', '인구밀집지역', 37.66281154490492, 127.01070580250341), +('POI029', '사당역', 'Sadang station', '인구밀집지역', 37.47635049189678, 126.98129519152415), +('POI030', '삼각지역', 'Samgakji station', '인구밀집지역', 37.535323919468816, 126.97392611189508), +('POI031', '서울대입구역', 'Seoul National University station', '인구밀집지역', 37.48060991955129, 126.95360407634334), +('POI032', '서울식물원·마곡나루역', 'Seoul Botanic Park·Magongnaru station', '인구밀집지역', 37.56745047572558, 126.82986126533689), +('POI033', '서울역', 'Seoul station', '인구밀집지역', 37.55662336923077, 126.97216878769233), +('POI034', '선릉역', 'Seolleung station', '인구밀집지역', 37.50584166753116, 127.0489100692203), +('POI035', '성신여대입구역', 'Sungshin Women''s University station', '인구밀집지역', 37.59262801654558, 127.01658994286346), +('POI036', '수유역', 'Suyu station', '인구밀집지역', 37.641215514705884, 127.02565393627451), +('POI037', '신논현역·논현역', 'Sinnonhyeon·Nonhyeon station', '인구밀집지역', 37.50848054554439, 127.02291493850254), +('POI038', '신도림역', 'Sindorim station', '인구밀집지역', 37.50941493666961, 126.89096014067083), +('POI039', '신림역', 'Sillim station', '인구밀집지역', 37.48396968904173, 126.92987028995233), +('POI040', '신촌·이대역', 'Sinchon·Ewha Womans University station', '인구밀집지역', 37.55564146783625, 126.93761215497075), +('POI041', '양재역', 'Yangjae station', '인구밀집지역', 37.48451422353994, 127.03551115644439), +('POI042', '역삼역', 'Yeoksam station', '인구밀집지역', 37.50011177778084, 127.03721339389585), +('POI043', '연신내역', 'Yeonsinnae station', '인구밀집지역', 37.61903547457627, 126.92180184322034), +('POI044', '오목교역·목동운동장', 'Omokgyo·Mok-dong Stadium station', '인구밀집지역', 37.53174788866626, 126.87924768246015), +('POI045', '왕십리역', 'Wangsimni station', '인구밀집지역', 37.561636745463275, 127.03857508984515), +('POI046', '용산역', 'Yongsan station', '인구밀집지역', 37.53059674809017, 126.95953798376128), +('POI047', '이태원역', 'Itaewon station', '인구밀집지역', 37.534847968613946, 126.99261390234051), +('POI048', '장지역', 'Jangji station', '인구밀집지역', 37.47861018962738, 127.12424263037641), +('POI049', '장한평역', 'Janghanpyeong station', '인구밀집지역', 37.562849061361526, 127.06235115915185), +('POI050', '천호역', 'Cheonho station', '인구밀집지역', 37.53921065146112, 127.12470374198884), +('POI051', '총신대입구(이수)역', 'Chongshin University(Isu) station', '인구밀집지역', 37.48677821828824, 126.9807539648491), +('POI052', '충정로역', 'Chungjeongno station', '인구밀집지역', 37.559524839900476, 126.9642059673645), +('POI053', '합정역', 'Hapjeong station', '인구밀집지역', 37.54857307399022, 126.91084543497917), +('POI054', '혜화역', 'Hyehwa station', '인구밀집지역', 37.58237426286851, 127.0014404389915), +('POI055', '홍대입구역(2호선)', 'Hongik University station(Line 2)', '인구밀집지역', 37.5560688250626, 126.92329766229786), +('POI056', '회기역', 'Hoegi station', '인구밀집지역', 37.590538258503685, 127.05700039395965), +('POI057', '4·19 카페거리', '4·19 Cafe street', '발달상권', 37.64481185893745, 127.00739701908337), +('POI058', '가락시장', 'Garak Market', '발달상권', 37.49445476721721, 127.11054976163454), +('POI059', '가로수길', 'Garosu-gil', '발달상권', 37.522662622516556, 127.02543305960263), +('POI060', '광장(전통)시장', 'Gwangjang(Traditional) Market', '발달상권', 37.569817036518465, 127.00014893169192), +('POI061', '김포공항', 'Gimpo Airport', '발달상권', 37.56228914604604, 126.80289965159135), +('POI062', '낙산공원·이화마을', 'Naksan Park·Ihwa Village', '발달상권', 37.580295002146926, 127.00692469978675), +('POI063', '노량진', 'Noryangjin', '발달상권', 37.513690699957316, 126.94907070192423), +('POI064', '덕수궁길·정동길', 'Deoksugung-gil·Jeongdong-gil', '발달상권', 37.5659744042236, 126.97244482634493), +('POI065', '방배역 먹자골목', 'Bangbae food alley', '발달상권', 37.48234102283986, 126.99597969571558), +('POI066', '북촌한옥마을', 'Bukchon Hanok Village', '발달상권', 37.58425649416171, 126.98386049085175), +('POI067', '서촌', 'Seochon', '발달상권', 37.58018833392947, 126.9666234325548), +('POI068', '성수카페거리', 'Seongsu Cafe Street', '발달상권', 37.54227657813654, 127.05560237183015), +('POI069', '수유리 먹자골목', 'Suyuri food alley', '발달상권', 37.64270823057644, 127.02733855833522), +('POI070', '쌍문동 맛집거리', 'Ssangmun-dong restaurant street', '발달상권', 37.6477285443038, 127.03242528481013), +('POI071', '압구정로데오거리', 'Apgujeong Rodeo Street', '발달상권', 37.52560441860466, 127.04040171511626), +('POI072', '여의도', 'Yeouido', '발달상권', 37.52549260943663, 126.92338937129487), +('POI073', '연남동', 'Yeonnam-dong', '발달상권', 37.561426426204264, 126.92406273242905), +('POI074', '영등포 타임스퀘어', 'Yeongdeungpo Time square', '발달상권', 37.51653179760669, 126.90523931614634), +('POI075', '외대앞', 'Hankuk University of Foreign Studies', '발달상권', 37.59442412843283, 127.06153833102397), +('POI076', '용리단길', 'Yongnidan-gil', '발달상권', 37.5311056439386, 126.97147359358237), +('POI077', '이태원 앤틱가구거리', 'Itaewon Antiques street', '발달상권', 37.53187270743738, 126.99343376513602), +('POI078', '인사동·익선동', 'Insa-dong·Ikseon-dong', '발달상권', 37.5741577489609, 126.98736374162822), +('POI079', '창동 신경제 중심지', 'Changdong New Economic Center', '발달상권', 37.656167961335676, 127.05328808787348), +('POI080', '청담동 명품거리', 'Cheongdam-dong Luxury Fashion street', '발달상권', 37.52645537152884, 127.04354058968364), +('POI081', '청량리 제기동 일대 전통시장', 'Traditional market in Cheongnyangni Jegi-dong', '발달상권', 37.579977738703214, 127.0382790126348), +('POI082', '해방촌·경리단길', 'Haebangchon·Gyeongnidan-gil', '발달상권', 37.54143829714048, 126.98836800969178), +('POI083', 'DDP(동대문디자인플라자)', 'DDP(Dongdaemun Design Plaza)', '발달상권', 37.56654201564661, 127.00993045294622), +('POI084', 'DMC(디지털미디어시티)', 'DMC(Digital Media City)', '발달상권', 37.577407110655734, 126.8936691147541), +('POI085', '강서한강공원', 'Gangseo Hangang Park', '공원', 37.5858708295553, 126.82002478578644), +('POI086', '고척돔', 'Gocheok Dome', '공원', 37.49776790791694, 126.86713336195083), +('POI087', '광나루한강공원', 'Gwangnaru Hangang Park', '공원', 37.550866089267345, 127.12370647922764), +('POI088', '광화문광장', 'Gwanghwamun Square', '공원', 37.57505062349029, 126.97680679071502), +('POI089', '국립중앙박물관·용산가족공원', 'The National Museum of Korea·Yongsan Family Park', '공원', 37.52202119313341, 126.9824679850178), +('POI090', '난지한강공원', 'Nanji Hangang Park', '공원', 37.566316740172454, 126.87770001597329), +('POI091', '남산공원', 'Namsan Park', '공원', 37.555070650369004, 126.99365012730628), +('POI092', '노들섬', 'Nodeul island', '공원', 37.517556872131706, 126.95871397855296), +('POI093', '뚝섬한강공원', 'Ttukseom Hangang Park', '공원', 37.52882503002481, 127.07460742502332), +('POI094', '망원한강공원', 'Mangwon Hangang Park', '공원', 37.55480087656761, 126.89823749216892), +('POI095', '반포한강공원', 'Banpo Hangang Park', '공원', 37.51065462735107, 126.99457550649858), +('POI096', '북서울꿈의숲', 'Dream Forest', '공원', 37.62244979881656, 127.04069568441815), +('POI097', '불광천', 'Bulgwangcheon River', '공원', 37.58940646530798, 126.91194122899687), +('POI098', '서리풀공원·몽마르뜨공원', 'Seoripul Park·Montmartre Park', '공원', 37.49022054658995, 127.00173655775917), +('POI100', '서울대공원', 'Seoul Grand Park', '공원', 37.42839607329216, 127.01863790236284), +('POI101', '서울숲공원', 'Seoul Forest', '공원', 37.54402680167043, 127.03673083133076), +('POI099', '서울광장', 'Seoul Plaza', '공원', 37.5655010296151, 126.97798702124584), +('POI102', '아차산', 'Achasan', '공원', 37.56307021845223, 127.10047918048643), +('POI103', '양화한강공원', 'Yanghwa Hangang Park', '공원', 37.54029653900909, 126.89963558394624), +('POI104', '어린이대공원', 'Children''s Grand Park', '공원', 37.54957100980681, 127.0824313180458), +('POI105', '여의도한강공원', 'Yeouido Hangang Park', '공원', 37.52868909088513, 126.92627587984478), +('POI106', '월드컵공원', 'World Cup Park', '공원', 37.5660391150387, 126.88740201899091), +('POI107', '응봉산', 'Eungbongsan', '공원', 37.54797346368485, 127.02904643771303), +('POI108', '이촌한강공원', 'Ichon Hangang Park', '공원', 37.52139035897837, 126.96356631928725), +('POI109', '잠실종합운동장', 'Jamsil (Seoul) Sports Complex', '공원', 37.51610665876664, 127.07230325047283), +('POI110', '잠실한강공원', 'Jamsil Hangang Park', '공원', 37.51981181577704, 127.08507449607517), +('POI111', '잠원한강공원', 'Jamwon Hangang Park', '공원', 37.52336245003122, 127.01397131593632), +('POI112', '청계산', 'Cheonggyesan', '공원', 37.443365424959026, 127.0469697170313), +('POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264), +('POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616), +('POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821), +('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286); \ No newline at end of file From 6b99c6481e373dfb9a310b634ada026f8cc0428b Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Thu, 17 Apr 2025 13:26:27 +0900 Subject: [PATCH 018/361] =?UTF-8?q?area-db=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=20=EC=82=BD=EC=9E=85=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- place-service/Dockerfile | 11 ++ place-service/build.gradle | 2 +- place-service/docker-compose.yml | 26 ++++ .../com/example/placeservice/entity/Area.java | 6 + .../src/main/resources/application.properties | 9 +- place-service/src/main/resources/data.sql | 117 ++++++++++++++++++ 6 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 place-service/Dockerfile create mode 100644 place-service/docker-compose.yml create mode 100644 place-service/src/main/resources/data.sql diff --git a/place-service/Dockerfile b/place-service/Dockerfile new file mode 100644 index 0000000..2e9e322 --- /dev/null +++ b/place-service/Dockerfile @@ -0,0 +1,11 @@ +# Gradle +FROM gradle:8.13.0-jdk17 AS build +WORKDIR /app +COPY . . +RUN gradle build -x test + +FROM eclipse-temurin:17-jdk +WORKDIR /app +COPY --from=build /app/build/libs/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/place-service/build.gradle b/place-service/build.gradle index 6329d25..201fa02 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -55,4 +55,4 @@ tasks.named('bootBuildImage') { tasks.named('test') { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/place-service/docker-compose.yml b/place-service/docker-compose.yml new file mode 100644 index 0000000..31e1bab --- /dev/null +++ b/place-service/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3' +services: + db: + image: postgres:16.8 + container_name: my_postgres + environment: + POSTGRES_DB: stars_db + POSTGRES_USER: root + POSTGRES_PASSWORD: admin + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + app: + build: . + container_name: my_spring_app + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/stars_db + SPRING_DATASOURCE_USERNAME: root + SPRING_DATASOURCE_PASSWORD: admin + ports: + - "8080:8080" + depends_on: + - db +volumes: + pgdata: diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index faa2087..6c14791 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -11,9 +11,15 @@ public class Area { @Column(name = "area_id") private Long area_id; + @Column(name = "seoul_id", length = 10, nullable = false) + private String seoul_id; + @Column(name = "name", length = 50, nullable = false) private String name; // 지역명 + @Column(name = "name_eng", length = 100, nullable = false) + private String name_eng; + @Column(name = "category", length = 20, nullable = false) private String category; // 구분 diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index d5cbebf..5734ed4 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -2,10 +2,13 @@ spring.application.name=place-service # postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/star -spring.datasource.username=admin +spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db +spring.datasource.username=root spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update +spring.jpa.defer-datasource-initialization=true +spring.jpa.hibernate.ddl-auto=create +spring.jpa.show-sql=true +spring.sql.init.mode=always diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql new file mode 100644 index 0000000..5ad3979 --- /dev/null +++ b/place-service/src/main/resources/data.sql @@ -0,0 +1,117 @@ +INSERT INTO area (seoul_id, name, name_eng, category, x, y) VALUES +('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), +('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), +('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), +('POI004', '이태원 관광특구', 'Itaewon Special Tourist Zone', '관광특구', 37.53475718815058, 126.99591956660342), +('POI005', '잠실 관광특구', 'Jamsil Special Tourist Zone', '관광특구', 37.51868923685722, 127.11694379878114), +('POI006', '종로·청계 관광특구', 'Jongno Cheonggye Special Toruist Zone', '관광특구', 37.56984229024498, 126.99866678943675), +('POI007', '홍대 관광특구', 'HongDae Culture & Arts Special Tourist Zone', '관광특구', 37.55506170187393, 126.92159115672915), +('POI008', '경복궁', 'Gyeongbokgung Palace', '고궁·문화유산', 37.5809150911806, 126.97764726625209), +('POI009', '광화문·덕수궁', 'Gwanghwamun & Deoksugung Palace', '고궁·문화유산', 37.56847081447724, 126.97620594802834), +('POI010', '보신각', 'Bosingak', '고궁·문화유산', 37.57089797616037, 126.98337607511172), +('POI011', '서울 암사동 유적', 'Amsa Prehistoric Settlement Site', '고궁·문화유산', 37.56102809827398, 127.13078491934499), +('POI012', '창덕궁·종묘', 'Changdeokgung Palace & Jongmyo Shrine', '고궁·문화유산', 37.5795304952381, 126.99416802285715), +('POI013', '가산디지털단지역', 'Gasan Digital Complex station', '인구밀집지역', 37.48267268376068, 126.88014526210826), +('POI014', '강남역', 'Gangnam station', '인구밀집지역', 37.50019794543511, 127.02822145618981), +('POI015', '건대입구역', 'Konkuk University station', '인구밀집지역', 37.540005312302846, 127.06718185804415), +('POI016', '고덕역', 'Godeok station', '인구밀집지역', 37.553377141111774, 127.15503341723486), +('POI017', '고속터미널역', 'Express Bus Terminal station', '인구밀집지역', 37.50472316731823, 127.00542276570697), +('POI018', '교대역', 'Seoul National University of Education station', '인구밀집지역', 37.49185444557083, 127.01456630812736), +('POI019', '구로디지털단지역', 'Guro Digital Complex station', '인구밀집지역', 37.48349491938897, 126.89557915984915), +('POI020', '구로역', 'Guro station', '인구밀집지역', 37.501517014484925, 126.88285531403136), +('POI021', '군자역', 'Gunja station', '인구밀집지역', 37.555916439038526, 127.08008933356267), +('POI022', '남구로역', 'Namguro station', '인구밀집지역', 37.484184300878496, 126.88539683418863), +('POI023', '대림역', 'Daerim station', '인구밀집지역', 37.492818237753816, 126.89567307890546), +('POI024', '동대문역', 'Dongdaemun station', '인구밀집지역', 37.57127393968462, 127.0096854515538), +('POI025', '뚝섬역', 'Ttukseom station', '인구밀집지역', 37.548249670448335, 127.04603079475764), +('POI026', '미아사거리역', 'Miasageori station', '인구밀집지역', 37.61244898963818, 127.0305835226811), +('POI027', '발산역', 'Balsan station', '인구밀집지역', 37.55796811527854, 126.84179723439551), +('POI028', '북한산우이역', 'Bukhansan Ui station', '인구밀집지역', 37.66281154490492, 127.01070580250341), +('POI029', '사당역', 'Sadang station', '인구밀집지역', 37.47635049189678, 126.98129519152415), +('POI030', '삼각지역', 'Samgakji station', '인구밀집지역', 37.535323919468816, 126.97392611189508), +('POI031', '서울대입구역', 'Seoul National University station', '인구밀집지역', 37.48060991955129, 126.95360407634334), +('POI032', '서울식물원·마곡나루역', 'Seoul Botanic Park·Magongnaru station', '인구밀집지역', 37.56745047572558, 126.82986126533689), +('POI033', '서울역', 'Seoul station', '인구밀집지역', 37.55662336923077, 126.97216878769233), +('POI034', '선릉역', 'Seolleung station', '인구밀집지역', 37.50584166753116, 127.0489100692203), +('POI035', '성신여대입구역', 'Sungshin Women''s University station', '인구밀집지역', 37.59262801654558, 127.01658994286346), +('POI036', '수유역', 'Suyu station', '인구밀집지역', 37.641215514705884, 127.02565393627451), +('POI037', '신논현역·논현역', 'Sinnonhyeon·Nonhyeon station', '인구밀집지역', 37.50848054554439, 127.02291493850254), +('POI038', '신도림역', 'Sindorim station', '인구밀집지역', 37.50941493666961, 126.89096014067083), +('POI039', '신림역', 'Sillim station', '인구밀집지역', 37.48396968904173, 126.92987028995233), +('POI040', '신촌·이대역', 'Sinchon·Ewha Womans University station', '인구밀집지역', 37.55564146783625, 126.93761215497075), +('POI041', '양재역', 'Yangjae station', '인구밀집지역', 37.48451422353994, 127.03551115644439), +('POI042', '역삼역', 'Yeoksam station', '인구밀집지역', 37.50011177778084, 127.03721339389585), +('POI043', '연신내역', 'Yeonsinnae station', '인구밀집지역', 37.61903547457627, 126.92180184322034), +('POI044', '오목교역·목동운동장', 'Omokgyo·Mok-dong Stadium station', '인구밀집지역', 37.53174788866626, 126.87924768246015), +('POI045', '왕십리역', 'Wangsimni station', '인구밀집지역', 37.561636745463275, 127.03857508984515), +('POI046', '용산역', 'Yongsan station', '인구밀집지역', 37.53059674809017, 126.95953798376128), +('POI047', '이태원역', 'Itaewon station', '인구밀집지역', 37.534847968613946, 126.99261390234051), +('POI048', '장지역', 'Jangji station', '인구밀집지역', 37.47861018962738, 127.12424263037641), +('POI049', '장한평역', 'Janghanpyeong station', '인구밀집지역', 37.562849061361526, 127.06235115915185), +('POI050', '천호역', 'Cheonho station', '인구밀집지역', 37.53921065146112, 127.12470374198884), +('POI051', '총신대입구(이수)역', 'Chongshin University(Isu) station', '인구밀집지역', 37.48677821828824, 126.9807539648491), +('POI052', '충정로역', 'Chungjeongno station', '인구밀집지역', 37.559524839900476, 126.9642059673645), +('POI053', '합정역', 'Hapjeong station', '인구밀집지역', 37.54857307399022, 126.91084543497917), +('POI054', '혜화역', 'Hyehwa station', '인구밀집지역', 37.58237426286851, 127.0014404389915), +('POI055', '홍대입구역(2호선)', 'Hongik University station(Line 2)', '인구밀집지역', 37.5560688250626, 126.92329766229786), +('POI056', '회기역', 'Hoegi station', '인구밀집지역', 37.590538258503685, 127.05700039395965), +('POI057', '4·19 카페거리', '4·19 Cafe street', '발달상권', 37.64481185893745, 127.00739701908337), +('POI058', '가락시장', 'Garak Market', '발달상권', 37.49445476721721, 127.11054976163454), +('POI059', '가로수길', 'Garosu-gil', '발달상권', 37.522662622516556, 127.02543305960263), +('POI060', '광장(전통)시장', 'Gwangjang(Traditional) Market', '발달상권', 37.569817036518465, 127.00014893169192), +('POI061', '김포공항', 'Gimpo Airport', '발달상권', 37.56228914604604, 126.80289965159135), +('POI062', '낙산공원·이화마을', 'Naksan Park·Ihwa Village', '발달상권', 37.580295002146926, 127.00692469978675), +('POI063', '노량진', 'Noryangjin', '발달상권', 37.513690699957316, 126.94907070192423), +('POI064', '덕수궁길·정동길', 'Deoksugung-gil·Jeongdong-gil', '발달상권', 37.5659744042236, 126.97244482634493), +('POI065', '방배역 먹자골목', 'Bangbae food alley', '발달상권', 37.48234102283986, 126.99597969571558), +('POI066', '북촌한옥마을', 'Bukchon Hanok Village', '발달상권', 37.58425649416171, 126.98386049085175), +('POI067', '서촌', 'Seochon', '발달상권', 37.58018833392947, 126.9666234325548), +('POI068', '성수카페거리', 'Seongsu Cafe Street', '발달상권', 37.54227657813654, 127.05560237183015), +('POI069', '수유리 먹자골목', 'Suyuri food alley', '발달상권', 37.64270823057644, 127.02733855833522), +('POI070', '쌍문동 맛집거리', 'Ssangmun-dong restaurant street', '발달상권', 37.6477285443038, 127.03242528481013), +('POI071', '압구정로데오거리', 'Apgujeong Rodeo Street', '발달상권', 37.52560441860466, 127.04040171511626), +('POI072', '여의도', 'Yeouido', '발달상권', 37.52549260943663, 126.92338937129487), +('POI073', '연남동', 'Yeonnam-dong', '발달상권', 37.561426426204264, 126.92406273242905), +('POI074', '영등포 타임스퀘어', 'Yeongdeungpo Time square', '발달상권', 37.51653179760669, 126.90523931614634), +('POI075', '외대앞', 'Hankuk University of Foreign Studies', '발달상권', 37.59442412843283, 127.06153833102397), +('POI076', '용리단길', 'Yongnidan-gil', '발달상권', 37.5311056439386, 126.97147359358237), +('POI077', '이태원 앤틱가구거리', 'Itaewon Antiques street', '발달상권', 37.53187270743738, 126.99343376513602), +('POI078', '인사동·익선동', 'Insa-dong·Ikseon-dong', '발달상권', 37.5741577489609, 126.98736374162822), +('POI079', '창동 신경제 중심지', 'Changdong New Economic Center', '발달상권', 37.656167961335676, 127.05328808787348), +('POI080', '청담동 명품거리', 'Cheongdam-dong Luxury Fashion street', '발달상권', 37.52645537152884, 127.04354058968364), +('POI081', '청량리 제기동 일대 전통시장', 'Traditional market in Cheongnyangni Jegi-dong', '발달상권', 37.579977738703214, 127.0382790126348), +('POI082', '해방촌·경리단길', 'Haebangchon·Gyeongnidan-gil', '발달상권', 37.54143829714048, 126.98836800969178), +('POI083', 'DDP(동대문디자인플라자)', 'DDP(Dongdaemun Design Plaza)', '발달상권', 37.56654201564661, 127.00993045294622), +('POI084', 'DMC(디지털미디어시티)', 'DMC(Digital Media City)', '발달상권', 37.577407110655734, 126.8936691147541), +('POI085', '강서한강공원', 'Gangseo Hangang Park', '공원', 37.5858708295553, 126.82002478578644), +('POI086', '고척돔', 'Gocheok Dome', '공원', 37.49776790791694, 126.86713336195083), +('POI087', '광나루한강공원', 'Gwangnaru Hangang Park', '공원', 37.550866089267345, 127.12370647922764), +('POI088', '광화문광장', 'Gwanghwamun Square', '공원', 37.57505062349029, 126.97680679071502), +('POI089', '국립중앙박물관·용산가족공원', 'The National Museum of Korea·Yongsan Family Park', '공원', 37.52202119313341, 126.9824679850178), +('POI090', '난지한강공원', 'Nanji Hangang Park', '공원', 37.566316740172454, 126.87770001597329), +('POI091', '남산공원', 'Namsan Park', '공원', 37.555070650369004, 126.99365012730628), +('POI092', '노들섬', 'Nodeul island', '공원', 37.517556872131706, 126.95871397855296), +('POI093', '뚝섬한강공원', 'Ttukseom Hangang Park', '공원', 37.52882503002481, 127.07460742502332), +('POI094', '망원한강공원', 'Mangwon Hangang Park', '공원', 37.55480087656761, 126.89823749216892), +('POI095', '반포한강공원', 'Banpo Hangang Park', '공원', 37.51065462735107, 126.99457550649858), +('POI096', '북서울꿈의숲', 'Dream Forest', '공원', 37.62244979881656, 127.04069568441815), +('POI097', '불광천', 'Bulgwangcheon River', '공원', 37.58940646530798, 126.91194122899687), +('POI098', '서리풀공원·몽마르뜨공원', 'Seoripul Park·Montmartre Park', '공원', 37.49022054658995, 127.00173655775917), +('POI100', '서울대공원', 'Seoul Grand Park', '공원', 37.42839607329216, 127.01863790236284), +('POI101', '서울숲공원', 'Seoul Forest', '공원', 37.54402680167043, 127.03673083133076), +('POI099', '서울광장', 'Seoul Plaza', '공원', 37.5655010296151, 126.97798702124584), +('POI102', '아차산', 'Achasan', '공원', 37.56307021845223, 127.10047918048643), +('POI103', '양화한강공원', 'Yanghwa Hangang Park', '공원', 37.54029653900909, 126.89963558394624), +('POI104', '어린이대공원', 'Children''s Grand Park', '공원', 37.54957100980681, 127.0824313180458), +('POI105', '여의도한강공원', 'Yeouido Hangang Park', '공원', 37.52868909088513, 126.92627587984478), +('POI106', '월드컵공원', 'World Cup Park', '공원', 37.5660391150387, 126.88740201899091), +('POI107', '응봉산', 'Eungbongsan', '공원', 37.54797346368485, 127.02904643771303), +('POI108', '이촌한강공원', 'Ichon Hangang Park', '공원', 37.52139035897837, 126.96356631928725), +('POI109', '잠실종합운동장', 'Jamsil (Seoul) Sports Complex', '공원', 37.51610665876664, 127.07230325047283), +('POI110', '잠실한강공원', 'Jamsil Hangang Park', '공원', 37.51981181577704, 127.08507449607517), +('POI111', '잠원한강공원', 'Jamwon Hangang Park', '공원', 37.52336245003122, 127.01397131593632), +('POI112', '청계산', 'Cheonggyesan', '공원', 37.443365424959026, 127.0469697170313), +('POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264), +('POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616), +('POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821), +('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286); \ No newline at end of file From a010ce9c727708541bb8bae748fca07cdb422a08 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Thu, 17 Apr 2025 13:26:27 +0900 Subject: [PATCH 019/361] =?UTF-8?q?area-db=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=20=EC=82=BD=EC=9E=85=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- place-service/Dockerfile | 11 ++ place-service/build.gradle | 2 +- place-service/docker-compose.yml | 26 ++++ .../com/example/placeservice/entity/Area.java | 6 + .../src/main/resources/application.properties | 9 +- place-service/src/main/resources/data.sql | 117 ++++++++++++++++++ 6 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 place-service/Dockerfile create mode 100644 place-service/docker-compose.yml create mode 100644 place-service/src/main/resources/data.sql diff --git a/place-service/Dockerfile b/place-service/Dockerfile new file mode 100644 index 0000000..2e9e322 --- /dev/null +++ b/place-service/Dockerfile @@ -0,0 +1,11 @@ +# Gradle +FROM gradle:8.13.0-jdk17 AS build +WORKDIR /app +COPY . . +RUN gradle build -x test + +FROM eclipse-temurin:17-jdk +WORKDIR /app +COPY --from=build /app/build/libs/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/place-service/build.gradle b/place-service/build.gradle index 6329d25..201fa02 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -55,4 +55,4 @@ tasks.named('bootBuildImage') { tasks.named('test') { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/place-service/docker-compose.yml b/place-service/docker-compose.yml new file mode 100644 index 0000000..31e1bab --- /dev/null +++ b/place-service/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3' +services: + db: + image: postgres:16.8 + container_name: my_postgres + environment: + POSTGRES_DB: stars_db + POSTGRES_USER: root + POSTGRES_PASSWORD: admin + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + app: + build: . + container_name: my_spring_app + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/stars_db + SPRING_DATASOURCE_USERNAME: root + SPRING_DATASOURCE_PASSWORD: admin + ports: + - "8080:8080" + depends_on: + - db +volumes: + pgdata: diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index faa2087..6c14791 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -11,9 +11,15 @@ public class Area { @Column(name = "area_id") private Long area_id; + @Column(name = "seoul_id", length = 10, nullable = false) + private String seoul_id; + @Column(name = "name", length = 50, nullable = false) private String name; // 지역명 + @Column(name = "name_eng", length = 100, nullable = false) + private String name_eng; + @Column(name = "category", length = 20, nullable = false) private String category; // 구분 diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index d5cbebf..5734ed4 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -2,10 +2,13 @@ spring.application.name=place-service # postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/star -spring.datasource.username=admin +spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db +spring.datasource.username=root spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update +spring.jpa.defer-datasource-initialization=true +spring.jpa.hibernate.ddl-auto=create +spring.jpa.show-sql=true +spring.sql.init.mode=always diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql new file mode 100644 index 0000000..5ad3979 --- /dev/null +++ b/place-service/src/main/resources/data.sql @@ -0,0 +1,117 @@ +INSERT INTO area (seoul_id, name, name_eng, category, x, y) VALUES +('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), +('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), +('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), +('POI004', '이태원 관광특구', 'Itaewon Special Tourist Zone', '관광특구', 37.53475718815058, 126.99591956660342), +('POI005', '잠실 관광특구', 'Jamsil Special Tourist Zone', '관광특구', 37.51868923685722, 127.11694379878114), +('POI006', '종로·청계 관광특구', 'Jongno Cheonggye Special Toruist Zone', '관광특구', 37.56984229024498, 126.99866678943675), +('POI007', '홍대 관광특구', 'HongDae Culture & Arts Special Tourist Zone', '관광특구', 37.55506170187393, 126.92159115672915), +('POI008', '경복궁', 'Gyeongbokgung Palace', '고궁·문화유산', 37.5809150911806, 126.97764726625209), +('POI009', '광화문·덕수궁', 'Gwanghwamun & Deoksugung Palace', '고궁·문화유산', 37.56847081447724, 126.97620594802834), +('POI010', '보신각', 'Bosingak', '고궁·문화유산', 37.57089797616037, 126.98337607511172), +('POI011', '서울 암사동 유적', 'Amsa Prehistoric Settlement Site', '고궁·문화유산', 37.56102809827398, 127.13078491934499), +('POI012', '창덕궁·종묘', 'Changdeokgung Palace & Jongmyo Shrine', '고궁·문화유산', 37.5795304952381, 126.99416802285715), +('POI013', '가산디지털단지역', 'Gasan Digital Complex station', '인구밀집지역', 37.48267268376068, 126.88014526210826), +('POI014', '강남역', 'Gangnam station', '인구밀집지역', 37.50019794543511, 127.02822145618981), +('POI015', '건대입구역', 'Konkuk University station', '인구밀집지역', 37.540005312302846, 127.06718185804415), +('POI016', '고덕역', 'Godeok station', '인구밀집지역', 37.553377141111774, 127.15503341723486), +('POI017', '고속터미널역', 'Express Bus Terminal station', '인구밀집지역', 37.50472316731823, 127.00542276570697), +('POI018', '교대역', 'Seoul National University of Education station', '인구밀집지역', 37.49185444557083, 127.01456630812736), +('POI019', '구로디지털단지역', 'Guro Digital Complex station', '인구밀집지역', 37.48349491938897, 126.89557915984915), +('POI020', '구로역', 'Guro station', '인구밀집지역', 37.501517014484925, 126.88285531403136), +('POI021', '군자역', 'Gunja station', '인구밀집지역', 37.555916439038526, 127.08008933356267), +('POI022', '남구로역', 'Namguro station', '인구밀집지역', 37.484184300878496, 126.88539683418863), +('POI023', '대림역', 'Daerim station', '인구밀집지역', 37.492818237753816, 126.89567307890546), +('POI024', '동대문역', 'Dongdaemun station', '인구밀집지역', 37.57127393968462, 127.0096854515538), +('POI025', '뚝섬역', 'Ttukseom station', '인구밀집지역', 37.548249670448335, 127.04603079475764), +('POI026', '미아사거리역', 'Miasageori station', '인구밀집지역', 37.61244898963818, 127.0305835226811), +('POI027', '발산역', 'Balsan station', '인구밀집지역', 37.55796811527854, 126.84179723439551), +('POI028', '북한산우이역', 'Bukhansan Ui station', '인구밀집지역', 37.66281154490492, 127.01070580250341), +('POI029', '사당역', 'Sadang station', '인구밀집지역', 37.47635049189678, 126.98129519152415), +('POI030', '삼각지역', 'Samgakji station', '인구밀집지역', 37.535323919468816, 126.97392611189508), +('POI031', '서울대입구역', 'Seoul National University station', '인구밀집지역', 37.48060991955129, 126.95360407634334), +('POI032', '서울식물원·마곡나루역', 'Seoul Botanic Park·Magongnaru station', '인구밀집지역', 37.56745047572558, 126.82986126533689), +('POI033', '서울역', 'Seoul station', '인구밀집지역', 37.55662336923077, 126.97216878769233), +('POI034', '선릉역', 'Seolleung station', '인구밀집지역', 37.50584166753116, 127.0489100692203), +('POI035', '성신여대입구역', 'Sungshin Women''s University station', '인구밀집지역', 37.59262801654558, 127.01658994286346), +('POI036', '수유역', 'Suyu station', '인구밀집지역', 37.641215514705884, 127.02565393627451), +('POI037', '신논현역·논현역', 'Sinnonhyeon·Nonhyeon station', '인구밀집지역', 37.50848054554439, 127.02291493850254), +('POI038', '신도림역', 'Sindorim station', '인구밀집지역', 37.50941493666961, 126.89096014067083), +('POI039', '신림역', 'Sillim station', '인구밀집지역', 37.48396968904173, 126.92987028995233), +('POI040', '신촌·이대역', 'Sinchon·Ewha Womans University station', '인구밀집지역', 37.55564146783625, 126.93761215497075), +('POI041', '양재역', 'Yangjae station', '인구밀집지역', 37.48451422353994, 127.03551115644439), +('POI042', '역삼역', 'Yeoksam station', '인구밀집지역', 37.50011177778084, 127.03721339389585), +('POI043', '연신내역', 'Yeonsinnae station', '인구밀집지역', 37.61903547457627, 126.92180184322034), +('POI044', '오목교역·목동운동장', 'Omokgyo·Mok-dong Stadium station', '인구밀집지역', 37.53174788866626, 126.87924768246015), +('POI045', '왕십리역', 'Wangsimni station', '인구밀집지역', 37.561636745463275, 127.03857508984515), +('POI046', '용산역', 'Yongsan station', '인구밀집지역', 37.53059674809017, 126.95953798376128), +('POI047', '이태원역', 'Itaewon station', '인구밀집지역', 37.534847968613946, 126.99261390234051), +('POI048', '장지역', 'Jangji station', '인구밀집지역', 37.47861018962738, 127.12424263037641), +('POI049', '장한평역', 'Janghanpyeong station', '인구밀집지역', 37.562849061361526, 127.06235115915185), +('POI050', '천호역', 'Cheonho station', '인구밀집지역', 37.53921065146112, 127.12470374198884), +('POI051', '총신대입구(이수)역', 'Chongshin University(Isu) station', '인구밀집지역', 37.48677821828824, 126.9807539648491), +('POI052', '충정로역', 'Chungjeongno station', '인구밀집지역', 37.559524839900476, 126.9642059673645), +('POI053', '합정역', 'Hapjeong station', '인구밀집지역', 37.54857307399022, 126.91084543497917), +('POI054', '혜화역', 'Hyehwa station', '인구밀집지역', 37.58237426286851, 127.0014404389915), +('POI055', '홍대입구역(2호선)', 'Hongik University station(Line 2)', '인구밀집지역', 37.5560688250626, 126.92329766229786), +('POI056', '회기역', 'Hoegi station', '인구밀집지역', 37.590538258503685, 127.05700039395965), +('POI057', '4·19 카페거리', '4·19 Cafe street', '발달상권', 37.64481185893745, 127.00739701908337), +('POI058', '가락시장', 'Garak Market', '발달상권', 37.49445476721721, 127.11054976163454), +('POI059', '가로수길', 'Garosu-gil', '발달상권', 37.522662622516556, 127.02543305960263), +('POI060', '광장(전통)시장', 'Gwangjang(Traditional) Market', '발달상권', 37.569817036518465, 127.00014893169192), +('POI061', '김포공항', 'Gimpo Airport', '발달상권', 37.56228914604604, 126.80289965159135), +('POI062', '낙산공원·이화마을', 'Naksan Park·Ihwa Village', '발달상권', 37.580295002146926, 127.00692469978675), +('POI063', '노량진', 'Noryangjin', '발달상권', 37.513690699957316, 126.94907070192423), +('POI064', '덕수궁길·정동길', 'Deoksugung-gil·Jeongdong-gil', '발달상권', 37.5659744042236, 126.97244482634493), +('POI065', '방배역 먹자골목', 'Bangbae food alley', '발달상권', 37.48234102283986, 126.99597969571558), +('POI066', '북촌한옥마을', 'Bukchon Hanok Village', '발달상권', 37.58425649416171, 126.98386049085175), +('POI067', '서촌', 'Seochon', '발달상권', 37.58018833392947, 126.9666234325548), +('POI068', '성수카페거리', 'Seongsu Cafe Street', '발달상권', 37.54227657813654, 127.05560237183015), +('POI069', '수유리 먹자골목', 'Suyuri food alley', '발달상권', 37.64270823057644, 127.02733855833522), +('POI070', '쌍문동 맛집거리', 'Ssangmun-dong restaurant street', '발달상권', 37.6477285443038, 127.03242528481013), +('POI071', '압구정로데오거리', 'Apgujeong Rodeo Street', '발달상권', 37.52560441860466, 127.04040171511626), +('POI072', '여의도', 'Yeouido', '발달상권', 37.52549260943663, 126.92338937129487), +('POI073', '연남동', 'Yeonnam-dong', '발달상권', 37.561426426204264, 126.92406273242905), +('POI074', '영등포 타임스퀘어', 'Yeongdeungpo Time square', '발달상권', 37.51653179760669, 126.90523931614634), +('POI075', '외대앞', 'Hankuk University of Foreign Studies', '발달상권', 37.59442412843283, 127.06153833102397), +('POI076', '용리단길', 'Yongnidan-gil', '발달상권', 37.5311056439386, 126.97147359358237), +('POI077', '이태원 앤틱가구거리', 'Itaewon Antiques street', '발달상권', 37.53187270743738, 126.99343376513602), +('POI078', '인사동·익선동', 'Insa-dong·Ikseon-dong', '발달상권', 37.5741577489609, 126.98736374162822), +('POI079', '창동 신경제 중심지', 'Changdong New Economic Center', '발달상권', 37.656167961335676, 127.05328808787348), +('POI080', '청담동 명품거리', 'Cheongdam-dong Luxury Fashion street', '발달상권', 37.52645537152884, 127.04354058968364), +('POI081', '청량리 제기동 일대 전통시장', 'Traditional market in Cheongnyangni Jegi-dong', '발달상권', 37.579977738703214, 127.0382790126348), +('POI082', '해방촌·경리단길', 'Haebangchon·Gyeongnidan-gil', '발달상권', 37.54143829714048, 126.98836800969178), +('POI083', 'DDP(동대문디자인플라자)', 'DDP(Dongdaemun Design Plaza)', '발달상권', 37.56654201564661, 127.00993045294622), +('POI084', 'DMC(디지털미디어시티)', 'DMC(Digital Media City)', '발달상권', 37.577407110655734, 126.8936691147541), +('POI085', '강서한강공원', 'Gangseo Hangang Park', '공원', 37.5858708295553, 126.82002478578644), +('POI086', '고척돔', 'Gocheok Dome', '공원', 37.49776790791694, 126.86713336195083), +('POI087', '광나루한강공원', 'Gwangnaru Hangang Park', '공원', 37.550866089267345, 127.12370647922764), +('POI088', '광화문광장', 'Gwanghwamun Square', '공원', 37.57505062349029, 126.97680679071502), +('POI089', '국립중앙박물관·용산가족공원', 'The National Museum of Korea·Yongsan Family Park', '공원', 37.52202119313341, 126.9824679850178), +('POI090', '난지한강공원', 'Nanji Hangang Park', '공원', 37.566316740172454, 126.87770001597329), +('POI091', '남산공원', 'Namsan Park', '공원', 37.555070650369004, 126.99365012730628), +('POI092', '노들섬', 'Nodeul island', '공원', 37.517556872131706, 126.95871397855296), +('POI093', '뚝섬한강공원', 'Ttukseom Hangang Park', '공원', 37.52882503002481, 127.07460742502332), +('POI094', '망원한강공원', 'Mangwon Hangang Park', '공원', 37.55480087656761, 126.89823749216892), +('POI095', '반포한강공원', 'Banpo Hangang Park', '공원', 37.51065462735107, 126.99457550649858), +('POI096', '북서울꿈의숲', 'Dream Forest', '공원', 37.62244979881656, 127.04069568441815), +('POI097', '불광천', 'Bulgwangcheon River', '공원', 37.58940646530798, 126.91194122899687), +('POI098', '서리풀공원·몽마르뜨공원', 'Seoripul Park·Montmartre Park', '공원', 37.49022054658995, 127.00173655775917), +('POI100', '서울대공원', 'Seoul Grand Park', '공원', 37.42839607329216, 127.01863790236284), +('POI101', '서울숲공원', 'Seoul Forest', '공원', 37.54402680167043, 127.03673083133076), +('POI099', '서울광장', 'Seoul Plaza', '공원', 37.5655010296151, 126.97798702124584), +('POI102', '아차산', 'Achasan', '공원', 37.56307021845223, 127.10047918048643), +('POI103', '양화한강공원', 'Yanghwa Hangang Park', '공원', 37.54029653900909, 126.89963558394624), +('POI104', '어린이대공원', 'Children''s Grand Park', '공원', 37.54957100980681, 127.0824313180458), +('POI105', '여의도한강공원', 'Yeouido Hangang Park', '공원', 37.52868909088513, 126.92627587984478), +('POI106', '월드컵공원', 'World Cup Park', '공원', 37.5660391150387, 126.88740201899091), +('POI107', '응봉산', 'Eungbongsan', '공원', 37.54797346368485, 127.02904643771303), +('POI108', '이촌한강공원', 'Ichon Hangang Park', '공원', 37.52139035897837, 126.96356631928725), +('POI109', '잠실종합운동장', 'Jamsil (Seoul) Sports Complex', '공원', 37.51610665876664, 127.07230325047283), +('POI110', '잠실한강공원', 'Jamsil Hangang Park', '공원', 37.51981181577704, 127.08507449607517), +('POI111', '잠원한강공원', 'Jamwon Hangang Park', '공원', 37.52336245003122, 127.01397131593632), +('POI112', '청계산', 'Cheonggyesan', '공원', 37.443365424959026, 127.0469697170313), +('POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264), +('POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616), +('POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821), +('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286); \ No newline at end of file From f8c14ceac0dc08ed9dccc93d7317e7acb9b5c354 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Thu, 17 Apr 2025 13:38:48 +0900 Subject: [PATCH 020/361] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 지역 db 관련 빌드 및 실행 방법 작성 --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a98003..21c6b6e 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,12 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update - ``` \ No newline at end of file + ``` + +#### PostgreSQL과 프로젝트 같이 띄우는 법 +1. 프로젝트 최상위 디렉토리 (/place-service, docker-compose.yml과 Dockerfile이 있는 위치)에서 docker compose up --build 실행 +2. 빌드가 완료되면 자동으로 Spring 로고가 나오면서 실행됨 +3. 다른 cmd 창에서 docker exec -it my_postgres bash 실행 +4. psql -U root -d stars_db 실행하면 접속이 될 것임 +5. 접속한 후 \dt 로 테이블 존재 확인 가능, select 문으로 데이터 확인 가능 ++ select * from area; 로 결과를 본 후, q 를 눌러야 다시 명령창으로 돌아갈 수 있음 From a5d467509b404ca957acc92405a09a392385c0d1 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Thu, 17 Apr 2025 13:38:48 +0900 Subject: [PATCH 021/361] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 지역 db 관련 빌드 및 실행 방법 작성 --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a98003..21c6b6e 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,12 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update - ``` \ No newline at end of file + ``` + +#### PostgreSQL과 프로젝트 같이 띄우는 법 +1. 프로젝트 최상위 디렉토리 (/place-service, docker-compose.yml과 Dockerfile이 있는 위치)에서 docker compose up --build 실행 +2. 빌드가 완료되면 자동으로 Spring 로고가 나오면서 실행됨 +3. 다른 cmd 창에서 docker exec -it my_postgres bash 실행 +4. psql -U root -d stars_db 실행하면 접속이 될 것임 +5. 접속한 후 \dt 로 테이블 존재 확인 가능, select 문으로 데이터 확인 가능 ++ select * from area; 로 결과를 본 후, q 를 눌러야 다시 명령창으로 돌아갈 수 있음 From 01b5b6baf40dd9df12213b2d74a17abe9c9d8057 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Thu, 17 Apr 2025 13:38:48 +0900 Subject: [PATCH 022/361] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 지역 db 관련 빌드 및 실행 방법 작성 --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a98003..21c6b6e 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,12 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update - ``` \ No newline at end of file + ``` + +#### PostgreSQL과 프로젝트 같이 띄우는 법 +1. 프로젝트 최상위 디렉토리 (/place-service, docker-compose.yml과 Dockerfile이 있는 위치)에서 docker compose up --build 실행 +2. 빌드가 완료되면 자동으로 Spring 로고가 나오면서 실행됨 +3. 다른 cmd 창에서 docker exec -it my_postgres bash 실행 +4. psql -U root -d stars_db 실행하면 접속이 될 것임 +5. 접속한 후 \dt 로 테이블 존재 확인 가능, select 문으로 데이터 확인 가능 ++ select * from area; 로 결과를 본 후, q 를 눌러야 다시 명령창으로 돌아갈 수 있음 From 74df598eb13d707c51cf3759d9ba20512f890e1a Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 17 Apr 2025 13:40:33 +0900 Subject: [PATCH 023/361] =?UTF-8?q?Feat:=20=EC=97=90=EB=9F=AC=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C-Globalexception=20=EC=B6=94=EA=B0=80=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/CustomExceptionClass.java | 34 ++++++++ .../exception/ErrorCode.java | 49 +++++++++++ .../exception/ErrorResponse.java | 18 ++++ .../exception/GlobalExceptionHandler.java | 87 +++++++++++++++++++ .../service/RoadService.java | 6 +- 5 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java new file mode 100644 index 0000000..9a78486 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java @@ -0,0 +1,34 @@ +package com.example.externalinfoservice.exception; + +public class CustomExceptionClass { + public static class DataNotFoundException extends RuntimeException { + public DataNotFoundException(String message) { + super(message); + } + + public DataNotFoundException(String message, Throwable cause) { + super(message, cause); + } + } + + public static class ConflictException extends RuntimeException { + public ConflictException(String message) { + super(message); + } + + public ConflictException(String message, Throwable cause) { + super(message, cause); + } + } + + public static class TooManyRequestsException extends RuntimeException { + public TooManyRequestsException(String message) { + super(message); + } + + public TooManyRequestsException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java new file mode 100644 index 0000000..a84b39c --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java @@ -0,0 +1,49 @@ +package com.example.externalinfoservice.exception; + +import org.springframework.http.HttpStatus; + +public enum ErrorCode { + // 400 - 1000 - Invalid request + INVALID_REQUEST("1000", HttpStatus.BAD_REQUEST), + DATA_NOT_FOUND("1001", HttpStatus.BAD_REQUEST), + + // 401 - 2000 - Unauthorized + UNAUTHORIZED("2000", HttpStatus.UNAUTHORIZED), + + // 403 - 2001 - Forbidden + FORBIDDEN("2001", HttpStatus.FORBIDDEN), + + // 404 - 3000 - Resource not found + RESOURCE_NOT_FOUND("3000", HttpStatus.NOT_FOUND), + + // 409 - 3001 - Conflict + CONFLICT("3001", HttpStatus.CONFLICT), + + // 422 - 3002 - Validation failed + VALIDATION_FAILED("3002", HttpStatus.UNPROCESSABLE_ENTITY), + + // 429 - 3003 - Too Many Requests + TOO_MANY_REQUESTS("3003", HttpStatus.TOO_MANY_REQUESTS), + + // 500 - 9000 - Internal server error + INTERNAL_SERVER_ERROR("9000", HttpStatus.INTERNAL_SERVER_ERROR), + + // 503 - 9001 - Service unavailable + SERVICE_UNAVAILABLE("9001", HttpStatus.SERVICE_UNAVAILABLE); + + private final String code; + private final HttpStatus status; + + ErrorCode(String code, HttpStatus status) { + this.code = code; + this.status = status; + } + + public String getCode() { + return code; + } + + public HttpStatus getStatus() { + return status; + } +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java new file mode 100644 index 0000000..276838f --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java @@ -0,0 +1,18 @@ +package com.example.externalinfoservice.exception; + +import lombok.Data; + +@Data +public class ErrorResponse { + private int status; + private String errorCode; + private String message; + + public ErrorResponse(int status, String errorCode, String message) { + this.status = status; + this.errorCode = errorCode; + this.message = message; + } + + +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..cc99e05 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java @@ -0,0 +1,87 @@ +package com.example.externalinfoservice.exception; + +import org.apache.kafka.common.errors.InvalidRequestException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import javax.naming.ServiceUnavailableException; +import javax.xml.bind.ValidationException; + +@RestControllerAdvice +public class GlobalExceptionHandler { + // Spring에서 제공하는 기본 예외 클래스 + + // 400 - 1000 - InvalidRequestException + @ExceptionHandler(InvalidRequestException.class) + public ResponseEntity handleInvalidRequestException(InvalidRequestException ex) { + return buildErrorResponse( ErrorCode.INVALID_REQUEST, ex.getMessage()); + } + // 401 - 2000 - Unauthorized + @ExceptionHandler(HttpClientErrorException.Unauthorized.class) + public ResponseEntity handleUnauthorizedException(HttpClientErrorException.Unauthorized ex) { + return buildErrorResponse( ErrorCode.UNAUTHORIZED, ex.getMessage()); + } + // 403 - 2001 - Forbidden + @ExceptionHandler(HttpClientErrorException.Forbidden.class) + public ResponseEntity handleForbiddenException(HttpClientErrorException.Forbidden ex) { + return buildErrorResponse(ErrorCode.FORBIDDEN, ex.getMessage()); + } + // 404 - 3000 - Resource not found + @ExceptionHandler(HttpClientErrorException.NotFound.class) + public ResponseEntity handleNotFoundException(HttpClientErrorException.NotFound ex) { + return buildErrorResponse(ErrorCode.RESOURCE_NOT_FOUND, ex.getMessage()); + } + + // 커스텀 예외 클래스 + // 400 - 1001 - DataNotFoundException + @ExceptionHandler(CustomExceptionClass.DataNotFoundException.class) + public ResponseEntity handleDataNotFoundException(CustomExceptionClass.DataNotFoundException ex) { + return buildErrorResponse(ErrorCode.DATA_NOT_FOUND, ex.getMessage()); + } + + // 409 - 3001 - Conflict + @ExceptionHandler(CustomExceptionClass.ConflictException.class) // 커스텀 예외 + public ResponseEntity handleConflictException(CustomExceptionClass.ConflictException ex) { + return buildErrorResponse(ErrorCode.CONFLICT, ex.getMessage()); + } + + // 422 - 3002 - Validation failed + @ExceptionHandler(ValidationException.class) // javax.validation 또는 커스텀 가능 + public ResponseEntity handleValidationException(ValidationException ex) { + return buildErrorResponse(ErrorCode.VALIDATION_FAILED, ex.getMessage()); + } + + // 429 - 3003 - Too Many Requests + @ExceptionHandler(CustomExceptionClass.TooManyRequestsException.class) + public ResponseEntity handleTooManyRequestsException(CustomExceptionClass.TooManyRequestsException ex) { + return buildErrorResponse(ErrorCode.TOO_MANY_REQUESTS, ex.getMessage()); + } + + // 500 - 9000 - Internal server error + @ExceptionHandler(Exception.class) + public ResponseEntity handleInternalServerError(Exception ex) { + return buildErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage()); + } + + // 503 - 9001 - Service unavailable + @ExceptionHandler(ServiceUnavailableException.class) // 커스텀 예외 + public ResponseEntity handleServiceUnavailableException(ServiceUnavailableException ex) { + return buildErrorResponse(ErrorCode.SERVICE_UNAVAILABLE, ex.getMessage()); + } + + + // === 공통 응답 생성 메서드 === + private ResponseEntity buildErrorResponse(ErrorCode errorCode, String message) { + return new ResponseEntity<>( + new ErrorResponse(errorCode.getStatus().value(), errorCode.getCode(), message), + errorCode.getStatus() + ); + } + + + + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java index 40ea77f..5362c98 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java @@ -3,8 +3,8 @@ import com.example.externalinfoservice.dto.RoadDto; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Unmarshaller; +import org.apache.kafka.common.errors.InvalidRequestException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @@ -35,7 +35,9 @@ public String getTrafficData(String id) { return json; } catch (Exception e) { - throw new RuntimeException("XML 파싱 실패", e); + System.out.println(xmlResponse); + System.out.println(e.getMessage()); + throw new InvalidRequestException(e.getMessage(), e); } } From 5795c71971916e6f18b3d38ab84b8b8ebf2bfc6f Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 17 Apr 2025 13:40:33 +0900 Subject: [PATCH 024/361] =?UTF-8?q?Feat:=20=EC=97=90=EB=9F=AC=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C-Globalexception=20=EC=B6=94=EA=B0=80=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/CustomExceptionClass.java | 34 ++++++++ .../exception/ErrorCode.java | 49 +++++++++++ .../exception/ErrorResponse.java | 18 ++++ .../exception/GlobalExceptionHandler.java | 87 +++++++++++++++++++ .../service/RoadService.java | 6 +- 5 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java new file mode 100644 index 0000000..9a78486 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java @@ -0,0 +1,34 @@ +package com.example.externalinfoservice.exception; + +public class CustomExceptionClass { + public static class DataNotFoundException extends RuntimeException { + public DataNotFoundException(String message) { + super(message); + } + + public DataNotFoundException(String message, Throwable cause) { + super(message, cause); + } + } + + public static class ConflictException extends RuntimeException { + public ConflictException(String message) { + super(message); + } + + public ConflictException(String message, Throwable cause) { + super(message, cause); + } + } + + public static class TooManyRequestsException extends RuntimeException { + public TooManyRequestsException(String message) { + super(message); + } + + public TooManyRequestsException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java new file mode 100644 index 0000000..a84b39c --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java @@ -0,0 +1,49 @@ +package com.example.externalinfoservice.exception; + +import org.springframework.http.HttpStatus; + +public enum ErrorCode { + // 400 - 1000 - Invalid request + INVALID_REQUEST("1000", HttpStatus.BAD_REQUEST), + DATA_NOT_FOUND("1001", HttpStatus.BAD_REQUEST), + + // 401 - 2000 - Unauthorized + UNAUTHORIZED("2000", HttpStatus.UNAUTHORIZED), + + // 403 - 2001 - Forbidden + FORBIDDEN("2001", HttpStatus.FORBIDDEN), + + // 404 - 3000 - Resource not found + RESOURCE_NOT_FOUND("3000", HttpStatus.NOT_FOUND), + + // 409 - 3001 - Conflict + CONFLICT("3001", HttpStatus.CONFLICT), + + // 422 - 3002 - Validation failed + VALIDATION_FAILED("3002", HttpStatus.UNPROCESSABLE_ENTITY), + + // 429 - 3003 - Too Many Requests + TOO_MANY_REQUESTS("3003", HttpStatus.TOO_MANY_REQUESTS), + + // 500 - 9000 - Internal server error + INTERNAL_SERVER_ERROR("9000", HttpStatus.INTERNAL_SERVER_ERROR), + + // 503 - 9001 - Service unavailable + SERVICE_UNAVAILABLE("9001", HttpStatus.SERVICE_UNAVAILABLE); + + private final String code; + private final HttpStatus status; + + ErrorCode(String code, HttpStatus status) { + this.code = code; + this.status = status; + } + + public String getCode() { + return code; + } + + public HttpStatus getStatus() { + return status; + } +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java new file mode 100644 index 0000000..276838f --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java @@ -0,0 +1,18 @@ +package com.example.externalinfoservice.exception; + +import lombok.Data; + +@Data +public class ErrorResponse { + private int status; + private String errorCode; + private String message; + + public ErrorResponse(int status, String errorCode, String message) { + this.status = status; + this.errorCode = errorCode; + this.message = message; + } + + +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..cc99e05 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java @@ -0,0 +1,87 @@ +package com.example.externalinfoservice.exception; + +import org.apache.kafka.common.errors.InvalidRequestException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import javax.naming.ServiceUnavailableException; +import javax.xml.bind.ValidationException; + +@RestControllerAdvice +public class GlobalExceptionHandler { + // Spring에서 제공하는 기본 예외 클래스 + + // 400 - 1000 - InvalidRequestException + @ExceptionHandler(InvalidRequestException.class) + public ResponseEntity handleInvalidRequestException(InvalidRequestException ex) { + return buildErrorResponse( ErrorCode.INVALID_REQUEST, ex.getMessage()); + } + // 401 - 2000 - Unauthorized + @ExceptionHandler(HttpClientErrorException.Unauthorized.class) + public ResponseEntity handleUnauthorizedException(HttpClientErrorException.Unauthorized ex) { + return buildErrorResponse( ErrorCode.UNAUTHORIZED, ex.getMessage()); + } + // 403 - 2001 - Forbidden + @ExceptionHandler(HttpClientErrorException.Forbidden.class) + public ResponseEntity handleForbiddenException(HttpClientErrorException.Forbidden ex) { + return buildErrorResponse(ErrorCode.FORBIDDEN, ex.getMessage()); + } + // 404 - 3000 - Resource not found + @ExceptionHandler(HttpClientErrorException.NotFound.class) + public ResponseEntity handleNotFoundException(HttpClientErrorException.NotFound ex) { + return buildErrorResponse(ErrorCode.RESOURCE_NOT_FOUND, ex.getMessage()); + } + + // 커스텀 예외 클래스 + // 400 - 1001 - DataNotFoundException + @ExceptionHandler(CustomExceptionClass.DataNotFoundException.class) + public ResponseEntity handleDataNotFoundException(CustomExceptionClass.DataNotFoundException ex) { + return buildErrorResponse(ErrorCode.DATA_NOT_FOUND, ex.getMessage()); + } + + // 409 - 3001 - Conflict + @ExceptionHandler(CustomExceptionClass.ConflictException.class) // 커스텀 예외 + public ResponseEntity handleConflictException(CustomExceptionClass.ConflictException ex) { + return buildErrorResponse(ErrorCode.CONFLICT, ex.getMessage()); + } + + // 422 - 3002 - Validation failed + @ExceptionHandler(ValidationException.class) // javax.validation 또는 커스텀 가능 + public ResponseEntity handleValidationException(ValidationException ex) { + return buildErrorResponse(ErrorCode.VALIDATION_FAILED, ex.getMessage()); + } + + // 429 - 3003 - Too Many Requests + @ExceptionHandler(CustomExceptionClass.TooManyRequestsException.class) + public ResponseEntity handleTooManyRequestsException(CustomExceptionClass.TooManyRequestsException ex) { + return buildErrorResponse(ErrorCode.TOO_MANY_REQUESTS, ex.getMessage()); + } + + // 500 - 9000 - Internal server error + @ExceptionHandler(Exception.class) + public ResponseEntity handleInternalServerError(Exception ex) { + return buildErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage()); + } + + // 503 - 9001 - Service unavailable + @ExceptionHandler(ServiceUnavailableException.class) // 커스텀 예외 + public ResponseEntity handleServiceUnavailableException(ServiceUnavailableException ex) { + return buildErrorResponse(ErrorCode.SERVICE_UNAVAILABLE, ex.getMessage()); + } + + + // === 공통 응답 생성 메서드 === + private ResponseEntity buildErrorResponse(ErrorCode errorCode, String message) { + return new ResponseEntity<>( + new ErrorResponse(errorCode.getStatus().value(), errorCode.getCode(), message), + errorCode.getStatus() + ); + } + + + + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java index 40ea77f..5362c98 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java @@ -3,8 +3,8 @@ import com.example.externalinfoservice.dto.RoadDto; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Unmarshaller; +import org.apache.kafka.common.errors.InvalidRequestException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @@ -35,7 +35,9 @@ public String getTrafficData(String id) { return json; } catch (Exception e) { - throw new RuntimeException("XML 파싱 실패", e); + System.out.println(xmlResponse); + System.out.println(e.getMessage()); + throw new InvalidRequestException(e.getMessage(), e); } } From 0aa6767dbced52ec4acad8c93a69c0245dd86227 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 17 Apr 2025 13:40:33 +0900 Subject: [PATCH 025/361] =?UTF-8?q?Feat:=20=EC=97=90=EB=9F=AC=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C-Globalexception=20=EC=B6=94=EA=B0=80=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/CustomExceptionClass.java | 34 ++++++++ .../exception/ErrorCode.java | 49 +++++++++++ .../exception/ErrorResponse.java | 18 ++++ .../exception/GlobalExceptionHandler.java | 87 +++++++++++++++++++ .../service/RoadService.java | 6 +- 5 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java new file mode 100644 index 0000000..9a78486 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java @@ -0,0 +1,34 @@ +package com.example.externalinfoservice.exception; + +public class CustomExceptionClass { + public static class DataNotFoundException extends RuntimeException { + public DataNotFoundException(String message) { + super(message); + } + + public DataNotFoundException(String message, Throwable cause) { + super(message, cause); + } + } + + public static class ConflictException extends RuntimeException { + public ConflictException(String message) { + super(message); + } + + public ConflictException(String message, Throwable cause) { + super(message, cause); + } + } + + public static class TooManyRequestsException extends RuntimeException { + public TooManyRequestsException(String message) { + super(message); + } + + public TooManyRequestsException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java new file mode 100644 index 0000000..a84b39c --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java @@ -0,0 +1,49 @@ +package com.example.externalinfoservice.exception; + +import org.springframework.http.HttpStatus; + +public enum ErrorCode { + // 400 - 1000 - Invalid request + INVALID_REQUEST("1000", HttpStatus.BAD_REQUEST), + DATA_NOT_FOUND("1001", HttpStatus.BAD_REQUEST), + + // 401 - 2000 - Unauthorized + UNAUTHORIZED("2000", HttpStatus.UNAUTHORIZED), + + // 403 - 2001 - Forbidden + FORBIDDEN("2001", HttpStatus.FORBIDDEN), + + // 404 - 3000 - Resource not found + RESOURCE_NOT_FOUND("3000", HttpStatus.NOT_FOUND), + + // 409 - 3001 - Conflict + CONFLICT("3001", HttpStatus.CONFLICT), + + // 422 - 3002 - Validation failed + VALIDATION_FAILED("3002", HttpStatus.UNPROCESSABLE_ENTITY), + + // 429 - 3003 - Too Many Requests + TOO_MANY_REQUESTS("3003", HttpStatus.TOO_MANY_REQUESTS), + + // 500 - 9000 - Internal server error + INTERNAL_SERVER_ERROR("9000", HttpStatus.INTERNAL_SERVER_ERROR), + + // 503 - 9001 - Service unavailable + SERVICE_UNAVAILABLE("9001", HttpStatus.SERVICE_UNAVAILABLE); + + private final String code; + private final HttpStatus status; + + ErrorCode(String code, HttpStatus status) { + this.code = code; + this.status = status; + } + + public String getCode() { + return code; + } + + public HttpStatus getStatus() { + return status; + } +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java new file mode 100644 index 0000000..276838f --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java @@ -0,0 +1,18 @@ +package com.example.externalinfoservice.exception; + +import lombok.Data; + +@Data +public class ErrorResponse { + private int status; + private String errorCode; + private String message; + + public ErrorResponse(int status, String errorCode, String message) { + this.status = status; + this.errorCode = errorCode; + this.message = message; + } + + +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..cc99e05 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java @@ -0,0 +1,87 @@ +package com.example.externalinfoservice.exception; + +import org.apache.kafka.common.errors.InvalidRequestException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import javax.naming.ServiceUnavailableException; +import javax.xml.bind.ValidationException; + +@RestControllerAdvice +public class GlobalExceptionHandler { + // Spring에서 제공하는 기본 예외 클래스 + + // 400 - 1000 - InvalidRequestException + @ExceptionHandler(InvalidRequestException.class) + public ResponseEntity handleInvalidRequestException(InvalidRequestException ex) { + return buildErrorResponse( ErrorCode.INVALID_REQUEST, ex.getMessage()); + } + // 401 - 2000 - Unauthorized + @ExceptionHandler(HttpClientErrorException.Unauthorized.class) + public ResponseEntity handleUnauthorizedException(HttpClientErrorException.Unauthorized ex) { + return buildErrorResponse( ErrorCode.UNAUTHORIZED, ex.getMessage()); + } + // 403 - 2001 - Forbidden + @ExceptionHandler(HttpClientErrorException.Forbidden.class) + public ResponseEntity handleForbiddenException(HttpClientErrorException.Forbidden ex) { + return buildErrorResponse(ErrorCode.FORBIDDEN, ex.getMessage()); + } + // 404 - 3000 - Resource not found + @ExceptionHandler(HttpClientErrorException.NotFound.class) + public ResponseEntity handleNotFoundException(HttpClientErrorException.NotFound ex) { + return buildErrorResponse(ErrorCode.RESOURCE_NOT_FOUND, ex.getMessage()); + } + + // 커스텀 예외 클래스 + // 400 - 1001 - DataNotFoundException + @ExceptionHandler(CustomExceptionClass.DataNotFoundException.class) + public ResponseEntity handleDataNotFoundException(CustomExceptionClass.DataNotFoundException ex) { + return buildErrorResponse(ErrorCode.DATA_NOT_FOUND, ex.getMessage()); + } + + // 409 - 3001 - Conflict + @ExceptionHandler(CustomExceptionClass.ConflictException.class) // 커스텀 예외 + public ResponseEntity handleConflictException(CustomExceptionClass.ConflictException ex) { + return buildErrorResponse(ErrorCode.CONFLICT, ex.getMessage()); + } + + // 422 - 3002 - Validation failed + @ExceptionHandler(ValidationException.class) // javax.validation 또는 커스텀 가능 + public ResponseEntity handleValidationException(ValidationException ex) { + return buildErrorResponse(ErrorCode.VALIDATION_FAILED, ex.getMessage()); + } + + // 429 - 3003 - Too Many Requests + @ExceptionHandler(CustomExceptionClass.TooManyRequestsException.class) + public ResponseEntity handleTooManyRequestsException(CustomExceptionClass.TooManyRequestsException ex) { + return buildErrorResponse(ErrorCode.TOO_MANY_REQUESTS, ex.getMessage()); + } + + // 500 - 9000 - Internal server error + @ExceptionHandler(Exception.class) + public ResponseEntity handleInternalServerError(Exception ex) { + return buildErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage()); + } + + // 503 - 9001 - Service unavailable + @ExceptionHandler(ServiceUnavailableException.class) // 커스텀 예외 + public ResponseEntity handleServiceUnavailableException(ServiceUnavailableException ex) { + return buildErrorResponse(ErrorCode.SERVICE_UNAVAILABLE, ex.getMessage()); + } + + + // === 공통 응답 생성 메서드 === + private ResponseEntity buildErrorResponse(ErrorCode errorCode, String message) { + return new ResponseEntity<>( + new ErrorResponse(errorCode.getStatus().value(), errorCode.getCode(), message), + errorCode.getStatus() + ); + } + + + + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java index 40ea77f..5362c98 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java @@ -3,8 +3,8 @@ import com.example.externalinfoservice.dto.RoadDto; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Unmarshaller; +import org.apache.kafka.common.errors.InvalidRequestException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @@ -35,7 +35,9 @@ public String getTrafficData(String id) { return json; } catch (Exception e) { - throw new RuntimeException("XML 파싱 실패", e); + System.out.println(xmlResponse); + System.out.println(e.getMessage()); + throw new InvalidRequestException(e.getMessage(), e); } } From e51facc73b9c6955528fdb489fed323e9ab2ca44 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 17 Apr 2025 14:54:47 +0900 Subject: [PATCH 026/361] =?UTF-8?q?Feat:=20=EC=97=90=EB=9F=AC=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EC=B6=94=EA=B0=80=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.java | 2 +- .../service/RoadService.java | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java index cc99e05..ef70e57 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java @@ -61,7 +61,7 @@ public ResponseEntity handleTooManyRequestsException(CustomExcept } // 500 - 9000 - Internal server error - @ExceptionHandler(Exception.class) + @ExceptionHandler(RuntimeException.class) public ResponseEntity handleInternalServerError(Exception ex) { return buildErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage()); } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java index 5362c98..da952dd 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java @@ -24,20 +24,21 @@ public RoadService(RestTemplate restTemplate) { } public String getTrafficData(String id) { - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String xmlResponse = restTemplate.getForObject(openApiUrl + id, String.class); - - // 2. XML 데이터를 DTO로 변환 try { - // DTO -> JSON + // 1. 외부 OpenAPI에서 XML 데이터 받아오기 + String xmlResponse = restTemplate.getForObject(openApiUrl + id, String.class); + + // 2. XML 데이터를 DTO로 변환 -> JSON ObjectMapper objectMapper = new ObjectMapper(); String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); return json; } catch (Exception e) { - System.out.println(xmlResponse); - System.out.println(e.getMessage()); - throw new InvalidRequestException(e.getMessage(), e); + if (e.getMessage() != null && e.getMessage().contains("Failed to select a proxy")) { + throw new InvalidRequestException(e.getMessage(), e); + }else{ + throw new RuntimeException(e.getMessage(), e); + } } } From fc96734bf879b10e1fde99e299dabb1a356315e4 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 17 Apr 2025 14:54:47 +0900 Subject: [PATCH 027/361] =?UTF-8?q?Feat:=20=EC=97=90=EB=9F=AC=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EC=B6=94=EA=B0=80=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.java | 2 +- .../service/RoadService.java | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java index cc99e05..ef70e57 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java @@ -61,7 +61,7 @@ public ResponseEntity handleTooManyRequestsException(CustomExcept } // 500 - 9000 - Internal server error - @ExceptionHandler(Exception.class) + @ExceptionHandler(RuntimeException.class) public ResponseEntity handleInternalServerError(Exception ex) { return buildErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage()); } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java index 5362c98..da952dd 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java @@ -24,20 +24,21 @@ public RoadService(RestTemplate restTemplate) { } public String getTrafficData(String id) { - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String xmlResponse = restTemplate.getForObject(openApiUrl + id, String.class); - - // 2. XML 데이터를 DTO로 변환 try { - // DTO -> JSON + // 1. 외부 OpenAPI에서 XML 데이터 받아오기 + String xmlResponse = restTemplate.getForObject(openApiUrl + id, String.class); + + // 2. XML 데이터를 DTO로 변환 -> JSON ObjectMapper objectMapper = new ObjectMapper(); String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); return json; } catch (Exception e) { - System.out.println(xmlResponse); - System.out.println(e.getMessage()); - throw new InvalidRequestException(e.getMessage(), e); + if (e.getMessage() != null && e.getMessage().contains("Failed to select a proxy")) { + throw new InvalidRequestException(e.getMessage(), e); + }else{ + throw new RuntimeException(e.getMessage(), e); + } } } From 1b6bc7936efa67292166adb067c2ad79c98a1d1d Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 17 Apr 2025 14:54:47 +0900 Subject: [PATCH 028/361] =?UTF-8?q?Feat:=20=EC=97=90=EB=9F=AC=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EC=B6=94=EA=B0=80=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.java | 2 +- .../service/RoadService.java | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java index cc99e05..ef70e57 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java @@ -61,7 +61,7 @@ public ResponseEntity handleTooManyRequestsException(CustomExcept } // 500 - 9000 - Internal server error - @ExceptionHandler(Exception.class) + @ExceptionHandler(RuntimeException.class) public ResponseEntity handleInternalServerError(Exception ex) { return buildErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage()); } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java index 5362c98..da952dd 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java @@ -24,20 +24,21 @@ public RoadService(RestTemplate restTemplate) { } public String getTrafficData(String id) { - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String xmlResponse = restTemplate.getForObject(openApiUrl + id, String.class); - - // 2. XML 데이터를 DTO로 변환 try { - // DTO -> JSON + // 1. 외부 OpenAPI에서 XML 데이터 받아오기 + String xmlResponse = restTemplate.getForObject(openApiUrl + id, String.class); + + // 2. XML 데이터를 DTO로 변환 -> JSON ObjectMapper objectMapper = new ObjectMapper(); String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); return json; } catch (Exception e) { - System.out.println(xmlResponse); - System.out.println(e.getMessage()); - throw new InvalidRequestException(e.getMessage(), e); + if (e.getMessage() != null && e.getMessage().contains("Failed to select a proxy")) { + throw new InvalidRequestException(e.getMessage(), e); + }else{ + throw new RuntimeException(e.getMessage(), e); + } } } From 32b232d0ca7fa00fa6c3541daee689b63ff0ea68 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 18 Apr 2025 13:04:48 +0900 Subject: [PATCH 029/361] =?UTF-8?q?Feat:=20VisitSeoul=20API=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=EB=B0=8F=20Attraction=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=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 --- README.md | 1 - .../service/RoadService.java | 12 ++- place-service/build.gradle | 6 ++ .../controller/AttractionController.java | 4 + .../placeservice/dto/AttractionDto.java | 53 +++++++++ .../com/example/placeservice/entity/Area.java | 10 +- .../placeservice/entity/Attraction.java | 48 +++++++++ .../placeservice/init/AttractionDataInit.java | 20 ++++ .../repository/AttractionRepository.java | 8 ++ .../service/AttractionService.java | 101 ++++++++++++++++++ .../example/placeservice/util/GeoUtils.java | 20 ++++ .../src/main/resources/application.properties | 1 + place-service/src/main/resources/data.sql | 2 +- 13 files changed, 275 insertions(+), 11 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/controller/AttractionController.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java create mode 100644 place-service/src/main/java/com/example/placeservice/entity/Attraction.java create mode 100644 place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/AttractionService.java create mode 100644 place-service/src/main/java/com/example/placeservice/util/GeoUtils.java diff --git a/README.md b/README.md index 6d0aba8..69a8d28 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update ``` - diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java index da952dd..131b5fd 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java @@ -1,6 +1,7 @@ package com.example.externalinfoservice.service; import com.example.externalinfoservice.dto.RoadDto; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.Unmarshaller; @@ -9,6 +10,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import java.io.IOException; import java.io.StringReader; @Service @@ -33,12 +35,12 @@ public String getTrafficData(String id) { String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); return json; + } catch (InvalidRequestException e) { + throw new InvalidRequestException(e.getMessage(), e); + } catch(IOException e){ + throw new RuntimeException("외부 API 오류",e); } catch (Exception e) { - if (e.getMessage() != null && e.getMessage().contains("Failed to select a proxy")) { - throw new InvalidRequestException(e.getMessage(), e); - }else{ - throw new RuntimeException(e.getMessage(), e); - } + throw new RuntimeException("예상치 못한 오류",e); } } diff --git a/place-service/build.gradle b/place-service/build.gradle index 94bd9c2..19168e5 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -24,6 +24,12 @@ repositories { } dependencies { + // JAXB (XML 파싱) + implementation 'javax.xml.bind:jaxb-api:2.3.1' + // JAXB Runtime (XML 파싱을 위해 필요) + implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3' + + // JWT 처리 implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' diff --git a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java new file mode 100644 index 0000000..f433c39 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java @@ -0,0 +1,4 @@ +package com.example.placeservice.controller; + +public class AttractionController { +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java b/place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java new file mode 100644 index 0000000..92348ab --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java @@ -0,0 +1,53 @@ +package com.example.placeservice.dto; + +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import lombok.Data; + +import java.util.List; + +@Data +@XmlRootElement(name = "NewDataSet") +@XmlAccessorType(XmlAccessType.FIELD) +public class AttractionDto { + + @XmlElement(name = "Table1") + private List attractions; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class AttractionTable { + + @XmlElement(name = "COT_ART_ID") + private String id; + + @XmlElement(name = "TITLE") + private String title; + + @XmlElement(name = "COT_ADDR_NEW") + private String newAddress; + + @XmlElement(name = "COT_MAP_POINTX") + private String mapX; + + @XmlElement(name = "COT_MAP_POINTY") + private String mapY; + + @XmlElement(name = "COT_TEL", nillable = true) + private String tel; + + @XmlElement(name = "COT_HOMEPAGE", nillable = true) + private String homepage; + + @XmlElement(name = "COT_CLOSE_DAY", nillable = true) + private String closeDay; + + @XmlElement(name = "COT_USE_TIME_DESC", nillable = true) + private String useTime; + + // 필요하면 나머지도 추가 + } + +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index 6c14791..1f4468f 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -1,9 +1,11 @@ package com.example.placeservice.entity; import jakarta.persistence.*; +import lombok.Data; import java.math.BigDecimal; +@Data @Entity public class Area { @Id @@ -23,11 +25,11 @@ public class Area { @Column(name = "category", length = 20, nullable = false) private String category; // 구분 - @Column(name = "x", precision = 10, scale = 6, nullable = false) - private BigDecimal x; // 위도 + @Column(name = "lat", precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 - @Column(name = "y", precision = 10, scale = 6, nullable = false) - private BigDecimal y; // 경도 + @Column(name = "lon", precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 } diff --git a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java new file mode 100644 index 0000000..e8ec27c --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java @@ -0,0 +1,48 @@ +package com.example.placeservice.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@Entity +public class Attraction { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "attraction_id") + private Long attraction_id; + + @Column(name = "visit_id", length = 10, nullable = false, unique = true) + private String visitId; + + @Column(name="name", length = 200, nullable = false) + private String name; + + @Column(name="address", length = 200, nullable = false) + private String address; + + @Column(name = "lat", precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 + + @Column(name = "lon", precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 + + + @Column(name="phone", length = 30) + private String phone; + + @Column(name="homepage_url", columnDefinition = "TEXT") + private String homepage_url; + + @Column(name="close_day", columnDefinition = "TEXT") + private String close_day; + + @Column(name="use_time", columnDefinition = "TEXT") + private String use_time; + + // 👇 추가: ManyToOne 관계 매핑 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "area_id", nullable = false) + private Area area; +} diff --git a/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java b/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java new file mode 100644 index 0000000..27d9dd1 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java @@ -0,0 +1,20 @@ +package com.example.placeservice.init; + +import com.example.placeservice.service.AttractionService; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +public class AttractionDataInit implements CommandLineRunner { + + private final AttractionService attractionService; + + public AttractionDataInit(AttractionService attractionService) { + this.attractionService = attractionService; + } + @Override + public void run(String... args) throws Exception { + attractionService.fetchDataFromVisitSeoul(); // 애플리케이션 시작 시 자동 실행 + } + +} diff --git a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java new file mode 100644 index 0000000..3050bf3 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java @@ -0,0 +1,8 @@ +package com.example.placeservice.repository; + +import com.example.placeservice.entity.Attraction; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AttractionRepository extends JpaRepository { + boolean existsByVisitId(String visitId); +} diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java new file mode 100644 index 0000000..1ae444f --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -0,0 +1,101 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.AttractionDto; +import com.example.placeservice.entity.Area; +import com.example.placeservice.entity.Attraction; +import com.example.placeservice.repository.AreaRepository; +import com.example.placeservice.repository.AttractionRepository; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.Unmarshaller; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.io.StringReader; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; + +import static com.example.placeservice.util.GeoUtils.calculateDistanceKm; + +@Service +@RequiredArgsConstructor +public class AttractionService { + private final RestTemplate restTemplate = new RestTemplate(); + private final AttractionRepository attractionRepository; + private final AreaRepository areaRepository; + + // visitSeoul 관광지 데이터 로드 후 attraction 테이블 저장 + public List fetchDataFromVisitSeoul() { + String url="https://www.visitseoul.net/file_save/OPENAPI/OPEN_API_kr.xml"; + List areaList = areaRepository.findAll(); // 여기서 불러와 + + try { + // 1. XML 받아오기 + ResponseEntity response = restTemplate.getForEntity(url, String.class); + String xml = new String(response.getBody().getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); + xml = xml.trim().replace("\uFEFF", ""); + + // 2. JAXB로 XML → DTO 변환 + JAXBContext jaxbContext = JAXBContext.newInstance(AttractionDto.class); + Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + StringReader reader = new StringReader(xml); + AttractionDto dto = (AttractionDto) unmarshaller.unmarshal(reader); + + // 3. DTO → Entity로 변환 (매핑) + List entities = dto.getAttractions().stream() + .map(table -> { + // visit_id 중복 체크 + if (attractionRepository.existsByVisitId(table.getId())) return null; + + // 👇: 조건에 따라 Area 객체를 지정 + Area area = findAreaByCondition(table, areaList); // 예: 주소나 지역코드 등으로 판단 + if (area == null) return null; + + Attraction attraction = new Attraction(); + attraction.setVisitId(table.getId()); + attraction.setName(table.getTitle()); + attraction.setAddress(table.getNewAddress()); + attraction.setLat(new BigDecimal(table.getMapX())); + attraction.setLon(new BigDecimal(table.getMapY())); + attraction.setPhone(table.getTel()); + attraction.setHomepage_url(table.getHomepage()); + attraction.setClose_day(table.getCloseDay()); + attraction.setUse_time(table.getUseTime()); + + attraction.setArea(area); + + return attraction; + }) + .filter(Objects::nonNull) // null 필터링 + .toList(); + + // 4. DB에 저장 + return attractionRepository.saveAll(entities); + + } catch (Exception e) { + e.printStackTrace(); // 로깅 처리 추천 + throw new RuntimeException("외부 XML 데이터 파싱 실패", e); + } + } + + // 관광지 2km이내 area 계산 + private Area findAreaByCondition(AttractionDto.AttractionTable table, List areaList) { + double lat = Double.parseDouble(table.getMapX()); + double lon = Double.parseDouble(table.getMapY()); + for (Area area : areaList) { + double distance = calculateDistanceKm( + lat, lon, + area.getLat().doubleValue(), area.getLon().doubleValue() + ); + if (distance <= 2.0) { // 2km 이내 + return area; + } + } + // 2km 이내에 없는 관광지 체크 + // System.out.println(table.getTitle()+"위도 :"+table.getMapX()+"경도 :"+table.getMapY() ); + return null; + } +} diff --git a/place-service/src/main/java/com/example/placeservice/util/GeoUtils.java b/place-service/src/main/java/com/example/placeservice/util/GeoUtils.java new file mode 100644 index 0000000..7c71636 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/util/GeoUtils.java @@ -0,0 +1,20 @@ +package com.example.placeservice.util; + + +public class GeoUtils { + + private static final double EARTH_RADIUS_KM = 6371.0; + + public static double calculateDistanceKm(double lat1, double lon1, double lat2, double lon2) { + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return EARTH_RADIUS_KM * c; + } + +} \ No newline at end of file diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 0cefa4f..0b6449d 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -1,5 +1,6 @@ spring.application.name=place-service +server.port=8081 # postgreSQL ?? spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index 5ad3979..6ca7d5d 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -1,4 +1,4 @@ -INSERT INTO area (seoul_id, name, name_eng, category, x, y) VALUES +INSERT INTO area (seoul_id, name, name_eng, category, lon, lat) VALUES ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), From 2ffa9ea4ddd4d148e09244915c6a6601a30ba721 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 18 Apr 2025 13:04:48 +0900 Subject: [PATCH 030/361] =?UTF-8?q?Feat:=20VisitSeoul=20API=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=EB=B0=8F=20Attraction=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=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 --- README.md | 1 - .../service/RoadService.java | 12 ++- place-service/build.gradle | 6 ++ .../controller/AttractionController.java | 4 + .../placeservice/dto/AttractionDto.java | 53 +++++++++ .../com/example/placeservice/entity/Area.java | 10 +- .../placeservice/entity/Attraction.java | 48 +++++++++ .../placeservice/init/AttractionDataInit.java | 20 ++++ .../repository/AttractionRepository.java | 8 ++ .../service/AttractionService.java | 101 ++++++++++++++++++ .../example/placeservice/util/GeoUtils.java | 20 ++++ .../src/main/resources/application.properties | 1 + place-service/src/main/resources/data.sql | 2 +- 13 files changed, 275 insertions(+), 11 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/controller/AttractionController.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java create mode 100644 place-service/src/main/java/com/example/placeservice/entity/Attraction.java create mode 100644 place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/AttractionService.java create mode 100644 place-service/src/main/java/com/example/placeservice/util/GeoUtils.java diff --git a/README.md b/README.md index 6d0aba8..69a8d28 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update ``` - diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java index da952dd..131b5fd 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java @@ -1,6 +1,7 @@ package com.example.externalinfoservice.service; import com.example.externalinfoservice.dto.RoadDto; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.Unmarshaller; @@ -9,6 +10,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import java.io.IOException; import java.io.StringReader; @Service @@ -33,12 +35,12 @@ public String getTrafficData(String id) { String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); return json; + } catch (InvalidRequestException e) { + throw new InvalidRequestException(e.getMessage(), e); + } catch(IOException e){ + throw new RuntimeException("외부 API 오류",e); } catch (Exception e) { - if (e.getMessage() != null && e.getMessage().contains("Failed to select a proxy")) { - throw new InvalidRequestException(e.getMessage(), e); - }else{ - throw new RuntimeException(e.getMessage(), e); - } + throw new RuntimeException("예상치 못한 오류",e); } } diff --git a/place-service/build.gradle b/place-service/build.gradle index 94bd9c2..19168e5 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -24,6 +24,12 @@ repositories { } dependencies { + // JAXB (XML 파싱) + implementation 'javax.xml.bind:jaxb-api:2.3.1' + // JAXB Runtime (XML 파싱을 위해 필요) + implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3' + + // JWT 처리 implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' diff --git a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java new file mode 100644 index 0000000..f433c39 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java @@ -0,0 +1,4 @@ +package com.example.placeservice.controller; + +public class AttractionController { +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java b/place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java new file mode 100644 index 0000000..92348ab --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java @@ -0,0 +1,53 @@ +package com.example.placeservice.dto; + +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import lombok.Data; + +import java.util.List; + +@Data +@XmlRootElement(name = "NewDataSet") +@XmlAccessorType(XmlAccessType.FIELD) +public class AttractionDto { + + @XmlElement(name = "Table1") + private List attractions; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class AttractionTable { + + @XmlElement(name = "COT_ART_ID") + private String id; + + @XmlElement(name = "TITLE") + private String title; + + @XmlElement(name = "COT_ADDR_NEW") + private String newAddress; + + @XmlElement(name = "COT_MAP_POINTX") + private String mapX; + + @XmlElement(name = "COT_MAP_POINTY") + private String mapY; + + @XmlElement(name = "COT_TEL", nillable = true) + private String tel; + + @XmlElement(name = "COT_HOMEPAGE", nillable = true) + private String homepage; + + @XmlElement(name = "COT_CLOSE_DAY", nillable = true) + private String closeDay; + + @XmlElement(name = "COT_USE_TIME_DESC", nillable = true) + private String useTime; + + // 필요하면 나머지도 추가 + } + +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index 6c14791..1f4468f 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -1,9 +1,11 @@ package com.example.placeservice.entity; import jakarta.persistence.*; +import lombok.Data; import java.math.BigDecimal; +@Data @Entity public class Area { @Id @@ -23,11 +25,11 @@ public class Area { @Column(name = "category", length = 20, nullable = false) private String category; // 구분 - @Column(name = "x", precision = 10, scale = 6, nullable = false) - private BigDecimal x; // 위도 + @Column(name = "lat", precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 - @Column(name = "y", precision = 10, scale = 6, nullable = false) - private BigDecimal y; // 경도 + @Column(name = "lon", precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 } diff --git a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java new file mode 100644 index 0000000..e8ec27c --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java @@ -0,0 +1,48 @@ +package com.example.placeservice.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@Entity +public class Attraction { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "attraction_id") + private Long attraction_id; + + @Column(name = "visit_id", length = 10, nullable = false, unique = true) + private String visitId; + + @Column(name="name", length = 200, nullable = false) + private String name; + + @Column(name="address", length = 200, nullable = false) + private String address; + + @Column(name = "lat", precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 + + @Column(name = "lon", precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 + + + @Column(name="phone", length = 30) + private String phone; + + @Column(name="homepage_url", columnDefinition = "TEXT") + private String homepage_url; + + @Column(name="close_day", columnDefinition = "TEXT") + private String close_day; + + @Column(name="use_time", columnDefinition = "TEXT") + private String use_time; + + // 👇 추가: ManyToOne 관계 매핑 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "area_id", nullable = false) + private Area area; +} diff --git a/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java b/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java new file mode 100644 index 0000000..27d9dd1 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java @@ -0,0 +1,20 @@ +package com.example.placeservice.init; + +import com.example.placeservice.service.AttractionService; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +public class AttractionDataInit implements CommandLineRunner { + + private final AttractionService attractionService; + + public AttractionDataInit(AttractionService attractionService) { + this.attractionService = attractionService; + } + @Override + public void run(String... args) throws Exception { + attractionService.fetchDataFromVisitSeoul(); // 애플리케이션 시작 시 자동 실행 + } + +} diff --git a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java new file mode 100644 index 0000000..3050bf3 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java @@ -0,0 +1,8 @@ +package com.example.placeservice.repository; + +import com.example.placeservice.entity.Attraction; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AttractionRepository extends JpaRepository { + boolean existsByVisitId(String visitId); +} diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java new file mode 100644 index 0000000..1ae444f --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -0,0 +1,101 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.AttractionDto; +import com.example.placeservice.entity.Area; +import com.example.placeservice.entity.Attraction; +import com.example.placeservice.repository.AreaRepository; +import com.example.placeservice.repository.AttractionRepository; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.Unmarshaller; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.io.StringReader; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; + +import static com.example.placeservice.util.GeoUtils.calculateDistanceKm; + +@Service +@RequiredArgsConstructor +public class AttractionService { + private final RestTemplate restTemplate = new RestTemplate(); + private final AttractionRepository attractionRepository; + private final AreaRepository areaRepository; + + // visitSeoul 관광지 데이터 로드 후 attraction 테이블 저장 + public List fetchDataFromVisitSeoul() { + String url="https://www.visitseoul.net/file_save/OPENAPI/OPEN_API_kr.xml"; + List areaList = areaRepository.findAll(); // 여기서 불러와 + + try { + // 1. XML 받아오기 + ResponseEntity response = restTemplate.getForEntity(url, String.class); + String xml = new String(response.getBody().getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); + xml = xml.trim().replace("\uFEFF", ""); + + // 2. JAXB로 XML → DTO 변환 + JAXBContext jaxbContext = JAXBContext.newInstance(AttractionDto.class); + Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + StringReader reader = new StringReader(xml); + AttractionDto dto = (AttractionDto) unmarshaller.unmarshal(reader); + + // 3. DTO → Entity로 변환 (매핑) + List entities = dto.getAttractions().stream() + .map(table -> { + // visit_id 중복 체크 + if (attractionRepository.existsByVisitId(table.getId())) return null; + + // 👇: 조건에 따라 Area 객체를 지정 + Area area = findAreaByCondition(table, areaList); // 예: 주소나 지역코드 등으로 판단 + if (area == null) return null; + + Attraction attraction = new Attraction(); + attraction.setVisitId(table.getId()); + attraction.setName(table.getTitle()); + attraction.setAddress(table.getNewAddress()); + attraction.setLat(new BigDecimal(table.getMapX())); + attraction.setLon(new BigDecimal(table.getMapY())); + attraction.setPhone(table.getTel()); + attraction.setHomepage_url(table.getHomepage()); + attraction.setClose_day(table.getCloseDay()); + attraction.setUse_time(table.getUseTime()); + + attraction.setArea(area); + + return attraction; + }) + .filter(Objects::nonNull) // null 필터링 + .toList(); + + // 4. DB에 저장 + return attractionRepository.saveAll(entities); + + } catch (Exception e) { + e.printStackTrace(); // 로깅 처리 추천 + throw new RuntimeException("외부 XML 데이터 파싱 실패", e); + } + } + + // 관광지 2km이내 area 계산 + private Area findAreaByCondition(AttractionDto.AttractionTable table, List areaList) { + double lat = Double.parseDouble(table.getMapX()); + double lon = Double.parseDouble(table.getMapY()); + for (Area area : areaList) { + double distance = calculateDistanceKm( + lat, lon, + area.getLat().doubleValue(), area.getLon().doubleValue() + ); + if (distance <= 2.0) { // 2km 이내 + return area; + } + } + // 2km 이내에 없는 관광지 체크 + // System.out.println(table.getTitle()+"위도 :"+table.getMapX()+"경도 :"+table.getMapY() ); + return null; + } +} diff --git a/place-service/src/main/java/com/example/placeservice/util/GeoUtils.java b/place-service/src/main/java/com/example/placeservice/util/GeoUtils.java new file mode 100644 index 0000000..7c71636 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/util/GeoUtils.java @@ -0,0 +1,20 @@ +package com.example.placeservice.util; + + +public class GeoUtils { + + private static final double EARTH_RADIUS_KM = 6371.0; + + public static double calculateDistanceKm(double lat1, double lon1, double lat2, double lon2) { + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return EARTH_RADIUS_KM * c; + } + +} \ No newline at end of file diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 0cefa4f..0b6449d 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -1,5 +1,6 @@ spring.application.name=place-service +server.port=8081 # postgreSQL ?? spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index 5ad3979..6ca7d5d 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -1,4 +1,4 @@ -INSERT INTO area (seoul_id, name, name_eng, category, x, y) VALUES +INSERT INTO area (seoul_id, name, name_eng, category, lon, lat) VALUES ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), From 814ab2db3f0719dd5a29fbdff791abed03eb60df Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 18 Apr 2025 13:04:48 +0900 Subject: [PATCH 031/361] =?UTF-8?q?Feat:=20VisitSeoul=20API=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=EB=B0=8F=20Attraction=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=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 --- README.md | 1 - .../service/RoadService.java | 12 ++- place-service/build.gradle | 6 ++ .../controller/AttractionController.java | 4 + .../placeservice/dto/AttractionDto.java | 53 +++++++++ .../com/example/placeservice/entity/Area.java | 10 +- .../placeservice/entity/Attraction.java | 48 +++++++++ .../placeservice/init/AttractionDataInit.java | 20 ++++ .../repository/AttractionRepository.java | 8 ++ .../service/AttractionService.java | 101 ++++++++++++++++++ .../example/placeservice/util/GeoUtils.java | 20 ++++ .../src/main/resources/application.properties | 1 + place-service/src/main/resources/data.sql | 2 +- 13 files changed, 275 insertions(+), 11 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/controller/AttractionController.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java create mode 100644 place-service/src/main/java/com/example/placeservice/entity/Attraction.java create mode 100644 place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/AttractionService.java create mode 100644 place-service/src/main/java/com/example/placeservice/util/GeoUtils.java diff --git a/README.md b/README.md index 6d0aba8..69a8d28 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update ``` - diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java index da952dd..131b5fd 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java @@ -1,6 +1,7 @@ package com.example.externalinfoservice.service; import com.example.externalinfoservice.dto.RoadDto; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.Unmarshaller; @@ -9,6 +10,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import java.io.IOException; import java.io.StringReader; @Service @@ -33,12 +35,12 @@ public String getTrafficData(String id) { String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); return json; + } catch (InvalidRequestException e) { + throw new InvalidRequestException(e.getMessage(), e); + } catch(IOException e){ + throw new RuntimeException("외부 API 오류",e); } catch (Exception e) { - if (e.getMessage() != null && e.getMessage().contains("Failed to select a proxy")) { - throw new InvalidRequestException(e.getMessage(), e); - }else{ - throw new RuntimeException(e.getMessage(), e); - } + throw new RuntimeException("예상치 못한 오류",e); } } diff --git a/place-service/build.gradle b/place-service/build.gradle index 94bd9c2..19168e5 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -24,6 +24,12 @@ repositories { } dependencies { + // JAXB (XML 파싱) + implementation 'javax.xml.bind:jaxb-api:2.3.1' + // JAXB Runtime (XML 파싱을 위해 필요) + implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3' + + // JWT 처리 implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' diff --git a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java new file mode 100644 index 0000000..f433c39 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java @@ -0,0 +1,4 @@ +package com.example.placeservice.controller; + +public class AttractionController { +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java b/place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java new file mode 100644 index 0000000..92348ab --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java @@ -0,0 +1,53 @@ +package com.example.placeservice.dto; + +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import lombok.Data; + +import java.util.List; + +@Data +@XmlRootElement(name = "NewDataSet") +@XmlAccessorType(XmlAccessType.FIELD) +public class AttractionDto { + + @XmlElement(name = "Table1") + private List attractions; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class AttractionTable { + + @XmlElement(name = "COT_ART_ID") + private String id; + + @XmlElement(name = "TITLE") + private String title; + + @XmlElement(name = "COT_ADDR_NEW") + private String newAddress; + + @XmlElement(name = "COT_MAP_POINTX") + private String mapX; + + @XmlElement(name = "COT_MAP_POINTY") + private String mapY; + + @XmlElement(name = "COT_TEL", nillable = true) + private String tel; + + @XmlElement(name = "COT_HOMEPAGE", nillable = true) + private String homepage; + + @XmlElement(name = "COT_CLOSE_DAY", nillable = true) + private String closeDay; + + @XmlElement(name = "COT_USE_TIME_DESC", nillable = true) + private String useTime; + + // 필요하면 나머지도 추가 + } + +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index 6c14791..1f4468f 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -1,9 +1,11 @@ package com.example.placeservice.entity; import jakarta.persistence.*; +import lombok.Data; import java.math.BigDecimal; +@Data @Entity public class Area { @Id @@ -23,11 +25,11 @@ public class Area { @Column(name = "category", length = 20, nullable = false) private String category; // 구분 - @Column(name = "x", precision = 10, scale = 6, nullable = false) - private BigDecimal x; // 위도 + @Column(name = "lat", precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 - @Column(name = "y", precision = 10, scale = 6, nullable = false) - private BigDecimal y; // 경도 + @Column(name = "lon", precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 } diff --git a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java new file mode 100644 index 0000000..e8ec27c --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java @@ -0,0 +1,48 @@ +package com.example.placeservice.entity; + +import jakarta.persistence.*; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@Entity +public class Attraction { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "attraction_id") + private Long attraction_id; + + @Column(name = "visit_id", length = 10, nullable = false, unique = true) + private String visitId; + + @Column(name="name", length = 200, nullable = false) + private String name; + + @Column(name="address", length = 200, nullable = false) + private String address; + + @Column(name = "lat", precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 + + @Column(name = "lon", precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 + + + @Column(name="phone", length = 30) + private String phone; + + @Column(name="homepage_url", columnDefinition = "TEXT") + private String homepage_url; + + @Column(name="close_day", columnDefinition = "TEXT") + private String close_day; + + @Column(name="use_time", columnDefinition = "TEXT") + private String use_time; + + // 👇 추가: ManyToOne 관계 매핑 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "area_id", nullable = false) + private Area area; +} diff --git a/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java b/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java new file mode 100644 index 0000000..27d9dd1 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java @@ -0,0 +1,20 @@ +package com.example.placeservice.init; + +import com.example.placeservice.service.AttractionService; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +public class AttractionDataInit implements CommandLineRunner { + + private final AttractionService attractionService; + + public AttractionDataInit(AttractionService attractionService) { + this.attractionService = attractionService; + } + @Override + public void run(String... args) throws Exception { + attractionService.fetchDataFromVisitSeoul(); // 애플리케이션 시작 시 자동 실행 + } + +} diff --git a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java new file mode 100644 index 0000000..3050bf3 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java @@ -0,0 +1,8 @@ +package com.example.placeservice.repository; + +import com.example.placeservice.entity.Attraction; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AttractionRepository extends JpaRepository { + boolean existsByVisitId(String visitId); +} diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java new file mode 100644 index 0000000..1ae444f --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -0,0 +1,101 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.AttractionDto; +import com.example.placeservice.entity.Area; +import com.example.placeservice.entity.Attraction; +import com.example.placeservice.repository.AreaRepository; +import com.example.placeservice.repository.AttractionRepository; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.Unmarshaller; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.io.StringReader; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; + +import static com.example.placeservice.util.GeoUtils.calculateDistanceKm; + +@Service +@RequiredArgsConstructor +public class AttractionService { + private final RestTemplate restTemplate = new RestTemplate(); + private final AttractionRepository attractionRepository; + private final AreaRepository areaRepository; + + // visitSeoul 관광지 데이터 로드 후 attraction 테이블 저장 + public List fetchDataFromVisitSeoul() { + String url="https://www.visitseoul.net/file_save/OPENAPI/OPEN_API_kr.xml"; + List areaList = areaRepository.findAll(); // 여기서 불러와 + + try { + // 1. XML 받아오기 + ResponseEntity response = restTemplate.getForEntity(url, String.class); + String xml = new String(response.getBody().getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); + xml = xml.trim().replace("\uFEFF", ""); + + // 2. JAXB로 XML → DTO 변환 + JAXBContext jaxbContext = JAXBContext.newInstance(AttractionDto.class); + Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + StringReader reader = new StringReader(xml); + AttractionDto dto = (AttractionDto) unmarshaller.unmarshal(reader); + + // 3. DTO → Entity로 변환 (매핑) + List entities = dto.getAttractions().stream() + .map(table -> { + // visit_id 중복 체크 + if (attractionRepository.existsByVisitId(table.getId())) return null; + + // 👇: 조건에 따라 Area 객체를 지정 + Area area = findAreaByCondition(table, areaList); // 예: 주소나 지역코드 등으로 판단 + if (area == null) return null; + + Attraction attraction = new Attraction(); + attraction.setVisitId(table.getId()); + attraction.setName(table.getTitle()); + attraction.setAddress(table.getNewAddress()); + attraction.setLat(new BigDecimal(table.getMapX())); + attraction.setLon(new BigDecimal(table.getMapY())); + attraction.setPhone(table.getTel()); + attraction.setHomepage_url(table.getHomepage()); + attraction.setClose_day(table.getCloseDay()); + attraction.setUse_time(table.getUseTime()); + + attraction.setArea(area); + + return attraction; + }) + .filter(Objects::nonNull) // null 필터링 + .toList(); + + // 4. DB에 저장 + return attractionRepository.saveAll(entities); + + } catch (Exception e) { + e.printStackTrace(); // 로깅 처리 추천 + throw new RuntimeException("외부 XML 데이터 파싱 실패", e); + } + } + + // 관광지 2km이내 area 계산 + private Area findAreaByCondition(AttractionDto.AttractionTable table, List areaList) { + double lat = Double.parseDouble(table.getMapX()); + double lon = Double.parseDouble(table.getMapY()); + for (Area area : areaList) { + double distance = calculateDistanceKm( + lat, lon, + area.getLat().doubleValue(), area.getLon().doubleValue() + ); + if (distance <= 2.0) { // 2km 이내 + return area; + } + } + // 2km 이내에 없는 관광지 체크 + // System.out.println(table.getTitle()+"위도 :"+table.getMapX()+"경도 :"+table.getMapY() ); + return null; + } +} diff --git a/place-service/src/main/java/com/example/placeservice/util/GeoUtils.java b/place-service/src/main/java/com/example/placeservice/util/GeoUtils.java new file mode 100644 index 0000000..7c71636 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/util/GeoUtils.java @@ -0,0 +1,20 @@ +package com.example.placeservice.util; + + +public class GeoUtils { + + private static final double EARTH_RADIUS_KM = 6371.0; + + public static double calculateDistanceKm(double lat1, double lon1, double lat2, double lon2) { + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return EARTH_RADIUS_KM * c; + } + +} \ No newline at end of file diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 0cefa4f..0b6449d 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -1,5 +1,6 @@ spring.application.name=place-service +server.port=8081 # postgreSQL ?? spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index 5ad3979..6ca7d5d 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -1,4 +1,4 @@ -INSERT INTO area (seoul_id, name, name_eng, category, x, y) VALUES +INSERT INTO area (seoul_id, name, name_eng, category, lon, lat) VALUES ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), From ed2e83df54218c4ec7fd908237c9fb7339b9f2f8 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 18 Apr 2025 13:40:43 +0900 Subject: [PATCH 032/361] =?UTF-8?q?Feat:CAFE=20DATA=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeservice/PlaceServiceApplication.java | 10 +- .../placeservice/config/SecurityConfig.java | 24 ++++ .../controller/CafeController.java | 101 +++++++++++++ .../com/example/placeservice/entity/Area.java | 23 ++- .../com/example/placeservice/entity/Cafe.java | 48 +++++++ .../repository/CafeRepository.java | 18 +++ .../placeservice/service/CafeService.java | 133 ++++++++++++++++++ .../placeservice/service/KakaoApiService.java | 109 ++++++++++++++ .../src/main/resources/application.properties | 6 + place-service/src/main/resources/data.sql | 2 +- 10 files changed, 468 insertions(+), 6 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java create mode 100644 place-service/src/main/java/com/example/placeservice/controller/CafeController.java create mode 100644 place-service/src/main/java/com/example/placeservice/entity/Cafe.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/CafeService.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java index 7aaef28..9a066dc 100644 --- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java +++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java @@ -2,12 +2,20 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.web.client.RestTemplate; @SpringBootApplication +@EnableAsync public class PlaceServiceApplication { public static void main(String[] args) { SpringApplication.run(PlaceServiceApplication.class, args); } -} + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java new file mode 100644 index 0000000..b562d3c --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java @@ -0,0 +1,24 @@ +package com.example.placeservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import static org.springframework.security.config.Customizer.withDefaults; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() // 모든 요청 허용 + ); + + return http.build(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java new file mode 100644 index 0000000..802b512 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java @@ -0,0 +1,101 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.service.CafeService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@RestController +@RequestMapping("/api/cafes") +@RequiredArgsConstructor +public class CafeController { + + private final CafeService cafeService; + + /** + * 모든 장소의 주변 카페 데이터 처리 + */ + @PostMapping("/process-all") + public ResponseEntity> processAllAreas() { + Map response = new HashMap<>(); + + try { + cafeService.processAllAreas(); + + response.put("success", true); + response.put("message", "모든 장소의 주변 카페 정보 처리가 시작되었습니다."); + + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("처리 중 오류 발생: {}", e.getMessage(), e); + + response.put("success", false); + response.put("message", "오류 발생: " + e.getMessage()); + + return ResponseEntity.badRequest().body(response); + } + } + + /** + * 특정 ID 목록의 장소만 처리 + */ + @PostMapping("/process-by-ids") + public ResponseEntity> processAreasByIds(@RequestBody Map> request) { + Map response = new HashMap<>(); + + try { + List areaIds = request.get("areaIds"); + if (areaIds == null || areaIds.isEmpty()) { + response.put("success", false); + response.put("message", "장소 ID 목록이 비어 있습니다."); + return ResponseEntity.badRequest().body(response); + } + + log.info("{}개 장소에 대한 처리 시작", areaIds.size()); + + cafeService.processAreasByIds(areaIds); + + response.put("success", true); + response.put("message", "지정한 장소의 주변 카페 정보 처리가 시작되었습니다."); + + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("처리 중 오류 발생: {}", e.getMessage(), e); + + response.put("success", false); + response.put("message", "오류 발생: " + e.getMessage()); + + return ResponseEntity.badRequest().body(response); + } + } + + /** + * 단일 장소 처리 + */ + @PostMapping("/process/{areaId}") + public ResponseEntity> processSingleArea(@PathVariable Long areaId) { + Map response = new HashMap<>(); + + try { + cafeService.processAreasByIds(List.of(areaId)); + + response.put("success", true); + response.put("message", "장소 ID " + areaId + "의 주변 카페 정보 처리가 시작되었습니다."); + + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("처리 중 오류 발생: {}", e.getMessage(), e); + + response.put("success", false); + response.put("message", "오류 발생: " + e.getMessage()); + + return ResponseEntity.badRequest().body(response); + } + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index 6c14791..8292532 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -23,11 +23,26 @@ public class Area { @Column(name = "category", length = 20, nullable = false) private String category; // 구분 - @Column(name = "x", precision = 10, scale = 6, nullable = false) - private BigDecimal x; // 위도 + @Column(name = "lat", precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 - @Column(name = "y", precision = 10, scale = 6, nullable = false) - private BigDecimal y; // 경도 + @Column(name = "lon", precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 + public Object getName() { + return this.name; + } + + public Long getAreaId() { + return this.area_id; + } + + public Number getY() { + return this.lon; + } + + public Number getX() { + return this.lat; + } } diff --git a/place-service/src/main/java/com/example/placeservice/entity/Cafe.java b/place-service/src/main/java/com/example/placeservice/entity/Cafe.java new file mode 100644 index 0000000..2dffa74 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/Cafe.java @@ -0,0 +1,48 @@ +package com.example.placeservice.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Entity +@Table(name = "cafe") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Cafe { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "cafe_id") + private Long id; + + @Column(name = "name") + private String name; + + @Column(name = "address") + private String address; + + @Column(name = "lat", precision = 10, scale = 6) + private BigDecimal lat; // 위도 + + @Column(name = "lon", precision = 10, scale = 6) + private BigDecimal lon; // 경도 + + @Column(name = "phone") + private String phone; + + @Column(name = "kakaomap_url") + private String kakaomapUrl; + + @Column(name = "category_code") + private String categoryCode; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "area_id") + private Area area; +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java new file mode 100644 index 0000000..eb8e1eb --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java @@ -0,0 +1,18 @@ +package com.example.placeservice.repository; + +import com.example.placeservice.entity.Cafe; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.transaction.annotation.Transactional; +import java.util.List; + +public interface CafeRepository extends JpaRepository { + @Query("SELECT c FROM Cafe c WHERE c.area.area_id = :areaId") + List findByAreaId(@org.springframework.data.repository.query.Param("areaId") Long areaId); + + @Transactional + @Modifying + @Query("DELETE FROM Cafe c WHERE c.area.area_id = :areaId") + void deleteByAreaId(Long areaId); +} diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java new file mode 100644 index 0000000..8b2ade6 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java @@ -0,0 +1,133 @@ +package com.example.placeservice.service; + +import com.example.placeservice.entity.Area; +import com.example.placeservice.entity.Cafe; +import com.example.placeservice.repository.AreaRepository; +import com.example.placeservice.repository.CafeRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CafeService { + + private final AreaRepository areaRepository; + private final CafeRepository cafeRepository; + private final KakaoApiService kakaoApiService; + + /** + * 모든 장소에 대해 주변 카페 데이터 처리 + */ + @Async + public void processAllAreas() { + List areas = areaRepository.findAll(); + log.info("총 {}개 장소에 대한 카페 정보를 처리합니다.", areas.size()); + + for (int i = 0; i < areas.size(); i++) { + Area area = areas.get(i); + log.info("[{}/{}] '{}' (ID: {}) 처리 중...", + i + 1, areas.size(), area.getName(), area.getAreaId()); + + processSingleArea(area); + + // API 호출 간격 조절 + try { + Thread.sleep(300); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + log.info("모든 장소 처리 완료."); + } + + /** + * 특정 ID 목록의 장소만 처리 + */ + @Async + public void processAreasByIds(List areaIds) { + log.info("{}개 지정된 장소에 대한 처리를 시작합니다.", areaIds.size()); + + for (int i = 0; i < areaIds.size(); i++) { + Long areaId = areaIds.get(i); + Area area = areaRepository.findById(areaId).orElse(null); + + if (area == null) { + log.warn("ID {}인 장소를 찾을 수 없습니다.", areaId); + continue; + } + + log.info("[{}/{}] '{}' (ID: {}) 처리 중...", + i + 1, areaIds.size(), area.getName(), area.getAreaId()); + + processSingleArea(area); + + // API 호출 간격 조절 + try { + Thread.sleep(300); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + log.info("지정된 모든 장소 처리 완료."); + } + + /** + * 단일 장소의 주변 카페 정보 처리 + */ + @Transactional + public void processSingleArea(Area area) { + try { + // 해당 지역의 기존 카페 데이터 삭제 + cafeRepository.deleteByAreaId(area.getAreaId()); + + // 주변 카페 검색 + double latitude = area.getY().doubleValue(); + double longitude = area.getX().doubleValue(); + + if (latitude == 0 || longitude == 0) { + log.warn("장소 ID {}의 위도/경도 정보가 없습니다.", area.getAreaId()); + return; + } + + List cafes = + kakaoApiService.findCafesNearby(latitude, longitude, 1000); + + // "음식점 > 카페"가 포함된 카페만 필터링하고 "(휴업중)" 카페는 제외 + List filteredCafes = cafes.stream() + .filter(cafe -> cafe.getCategoryName() != null && cafe.getCategoryName().contains("음식점 > 카페")) + .filter(cafe -> cafe.getPlaceName() == null || !cafe.getPlaceName().contains("(휴업중)")) + .collect(Collectors.toList()); + + log.info("'{}' 주변 {}개의 카페 중 {}개의 '음식점 > 카페' 카테고리를 찾았습니다.", + area.getName(), cafes.size(), filteredCafes.size()); + + // 필터링된 카페 정보 저장 + for (KakaoApiService.CafeResponse.Document cafeDoc : filteredCafes) { + Cafe cafe = Cafe.builder() + .name(cafeDoc.getPlaceName()) + .address(cafeDoc.getAddressName()) + .lat(new BigDecimal(cafeDoc.getX())) + .lon(new BigDecimal(cafeDoc.getY())) + .phone(cafeDoc.getPhone()) + .kakaomapUrl(cafeDoc.getPlaceUrl()) + .categoryCode(cafeDoc.getCategoryGroupCode()) + .area(area) + .build(); + + cafeRepository.save(cafe); + } + } catch (Exception e) { + log.error("장소 ID {} 처리 중 오류: {}", area.getAreaId(), e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java new file mode 100644 index 0000000..9c4c053 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java @@ -0,0 +1,109 @@ +package com.example.placeservice.service; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.Collections; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class KakaoApiService { + + private final RestTemplate restTemplate; + + @Value("${kakao.api.key}") + private String kakaoApiKey; + + private static final String KAKAO_API_BASE_URL = "https://dapi.kakao.com/v2/local"; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class CafeResponse { + private Meta meta; + private List documents; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Meta { + @JsonProperty("total_count") + private int totalCount; + @JsonProperty("pageable_count") + private int pageableCount; + @JsonProperty("is_end") + private boolean isEnd; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Document { + @JsonProperty("id") + private String id; + @JsonProperty("place_name") + private String placeName; + @JsonProperty("category_name") + private String categoryName; + @JsonProperty("category_group_code") + private String categoryGroupCode; + @JsonProperty("category_group_name") + private String categoryGroupName; + @JsonProperty("phone") + private String phone; + @JsonProperty("address_name") + private String addressName; + @JsonProperty("road_address_name") + private String roadAddressName; + private String x; // 경도 + private String y; // 위도 + @JsonProperty("place_url") + private String placeUrl; + private String distance; + } + } + + // 위도/경도 기준으로 주변 카페 검색 + public List findCafesNearby(double latitude, double longitude, int radius) { + try { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "KakaoAK " + kakaoApiKey); + + String url = UriComponentsBuilder.fromHttpUrl(KAKAO_API_BASE_URL + "/search/category.json") + .queryParam("category_group_code", "CE7") // 카페 카테고리 코드 + .queryParam("x", longitude) + .queryParam("y", latitude) + .queryParam("radius", radius) // 반경(미터) + .queryParam("size", 15) // 최대 15개 결과 + .build() + .toUriString(); + + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + new HttpEntity<>(headers), + CafeResponse.class + ); + + CafeResponse result = response.getBody(); + if (result != null && result.getDocuments() != null) { + return result.getDocuments(); + } + + return Collections.emptyList(); + } catch (Exception e) { + log.error("카페 검색 중 오류: {}", e.getMessage(), e); + return Collections.emptyList(); + } + } +} \ No newline at end of file diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 5734ed4..439bd92 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -12,3 +12,9 @@ spring.jpa.defer-datasource-initialization=true spring.jpa.hibernate.ddl-auto=create spring.jpa.show-sql=true spring.sql.init.mode=always + +# ??? API ? ?? +kakao.api.key=585808366c299a6686e7104a193017bb + +# ?? ?? +logging.level.com.example.placeservice=INFO \ No newline at end of file diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index 5ad3979..6ca7d5d 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -1,4 +1,4 @@ -INSERT INTO area (seoul_id, name, name_eng, category, x, y) VALUES +INSERT INTO area (seoul_id, name, name_eng, category, lon, lat) VALUES ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), From 72d21e687e6d2d637bb45753156aa554fbcf6795 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 18 Apr 2025 13:40:43 +0900 Subject: [PATCH 033/361] =?UTF-8?q?Feat:CAFE=20DATA=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeservice/PlaceServiceApplication.java | 10 +- .../placeservice/config/SecurityConfig.java | 24 ++++ .../controller/CafeController.java | 101 +++++++++++++ .../com/example/placeservice/entity/Area.java | 23 ++- .../com/example/placeservice/entity/Cafe.java | 48 +++++++ .../repository/CafeRepository.java | 18 +++ .../placeservice/service/CafeService.java | 133 ++++++++++++++++++ .../placeservice/service/KakaoApiService.java | 109 ++++++++++++++ .../src/main/resources/application.properties | 6 + place-service/src/main/resources/data.sql | 2 +- 10 files changed, 468 insertions(+), 6 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java create mode 100644 place-service/src/main/java/com/example/placeservice/controller/CafeController.java create mode 100644 place-service/src/main/java/com/example/placeservice/entity/Cafe.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/CafeService.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java index 7aaef28..9a066dc 100644 --- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java +++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java @@ -2,12 +2,20 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.web.client.RestTemplate; @SpringBootApplication +@EnableAsync public class PlaceServiceApplication { public static void main(String[] args) { SpringApplication.run(PlaceServiceApplication.class, args); } -} + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java new file mode 100644 index 0000000..b562d3c --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java @@ -0,0 +1,24 @@ +package com.example.placeservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import static org.springframework.security.config.Customizer.withDefaults; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() // 모든 요청 허용 + ); + + return http.build(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java new file mode 100644 index 0000000..802b512 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java @@ -0,0 +1,101 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.service.CafeService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@RestController +@RequestMapping("/api/cafes") +@RequiredArgsConstructor +public class CafeController { + + private final CafeService cafeService; + + /** + * 모든 장소의 주변 카페 데이터 처리 + */ + @PostMapping("/process-all") + public ResponseEntity> processAllAreas() { + Map response = new HashMap<>(); + + try { + cafeService.processAllAreas(); + + response.put("success", true); + response.put("message", "모든 장소의 주변 카페 정보 처리가 시작되었습니다."); + + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("처리 중 오류 발생: {}", e.getMessage(), e); + + response.put("success", false); + response.put("message", "오류 발생: " + e.getMessage()); + + return ResponseEntity.badRequest().body(response); + } + } + + /** + * 특정 ID 목록의 장소만 처리 + */ + @PostMapping("/process-by-ids") + public ResponseEntity> processAreasByIds(@RequestBody Map> request) { + Map response = new HashMap<>(); + + try { + List areaIds = request.get("areaIds"); + if (areaIds == null || areaIds.isEmpty()) { + response.put("success", false); + response.put("message", "장소 ID 목록이 비어 있습니다."); + return ResponseEntity.badRequest().body(response); + } + + log.info("{}개 장소에 대한 처리 시작", areaIds.size()); + + cafeService.processAreasByIds(areaIds); + + response.put("success", true); + response.put("message", "지정한 장소의 주변 카페 정보 처리가 시작되었습니다."); + + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("처리 중 오류 발생: {}", e.getMessage(), e); + + response.put("success", false); + response.put("message", "오류 발생: " + e.getMessage()); + + return ResponseEntity.badRequest().body(response); + } + } + + /** + * 단일 장소 처리 + */ + @PostMapping("/process/{areaId}") + public ResponseEntity> processSingleArea(@PathVariable Long areaId) { + Map response = new HashMap<>(); + + try { + cafeService.processAreasByIds(List.of(areaId)); + + response.put("success", true); + response.put("message", "장소 ID " + areaId + "의 주변 카페 정보 처리가 시작되었습니다."); + + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error("처리 중 오류 발생: {}", e.getMessage(), e); + + response.put("success", false); + response.put("message", "오류 발생: " + e.getMessage()); + + return ResponseEntity.badRequest().body(response); + } + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index 6c14791..8292532 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -23,11 +23,26 @@ public class Area { @Column(name = "category", length = 20, nullable = false) private String category; // 구분 - @Column(name = "x", precision = 10, scale = 6, nullable = false) - private BigDecimal x; // 위도 + @Column(name = "lat", precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 - @Column(name = "y", precision = 10, scale = 6, nullable = false) - private BigDecimal y; // 경도 + @Column(name = "lon", precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 + public Object getName() { + return this.name; + } + + public Long getAreaId() { + return this.area_id; + } + + public Number getY() { + return this.lon; + } + + public Number getX() { + return this.lat; + } } diff --git a/place-service/src/main/java/com/example/placeservice/entity/Cafe.java b/place-service/src/main/java/com/example/placeservice/entity/Cafe.java new file mode 100644 index 0000000..2dffa74 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/Cafe.java @@ -0,0 +1,48 @@ +package com.example.placeservice.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Entity +@Table(name = "cafe") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Cafe { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "cafe_id") + private Long id; + + @Column(name = "name") + private String name; + + @Column(name = "address") + private String address; + + @Column(name = "lat", precision = 10, scale = 6) + private BigDecimal lat; // 위도 + + @Column(name = "lon", precision = 10, scale = 6) + private BigDecimal lon; // 경도 + + @Column(name = "phone") + private String phone; + + @Column(name = "kakaomap_url") + private String kakaomapUrl; + + @Column(name = "category_code") + private String categoryCode; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "area_id") + private Area area; +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java new file mode 100644 index 0000000..eb8e1eb --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java @@ -0,0 +1,18 @@ +package com.example.placeservice.repository; + +import com.example.placeservice.entity.Cafe; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.transaction.annotation.Transactional; +import java.util.List; + +public interface CafeRepository extends JpaRepository { + @Query("SELECT c FROM Cafe c WHERE c.area.area_id = :areaId") + List findByAreaId(@org.springframework.data.repository.query.Param("areaId") Long areaId); + + @Transactional + @Modifying + @Query("DELETE FROM Cafe c WHERE c.area.area_id = :areaId") + void deleteByAreaId(Long areaId); +} diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java new file mode 100644 index 0000000..8b2ade6 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java @@ -0,0 +1,133 @@ +package com.example.placeservice.service; + +import com.example.placeservice.entity.Area; +import com.example.placeservice.entity.Cafe; +import com.example.placeservice.repository.AreaRepository; +import com.example.placeservice.repository.CafeRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CafeService { + + private final AreaRepository areaRepository; + private final CafeRepository cafeRepository; + private final KakaoApiService kakaoApiService; + + /** + * 모든 장소에 대해 주변 카페 데이터 처리 + */ + @Async + public void processAllAreas() { + List areas = areaRepository.findAll(); + log.info("총 {}개 장소에 대한 카페 정보를 처리합니다.", areas.size()); + + for (int i = 0; i < areas.size(); i++) { + Area area = areas.get(i); + log.info("[{}/{}] '{}' (ID: {}) 처리 중...", + i + 1, areas.size(), area.getName(), area.getAreaId()); + + processSingleArea(area); + + // API 호출 간격 조절 + try { + Thread.sleep(300); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + log.info("모든 장소 처리 완료."); + } + + /** + * 특정 ID 목록의 장소만 처리 + */ + @Async + public void processAreasByIds(List areaIds) { + log.info("{}개 지정된 장소에 대한 처리를 시작합니다.", areaIds.size()); + + for (int i = 0; i < areaIds.size(); i++) { + Long areaId = areaIds.get(i); + Area area = areaRepository.findById(areaId).orElse(null); + + if (area == null) { + log.warn("ID {}인 장소를 찾을 수 없습니다.", areaId); + continue; + } + + log.info("[{}/{}] '{}' (ID: {}) 처리 중...", + i + 1, areaIds.size(), area.getName(), area.getAreaId()); + + processSingleArea(area); + + // API 호출 간격 조절 + try { + Thread.sleep(300); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + log.info("지정된 모든 장소 처리 완료."); + } + + /** + * 단일 장소의 주변 카페 정보 처리 + */ + @Transactional + public void processSingleArea(Area area) { + try { + // 해당 지역의 기존 카페 데이터 삭제 + cafeRepository.deleteByAreaId(area.getAreaId()); + + // 주변 카페 검색 + double latitude = area.getY().doubleValue(); + double longitude = area.getX().doubleValue(); + + if (latitude == 0 || longitude == 0) { + log.warn("장소 ID {}의 위도/경도 정보가 없습니다.", area.getAreaId()); + return; + } + + List cafes = + kakaoApiService.findCafesNearby(latitude, longitude, 1000); + + // "음식점 > 카페"가 포함된 카페만 필터링하고 "(휴업중)" 카페는 제외 + List filteredCafes = cafes.stream() + .filter(cafe -> cafe.getCategoryName() != null && cafe.getCategoryName().contains("음식점 > 카페")) + .filter(cafe -> cafe.getPlaceName() == null || !cafe.getPlaceName().contains("(휴업중)")) + .collect(Collectors.toList()); + + log.info("'{}' 주변 {}개의 카페 중 {}개의 '음식점 > 카페' 카테고리를 찾았습니다.", + area.getName(), cafes.size(), filteredCafes.size()); + + // 필터링된 카페 정보 저장 + for (KakaoApiService.CafeResponse.Document cafeDoc : filteredCafes) { + Cafe cafe = Cafe.builder() + .name(cafeDoc.getPlaceName()) + .address(cafeDoc.getAddressName()) + .lat(new BigDecimal(cafeDoc.getX())) + .lon(new BigDecimal(cafeDoc.getY())) + .phone(cafeDoc.getPhone()) + .kakaomapUrl(cafeDoc.getPlaceUrl()) + .categoryCode(cafeDoc.getCategoryGroupCode()) + .area(area) + .build(); + + cafeRepository.save(cafe); + } + } catch (Exception e) { + log.error("장소 ID {} 처리 중 오류: {}", area.getAreaId(), e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java new file mode 100644 index 0000000..9c4c053 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java @@ -0,0 +1,109 @@ +package com.example.placeservice.service; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.Collections; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class KakaoApiService { + + private final RestTemplate restTemplate; + + @Value("${kakao.api.key}") + private String kakaoApiKey; + + private static final String KAKAO_API_BASE_URL = "https://dapi.kakao.com/v2/local"; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class CafeResponse { + private Meta meta; + private List documents; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Meta { + @JsonProperty("total_count") + private int totalCount; + @JsonProperty("pageable_count") + private int pageableCount; + @JsonProperty("is_end") + private boolean isEnd; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Document { + @JsonProperty("id") + private String id; + @JsonProperty("place_name") + private String placeName; + @JsonProperty("category_name") + private String categoryName; + @JsonProperty("category_group_code") + private String categoryGroupCode; + @JsonProperty("category_group_name") + private String categoryGroupName; + @JsonProperty("phone") + private String phone; + @JsonProperty("address_name") + private String addressName; + @JsonProperty("road_address_name") + private String roadAddressName; + private String x; // 경도 + private String y; // 위도 + @JsonProperty("place_url") + private String placeUrl; + private String distance; + } + } + + // 위도/경도 기준으로 주변 카페 검색 + public List findCafesNearby(double latitude, double longitude, int radius) { + try { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "KakaoAK " + kakaoApiKey); + + String url = UriComponentsBuilder.fromHttpUrl(KAKAO_API_BASE_URL + "/search/category.json") + .queryParam("category_group_code", "CE7") // 카페 카테고리 코드 + .queryParam("x", longitude) + .queryParam("y", latitude) + .queryParam("radius", radius) // 반경(미터) + .queryParam("size", 15) // 최대 15개 결과 + .build() + .toUriString(); + + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + new HttpEntity<>(headers), + CafeResponse.class + ); + + CafeResponse result = response.getBody(); + if (result != null && result.getDocuments() != null) { + return result.getDocuments(); + } + + return Collections.emptyList(); + } catch (Exception e) { + log.error("카페 검색 중 오류: {}", e.getMessage(), e); + return Collections.emptyList(); + } + } +} \ No newline at end of file diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 5734ed4..439bd92 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -12,3 +12,9 @@ spring.jpa.defer-datasource-initialization=true spring.jpa.hibernate.ddl-auto=create spring.jpa.show-sql=true spring.sql.init.mode=always + +# ??? API ? ?? +kakao.api.key=585808366c299a6686e7104a193017bb + +# ?? ?? +logging.level.com.example.placeservice=INFO \ No newline at end of file diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index 5ad3979..6ca7d5d 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -1,4 +1,4 @@ -INSERT INTO area (seoul_id, name, name_eng, category, x, y) VALUES +INSERT INTO area (seoul_id, name, name_eng, category, lon, lat) VALUES ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), From 9342732e2039424d6a79c55df0d0a82c6d4ee49a Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 18 Apr 2025 14:14:52 +0900 Subject: [PATCH 034/361] =?UTF-8?q?Feat:CAFE=20DATA=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CafeController.java | 57 ------------------- 1 file changed, 57 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java index 802b512..88743fa 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java @@ -41,61 +41,4 @@ public ResponseEntity> processAllAreas() { return ResponseEntity.badRequest().body(response); } } - - /** - * 특정 ID 목록의 장소만 처리 - */ - @PostMapping("/process-by-ids") - public ResponseEntity> processAreasByIds(@RequestBody Map> request) { - Map response = new HashMap<>(); - - try { - List areaIds = request.get("areaIds"); - if (areaIds == null || areaIds.isEmpty()) { - response.put("success", false); - response.put("message", "장소 ID 목록이 비어 있습니다."); - return ResponseEntity.badRequest().body(response); - } - - log.info("{}개 장소에 대한 처리 시작", areaIds.size()); - - cafeService.processAreasByIds(areaIds); - - response.put("success", true); - response.put("message", "지정한 장소의 주변 카페 정보 처리가 시작되었습니다."); - - return ResponseEntity.ok(response); - } catch (Exception e) { - log.error("처리 중 오류 발생: {}", e.getMessage(), e); - - response.put("success", false); - response.put("message", "오류 발생: " + e.getMessage()); - - return ResponseEntity.badRequest().body(response); - } - } - - /** - * 단일 장소 처리 - */ - @PostMapping("/process/{areaId}") - public ResponseEntity> processSingleArea(@PathVariable Long areaId) { - Map response = new HashMap<>(); - - try { - cafeService.processAreasByIds(List.of(areaId)); - - response.put("success", true); - response.put("message", "장소 ID " + areaId + "의 주변 카페 정보 처리가 시작되었습니다."); - - return ResponseEntity.ok(response); - } catch (Exception e) { - log.error("처리 중 오류 발생: {}", e.getMessage(), e); - - response.put("success", false); - response.put("message", "오류 발생: " + e.getMessage()); - - return ResponseEntity.badRequest().body(response); - } - } } \ No newline at end of file From aa4bdc641b51faa575c5249ff09b8031c76baa4f Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 18 Apr 2025 14:14:52 +0900 Subject: [PATCH 035/361] =?UTF-8?q?Feat:CAFE=20DATA=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CafeController.java | 57 ------------------- 1 file changed, 57 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java index 802b512..88743fa 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java @@ -41,61 +41,4 @@ public ResponseEntity> processAllAreas() { return ResponseEntity.badRequest().body(response); } } - - /** - * 특정 ID 목록의 장소만 처리 - */ - @PostMapping("/process-by-ids") - public ResponseEntity> processAreasByIds(@RequestBody Map> request) { - Map response = new HashMap<>(); - - try { - List areaIds = request.get("areaIds"); - if (areaIds == null || areaIds.isEmpty()) { - response.put("success", false); - response.put("message", "장소 ID 목록이 비어 있습니다."); - return ResponseEntity.badRequest().body(response); - } - - log.info("{}개 장소에 대한 처리 시작", areaIds.size()); - - cafeService.processAreasByIds(areaIds); - - response.put("success", true); - response.put("message", "지정한 장소의 주변 카페 정보 처리가 시작되었습니다."); - - return ResponseEntity.ok(response); - } catch (Exception e) { - log.error("처리 중 오류 발생: {}", e.getMessage(), e); - - response.put("success", false); - response.put("message", "오류 발생: " + e.getMessage()); - - return ResponseEntity.badRequest().body(response); - } - } - - /** - * 단일 장소 처리 - */ - @PostMapping("/process/{areaId}") - public ResponseEntity> processSingleArea(@PathVariable Long areaId) { - Map response = new HashMap<>(); - - try { - cafeService.processAreasByIds(List.of(areaId)); - - response.put("success", true); - response.put("message", "장소 ID " + areaId + "의 주변 카페 정보 처리가 시작되었습니다."); - - return ResponseEntity.ok(response); - } catch (Exception e) { - log.error("처리 중 오류 발생: {}", e.getMessage(), e); - - response.put("success", false); - response.put("message", "오류 발생: " + e.getMessage()); - - return ResponseEntity.badRequest().body(response); - } - } } \ No newline at end of file From fac4bf6756542934236f4bb133b259fe6ba23fc6 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 18 Apr 2025 14:15:49 +0900 Subject: [PATCH 036/361] =?UTF-8?q?Feat:CAFE=20DATA=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a98003..577f366 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,20 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update - ``` \ No newline at end of file + ``` + +### CAFE DATA +1. postman에서 설정 + - Method: POST + - URL: http://localhost:8080/api/cafes/process-all + - Auth: Basic Auth + - Username: user + - Password: docker에서 비번 확인 + +2. 데이터 확인 + - docker 또는 터미널에서 확인 + ``` + docker exec -it my_postgres bash + psql -U root -d stars_db(docker시 여기부터) + select * from cafe; + ``` \ No newline at end of file From 78481b619c45a65e598a1f75d7ec3bbe59807f75 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 18 Apr 2025 14:15:49 +0900 Subject: [PATCH 037/361] =?UTF-8?q?Feat:CAFE=20DATA=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a98003..577f366 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,20 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update - ``` \ No newline at end of file + ``` + +### CAFE DATA +1. postman에서 설정 + - Method: POST + - URL: http://localhost:8080/api/cafes/process-all + - Auth: Basic Auth + - Username: user + - Password: docker에서 비번 확인 + +2. 데이터 확인 + - docker 또는 터미널에서 확인 + ``` + docker exec -it my_postgres bash + psql -U root -d stars_db(docker시 여기부터) + select * from cafe; + ``` \ No newline at end of file From e4f608b088556ec1d97ab5bd54b30909eb026a4e Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 18 Apr 2025 14:34:07 +0900 Subject: [PATCH 038/361] =?UTF-8?q?Feat:=20=EA=B4=80=EA=B4=91=EC=A7=80=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20place-cod?= =?UTF-8?q?e=20=EA=B8=B0=EB=B0=98=20=EA=B4=80=EA=B4=91=EC=A7=80=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20API=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 --- .../placeservice/config/SecurityConfig.java | 24 ++++++++ .../controller/AttractionController.java | 28 ++++++++++ .../placeservice/dto/AreaAttractionsDto.java | 12 ++++ .../placeservice/dto/AttractionInfoDto.java | 33 +++++++++++ .../placeservice/dto/AttractionListDto.java | 23 ++++++++ .../com/example/placeservice/entity/Area.java | 5 ++ .../placeservice/entity/Attraction.java | 5 +- .../placeservice/init/AttractionDataInit.java | 1 + .../repository/AttractionRepository.java | 3 + .../service/AttractionService.java | 55 +++++++++++++++++++ .../src/main/resources/application.properties | 3 +- 11 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java diff --git a/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java new file mode 100644 index 0000000..8fe1402 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java @@ -0,0 +1,24 @@ +package com.example.placeservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + // spring-security 임시 block + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() // 모든 요청 허용 + ); + + return http.build(); + } + + +} diff --git a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java index f433c39..b508e6e 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java @@ -1,4 +1,32 @@ package com.example.placeservice.controller; +import com.example.placeservice.dto.AreaAttractionsDto; +import com.example.placeservice.dto.AttractionInfoDto; +import com.example.placeservice.service.AttractionService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/main") +@RequiredArgsConstructor public class AttractionController { + + private final AttractionService attractionService; + + // 관광지 목록 조회 + @GetMapping("/attraction") + public List getAttraction() { + return attractionService.getAttractionData(); + } + + // 관광지 정보 조회(place-code) + @GetMapping("info/attraction/{place-code}") + public AttractionInfoDto getAttractionInfo(@PathVariable("place-code") long placeCode) { + return attractionService.getAttractionInfoData(placeCode); + } } diff --git a/place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java b/place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java new file mode 100644 index 0000000..b79c097 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java @@ -0,0 +1,12 @@ +package com.example.placeservice.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class AreaAttractionsDto { + private String area_name; // Area 이름 + private List attraction_list; + +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java b/place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java new file mode 100644 index 0000000..91a0464 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java @@ -0,0 +1,33 @@ +package com.example.placeservice.dto; + +import com.example.placeservice.entity.Attraction; +import jakarta.persistence.Column; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class AttractionInfoDto { + private Long attraction_id; + private String name; + private String address; + private BigDecimal lat; + private BigDecimal lon; + private String phone; + private String homepage_url; + private String close_day; + private String use_time; + + public AttractionInfoDto(Long attractionId, String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time) { + this.attraction_id = attractionId; + this.name = name; + this.address = address; + this.lat = lat; + this.lon = lon; + this.phone = phone; + this.homepage_url = homepage_url; + this.close_day = close_day; + this.use_time = use_time; + } + +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java b/place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java new file mode 100644 index 0000000..516636e --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java @@ -0,0 +1,23 @@ +package com.example.placeservice.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class AttractionListDto { + private Long attraction_id; + private String name; + private String address; + private BigDecimal lat; + private BigDecimal lon; + + + public AttractionListDto(Long attraction_id, String name, String address, BigDecimal lat, BigDecimal lon) { + this.attraction_id = attraction_id; + this.name = name; + this.address = address; + this.lat = lat; + this.lon = lon; + } +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index 1f4468f..70a0b04 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -1,9 +1,11 @@ package com.example.placeservice.entity; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.Data; import java.math.BigDecimal; +import java.util.List; @Data @Entity @@ -32,4 +34,7 @@ public class Area { private BigDecimal lon; // 경도 + @OneToMany(mappedBy = "area", fetch = FetchType.LAZY) + @JsonIgnore + private List attractions; } diff --git a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java index e8ec27c..e8a22a1 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java @@ -1,5 +1,6 @@ package com.example.placeservice.entity; +import com.fasterxml.jackson.annotation.JsonBackReference; import jakarta.persistence.*; import lombok.Data; @@ -41,8 +42,8 @@ public class Attraction { @Column(name="use_time", columnDefinition = "TEXT") private String use_time; - // 👇 추가: ManyToOne 관계 매핑 - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "area_id", nullable = false) private Area area; + } diff --git a/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java b/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java index 27d9dd1..22d1826 100644 --- a/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java +++ b/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java @@ -4,6 +4,7 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; +// SB 실행 시, attraction data 저장(area 데이터가 먼저 저장되어 있어야 함) @Component public class AttractionDataInit implements CommandLineRunner { diff --git a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java index 3050bf3..2374e53 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java @@ -3,6 +3,9 @@ import com.example.placeservice.entity.Attraction; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface AttractionRepository extends JpaRepository { boolean existsByVisitId(String visitId); + Optional findById(Long id); } diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index 1ae444f..3c3b78f 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -1,6 +1,9 @@ package com.example.placeservice.service; +import com.example.placeservice.dto.AreaAttractionsDto; import com.example.placeservice.dto.AttractionDto; +import com.example.placeservice.dto.AttractionInfoDto; +import com.example.placeservice.dto.AttractionListDto; import com.example.placeservice.entity.Area; import com.example.placeservice.entity.Attraction; import com.example.placeservice.repository.AreaRepository; @@ -8,9 +11,11 @@ import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.Unmarshaller; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import org.springframework.web.server.ResponseStatusException; import java.io.StringReader; import java.math.BigDecimal; @@ -98,4 +103,54 @@ private Area findAreaByCondition(AttractionDto.AttractionTable table, List // System.out.println(table.getTitle()+"위도 :"+table.getMapX()+"경도 :"+table.getMapY() ); return null; } + + // attraction 목록 DB 불러오기 + public List getAttractionData() { + try { + List areas = areaRepository.findAll(); + + return areas.stream() + .map(area -> { + List attractions = area.getAttractions().stream() + .map(attraction -> new AttractionListDto( + attraction.getAttraction_id(), + attraction.getName(), + attraction.getAddress(), + attraction.getLat(), + attraction.getLon() + )).toList(); + + AreaAttractionsDto dto = new AreaAttractionsDto(); + dto.setArea_name(area.getName()); + dto.setAttraction_list(attractions); + return dto; + }) + .toList(); + + } catch (RuntimeException e) { + throw new RuntimeException("예상치 못한 오류",e); + } + } + + public AttractionInfoDto getAttractionInfoData(long placeCode) { + try{ + Attraction attraction = attractionRepository.findById(placeCode) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "해당 관광지 정보를 찾을 수 없습니다.")); + + return new AttractionInfoDto( + attraction.getAttraction_id(), + attraction.getName(), + attraction.getAddress(), + attraction.getLat(), + attraction.getLon(), + attraction.getPhone(), + attraction.getHomepage_url(), + attraction.getClose_day(), + attraction.getUse_time() + ); + + } catch (RuntimeException e) { + throw new RuntimeException("예상치 못한 오류",e); + } + } } diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 0b6449d..02c04c8 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -1,7 +1,5 @@ spring.application.name=place-service -server.port=8081 - # postgreSQL ?? spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db spring.datasource.username=root @@ -12,5 +10,6 @@ spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update spring.jpa.defer-datasource-initialization=true +# Hibernate SQL ?? ??? spring.jpa.show-sql=true spring.sql.init.mode=always From fb1476acf9df529e44ec2921d647b75ebc09c25b Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 18 Apr 2025 14:34:07 +0900 Subject: [PATCH 039/361] =?UTF-8?q?Feat:=20=EA=B4=80=EA=B4=91=EC=A7=80=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20place-cod?= =?UTF-8?q?e=20=EA=B8=B0=EB=B0=98=20=EA=B4=80=EA=B4=91=EC=A7=80=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20API=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 --- .../placeservice/config/SecurityConfig.java | 24 ++++++++ .../controller/AttractionController.java | 28 ++++++++++ .../placeservice/dto/AreaAttractionsDto.java | 12 ++++ .../placeservice/dto/AttractionInfoDto.java | 33 +++++++++++ .../placeservice/dto/AttractionListDto.java | 23 ++++++++ .../com/example/placeservice/entity/Area.java | 5 ++ .../placeservice/entity/Attraction.java | 5 +- .../placeservice/init/AttractionDataInit.java | 1 + .../repository/AttractionRepository.java | 3 + .../service/AttractionService.java | 55 +++++++++++++++++++ .../src/main/resources/application.properties | 3 +- 11 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java diff --git a/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java new file mode 100644 index 0000000..8fe1402 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java @@ -0,0 +1,24 @@ +package com.example.placeservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + // spring-security 임시 block + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() // 모든 요청 허용 + ); + + return http.build(); + } + + +} diff --git a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java index f433c39..b508e6e 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java @@ -1,4 +1,32 @@ package com.example.placeservice.controller; +import com.example.placeservice.dto.AreaAttractionsDto; +import com.example.placeservice.dto.AttractionInfoDto; +import com.example.placeservice.service.AttractionService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/main") +@RequiredArgsConstructor public class AttractionController { + + private final AttractionService attractionService; + + // 관광지 목록 조회 + @GetMapping("/attraction") + public List getAttraction() { + return attractionService.getAttractionData(); + } + + // 관광지 정보 조회(place-code) + @GetMapping("info/attraction/{place-code}") + public AttractionInfoDto getAttractionInfo(@PathVariable("place-code") long placeCode) { + return attractionService.getAttractionInfoData(placeCode); + } } diff --git a/place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java b/place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java new file mode 100644 index 0000000..b79c097 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java @@ -0,0 +1,12 @@ +package com.example.placeservice.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class AreaAttractionsDto { + private String area_name; // Area 이름 + private List attraction_list; + +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java b/place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java new file mode 100644 index 0000000..91a0464 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java @@ -0,0 +1,33 @@ +package com.example.placeservice.dto; + +import com.example.placeservice.entity.Attraction; +import jakarta.persistence.Column; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class AttractionInfoDto { + private Long attraction_id; + private String name; + private String address; + private BigDecimal lat; + private BigDecimal lon; + private String phone; + private String homepage_url; + private String close_day; + private String use_time; + + public AttractionInfoDto(Long attractionId, String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time) { + this.attraction_id = attractionId; + this.name = name; + this.address = address; + this.lat = lat; + this.lon = lon; + this.phone = phone; + this.homepage_url = homepage_url; + this.close_day = close_day; + this.use_time = use_time; + } + +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java b/place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java new file mode 100644 index 0000000..516636e --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java @@ -0,0 +1,23 @@ +package com.example.placeservice.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class AttractionListDto { + private Long attraction_id; + private String name; + private String address; + private BigDecimal lat; + private BigDecimal lon; + + + public AttractionListDto(Long attraction_id, String name, String address, BigDecimal lat, BigDecimal lon) { + this.attraction_id = attraction_id; + this.name = name; + this.address = address; + this.lat = lat; + this.lon = lon; + } +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index 1f4468f..70a0b04 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -1,9 +1,11 @@ package com.example.placeservice.entity; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.Data; import java.math.BigDecimal; +import java.util.List; @Data @Entity @@ -32,4 +34,7 @@ public class Area { private BigDecimal lon; // 경도 + @OneToMany(mappedBy = "area", fetch = FetchType.LAZY) + @JsonIgnore + private List attractions; } diff --git a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java index e8ec27c..e8a22a1 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java @@ -1,5 +1,6 @@ package com.example.placeservice.entity; +import com.fasterxml.jackson.annotation.JsonBackReference; import jakarta.persistence.*; import lombok.Data; @@ -41,8 +42,8 @@ public class Attraction { @Column(name="use_time", columnDefinition = "TEXT") private String use_time; - // 👇 추가: ManyToOne 관계 매핑 - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "area_id", nullable = false) private Area area; + } diff --git a/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java b/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java index 27d9dd1..22d1826 100644 --- a/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java +++ b/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java @@ -4,6 +4,7 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; +// SB 실행 시, attraction data 저장(area 데이터가 먼저 저장되어 있어야 함) @Component public class AttractionDataInit implements CommandLineRunner { diff --git a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java index 3050bf3..2374e53 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java @@ -3,6 +3,9 @@ import com.example.placeservice.entity.Attraction; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface AttractionRepository extends JpaRepository { boolean existsByVisitId(String visitId); + Optional findById(Long id); } diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index 1ae444f..3c3b78f 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -1,6 +1,9 @@ package com.example.placeservice.service; +import com.example.placeservice.dto.AreaAttractionsDto; import com.example.placeservice.dto.AttractionDto; +import com.example.placeservice.dto.AttractionInfoDto; +import com.example.placeservice.dto.AttractionListDto; import com.example.placeservice.entity.Area; import com.example.placeservice.entity.Attraction; import com.example.placeservice.repository.AreaRepository; @@ -8,9 +11,11 @@ import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.Unmarshaller; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import org.springframework.web.server.ResponseStatusException; import java.io.StringReader; import java.math.BigDecimal; @@ -98,4 +103,54 @@ private Area findAreaByCondition(AttractionDto.AttractionTable table, List // System.out.println(table.getTitle()+"위도 :"+table.getMapX()+"경도 :"+table.getMapY() ); return null; } + + // attraction 목록 DB 불러오기 + public List getAttractionData() { + try { + List areas = areaRepository.findAll(); + + return areas.stream() + .map(area -> { + List attractions = area.getAttractions().stream() + .map(attraction -> new AttractionListDto( + attraction.getAttraction_id(), + attraction.getName(), + attraction.getAddress(), + attraction.getLat(), + attraction.getLon() + )).toList(); + + AreaAttractionsDto dto = new AreaAttractionsDto(); + dto.setArea_name(area.getName()); + dto.setAttraction_list(attractions); + return dto; + }) + .toList(); + + } catch (RuntimeException e) { + throw new RuntimeException("예상치 못한 오류",e); + } + } + + public AttractionInfoDto getAttractionInfoData(long placeCode) { + try{ + Attraction attraction = attractionRepository.findById(placeCode) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "해당 관광지 정보를 찾을 수 없습니다.")); + + return new AttractionInfoDto( + attraction.getAttraction_id(), + attraction.getName(), + attraction.getAddress(), + attraction.getLat(), + attraction.getLon(), + attraction.getPhone(), + attraction.getHomepage_url(), + attraction.getClose_day(), + attraction.getUse_time() + ); + + } catch (RuntimeException e) { + throw new RuntimeException("예상치 못한 오류",e); + } + } } diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 0b6449d..02c04c8 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -1,7 +1,5 @@ spring.application.name=place-service -server.port=8081 - # postgreSQL ?? spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db spring.datasource.username=root @@ -12,5 +10,6 @@ spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update spring.jpa.defer-datasource-initialization=true +# Hibernate SQL ?? ??? spring.jpa.show-sql=true spring.sql.init.mode=always From 07f58499e807994c244bbb19c794f5e162c7fad7 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 18 Apr 2025 14:34:07 +0900 Subject: [PATCH 040/361] =?UTF-8?q?Feat:=20=EA=B4=80=EA=B4=91=EC=A7=80=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20place-cod?= =?UTF-8?q?e=20=EA=B8=B0=EB=B0=98=20=EA=B4=80=EA=B4=91=EC=A7=80=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20API=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 --- .../placeservice/config/SecurityConfig.java | 24 ++++++++ .../controller/AttractionController.java | 28 ++++++++++ .../placeservice/dto/AreaAttractionsDto.java | 12 ++++ .../placeservice/dto/AttractionInfoDto.java | 33 +++++++++++ .../placeservice/dto/AttractionListDto.java | 23 ++++++++ .../com/example/placeservice/entity/Area.java | 5 ++ .../placeservice/entity/Attraction.java | 5 +- .../placeservice/init/AttractionDataInit.java | 1 + .../repository/AttractionRepository.java | 3 + .../service/AttractionService.java | 55 +++++++++++++++++++ .../src/main/resources/application.properties | 3 +- 11 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java diff --git a/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java new file mode 100644 index 0000000..8fe1402 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java @@ -0,0 +1,24 @@ +package com.example.placeservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + // spring-security 임시 block + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() // 모든 요청 허용 + ); + + return http.build(); + } + + +} diff --git a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java index f433c39..b508e6e 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java @@ -1,4 +1,32 @@ package com.example.placeservice.controller; +import com.example.placeservice.dto.AreaAttractionsDto; +import com.example.placeservice.dto.AttractionInfoDto; +import com.example.placeservice.service.AttractionService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/main") +@RequiredArgsConstructor public class AttractionController { + + private final AttractionService attractionService; + + // 관광지 목록 조회 + @GetMapping("/attraction") + public List getAttraction() { + return attractionService.getAttractionData(); + } + + // 관광지 정보 조회(place-code) + @GetMapping("info/attraction/{place-code}") + public AttractionInfoDto getAttractionInfo(@PathVariable("place-code") long placeCode) { + return attractionService.getAttractionInfoData(placeCode); + } } diff --git a/place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java b/place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java new file mode 100644 index 0000000..b79c097 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java @@ -0,0 +1,12 @@ +package com.example.placeservice.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class AreaAttractionsDto { + private String area_name; // Area 이름 + private List attraction_list; + +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java b/place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java new file mode 100644 index 0000000..91a0464 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java @@ -0,0 +1,33 @@ +package com.example.placeservice.dto; + +import com.example.placeservice.entity.Attraction; +import jakarta.persistence.Column; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class AttractionInfoDto { + private Long attraction_id; + private String name; + private String address; + private BigDecimal lat; + private BigDecimal lon; + private String phone; + private String homepage_url; + private String close_day; + private String use_time; + + public AttractionInfoDto(Long attractionId, String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time) { + this.attraction_id = attractionId; + this.name = name; + this.address = address; + this.lat = lat; + this.lon = lon; + this.phone = phone; + this.homepage_url = homepage_url; + this.close_day = close_day; + this.use_time = use_time; + } + +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java b/place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java new file mode 100644 index 0000000..516636e --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java @@ -0,0 +1,23 @@ +package com.example.placeservice.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class AttractionListDto { + private Long attraction_id; + private String name; + private String address; + private BigDecimal lat; + private BigDecimal lon; + + + public AttractionListDto(Long attraction_id, String name, String address, BigDecimal lat, BigDecimal lon) { + this.attraction_id = attraction_id; + this.name = name; + this.address = address; + this.lat = lat; + this.lon = lon; + } +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index 1f4468f..70a0b04 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -1,9 +1,11 @@ package com.example.placeservice.entity; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.Data; import java.math.BigDecimal; +import java.util.List; @Data @Entity @@ -32,4 +34,7 @@ public class Area { private BigDecimal lon; // 경도 + @OneToMany(mappedBy = "area", fetch = FetchType.LAZY) + @JsonIgnore + private List attractions; } diff --git a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java index e8ec27c..e8a22a1 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java @@ -1,5 +1,6 @@ package com.example.placeservice.entity; +import com.fasterxml.jackson.annotation.JsonBackReference; import jakarta.persistence.*; import lombok.Data; @@ -41,8 +42,8 @@ public class Attraction { @Column(name="use_time", columnDefinition = "TEXT") private String use_time; - // 👇 추가: ManyToOne 관계 매핑 - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "area_id", nullable = false) private Area area; + } diff --git a/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java b/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java index 27d9dd1..22d1826 100644 --- a/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java +++ b/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java @@ -4,6 +4,7 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; +// SB 실행 시, attraction data 저장(area 데이터가 먼저 저장되어 있어야 함) @Component public class AttractionDataInit implements CommandLineRunner { diff --git a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java index 3050bf3..2374e53 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java @@ -3,6 +3,9 @@ import com.example.placeservice.entity.Attraction; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface AttractionRepository extends JpaRepository { boolean existsByVisitId(String visitId); + Optional findById(Long id); } diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index 1ae444f..3c3b78f 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -1,6 +1,9 @@ package com.example.placeservice.service; +import com.example.placeservice.dto.AreaAttractionsDto; import com.example.placeservice.dto.AttractionDto; +import com.example.placeservice.dto.AttractionInfoDto; +import com.example.placeservice.dto.AttractionListDto; import com.example.placeservice.entity.Area; import com.example.placeservice.entity.Attraction; import com.example.placeservice.repository.AreaRepository; @@ -8,9 +11,11 @@ import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.Unmarshaller; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import org.springframework.web.server.ResponseStatusException; import java.io.StringReader; import java.math.BigDecimal; @@ -98,4 +103,54 @@ private Area findAreaByCondition(AttractionDto.AttractionTable table, List // System.out.println(table.getTitle()+"위도 :"+table.getMapX()+"경도 :"+table.getMapY() ); return null; } + + // attraction 목록 DB 불러오기 + public List getAttractionData() { + try { + List areas = areaRepository.findAll(); + + return areas.stream() + .map(area -> { + List attractions = area.getAttractions().stream() + .map(attraction -> new AttractionListDto( + attraction.getAttraction_id(), + attraction.getName(), + attraction.getAddress(), + attraction.getLat(), + attraction.getLon() + )).toList(); + + AreaAttractionsDto dto = new AreaAttractionsDto(); + dto.setArea_name(area.getName()); + dto.setAttraction_list(attractions); + return dto; + }) + .toList(); + + } catch (RuntimeException e) { + throw new RuntimeException("예상치 못한 오류",e); + } + } + + public AttractionInfoDto getAttractionInfoData(long placeCode) { + try{ + Attraction attraction = attractionRepository.findById(placeCode) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "해당 관광지 정보를 찾을 수 없습니다.")); + + return new AttractionInfoDto( + attraction.getAttraction_id(), + attraction.getName(), + attraction.getAddress(), + attraction.getLat(), + attraction.getLon(), + attraction.getPhone(), + attraction.getHomepage_url(), + attraction.getClose_day(), + attraction.getUse_time() + ); + + } catch (RuntimeException e) { + throw new RuntimeException("예상치 못한 오류",e); + } + } } diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 0b6449d..02c04c8 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -1,7 +1,5 @@ spring.application.name=place-service -server.port=8081 - # postgreSQL ?? spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db spring.datasource.username=root @@ -12,5 +10,6 @@ spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update spring.jpa.defer-datasource-initialization=true +# Hibernate SQL ?? ??? spring.jpa.show-sql=true spring.sql.init.mode=always From 8f036dfbea56d71641e1e0ab7692ff0c03a043c1 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 18 Apr 2025 14:40:40 +0900 Subject: [PATCH 041/361] =?UTF-8?q?Feat:CAFE=20DATA=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeservice/service/CafeService.java | 2 +- .../placeservice/service/KakaoApiService.java | 83 +++++++++++++------ 2 files changed, 60 insertions(+), 25 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java index 8b2ade6..7c128cf 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CafeService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java @@ -108,7 +108,7 @@ public void processSingleArea(Area area) { .filter(cafe -> cafe.getPlaceName() == null || !cafe.getPlaceName().contains("(휴업중)")) .collect(Collectors.toList()); - log.info("'{}' 주변 {}개의 카페 중 {}개의 '음식점 > 카페' 카테고리를 찾았습니다.", + log.info("'{}' 주변 총 {}개의 카페 중 {}개의 '음식점 > 카페' 카테고리(휴업중 제외)를 찾았습니다.", area.getName(), cafes.size(), filteredCafes.size()); // 필터링된 카페 정보 저장 diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java index 9c4c053..7f028a2 100644 --- a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java @@ -14,6 +14,7 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -73,34 +74,68 @@ public static class Document { } } - // 위도/경도 기준으로 주변 카페 검색 + // 위도/경도 기준으로 주변 카페 검색 (최대 15개만) public List findCafesNearby(double latitude, double longitude, int radius) { + List allCafes = new ArrayList<>(); + boolean isEnd = false; + int page = 1; + int maxResults = 15; // 최대 15개만 가져오기 + try { - HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", "KakaoAK " + kakaoApiKey); - - String url = UriComponentsBuilder.fromHttpUrl(KAKAO_API_BASE_URL + "/search/category.json") - .queryParam("category_group_code", "CE7") // 카페 카테고리 코드 - .queryParam("x", longitude) - .queryParam("y", latitude) - .queryParam("radius", radius) // 반경(미터) - .queryParam("size", 15) // 최대 15개 결과 - .build() - .toUriString(); - - ResponseEntity response = restTemplate.exchange( - url, - HttpMethod.GET, - new HttpEntity<>(headers), - CafeResponse.class - ); - - CafeResponse result = response.getBody(); - if (result != null && result.getDocuments() != null) { - return result.getDocuments(); + // 페이지별로 반복 요청하되, 총 15개가 모이면 종료 + while (!isEnd && allCafes.size() < maxResults) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "KakaoAK " + kakaoApiKey); + + // 페이지당 결과 개수는 한번에 최대한 많이 가져오도록 15로 설정 + String url = UriComponentsBuilder.fromHttpUrl(KAKAO_API_BASE_URL + "/search/category.json") + .queryParam("category_group_code", "CE7") // 카페 카테고리 코드 + .queryParam("x", longitude) + .queryParam("y", latitude) + .queryParam("radius", radius) // 반경(미터) + .queryParam("page", page) // 페이지 번호 + .queryParam("size", 15) // 페이지당 15개 결과 + .queryParam("sort", "accuracy") // 정확도 순 정렬 + .build() + .toUriString(); + + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + new HttpEntity<>(headers), + CafeResponse.class + ); + + CafeResponse result = response.getBody(); + if (result != null && result.getDocuments() != null && !result.getDocuments().isEmpty()) { + // 최대 15개까지만 추가 + int remainingSpace = maxResults - allCafes.size(); + int toAdd = Math.min(remainingSpace, result.getDocuments().size()); + + allCafes.addAll(result.getDocuments().subList(0, toAdd)); + + log.info("카페 검색 - 페이지 {}: {}개 데이터 추가 (현재 총 {}개/최대 {}개)", + page, toAdd, allCafes.size(), maxResults); + + // 다음 페이지 존재 여부 확인 또는 이미 15개를 채웠는지 확인 + isEnd = result.getMeta().isEnd() || allCafes.size() >= maxResults; + page++; + + // API 호출 간격 조절 + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } else { + // 결과가 없으면 종료 + isEnd = true; + } } - return Collections.emptyList(); + log.info("총 {}개의 카페 데이터 조회 완료", allCafes.size()); + return allCafes; + } catch (Exception e) { log.error("카페 검색 중 오류: {}", e.getMessage(), e); return Collections.emptyList(); From 7e85182524c6eebb45b5a99a894e24738a69a525 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 18 Apr 2025 14:40:40 +0900 Subject: [PATCH 042/361] =?UTF-8?q?Feat:CAFE=20DATA=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeservice/service/CafeService.java | 2 +- .../placeservice/service/KakaoApiService.java | 83 +++++++++++++------ 2 files changed, 60 insertions(+), 25 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java index 8b2ade6..7c128cf 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CafeService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java @@ -108,7 +108,7 @@ public void processSingleArea(Area area) { .filter(cafe -> cafe.getPlaceName() == null || !cafe.getPlaceName().contains("(휴업중)")) .collect(Collectors.toList()); - log.info("'{}' 주변 {}개의 카페 중 {}개의 '음식점 > 카페' 카테고리를 찾았습니다.", + log.info("'{}' 주변 총 {}개의 카페 중 {}개의 '음식점 > 카페' 카테고리(휴업중 제외)를 찾았습니다.", area.getName(), cafes.size(), filteredCafes.size()); // 필터링된 카페 정보 저장 diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java index 9c4c053..7f028a2 100644 --- a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java @@ -14,6 +14,7 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -73,34 +74,68 @@ public static class Document { } } - // 위도/경도 기준으로 주변 카페 검색 + // 위도/경도 기준으로 주변 카페 검색 (최대 15개만) public List findCafesNearby(double latitude, double longitude, int radius) { + List allCafes = new ArrayList<>(); + boolean isEnd = false; + int page = 1; + int maxResults = 15; // 최대 15개만 가져오기 + try { - HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", "KakaoAK " + kakaoApiKey); - - String url = UriComponentsBuilder.fromHttpUrl(KAKAO_API_BASE_URL + "/search/category.json") - .queryParam("category_group_code", "CE7") // 카페 카테고리 코드 - .queryParam("x", longitude) - .queryParam("y", latitude) - .queryParam("radius", radius) // 반경(미터) - .queryParam("size", 15) // 최대 15개 결과 - .build() - .toUriString(); - - ResponseEntity response = restTemplate.exchange( - url, - HttpMethod.GET, - new HttpEntity<>(headers), - CafeResponse.class - ); - - CafeResponse result = response.getBody(); - if (result != null && result.getDocuments() != null) { - return result.getDocuments(); + // 페이지별로 반복 요청하되, 총 15개가 모이면 종료 + while (!isEnd && allCafes.size() < maxResults) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "KakaoAK " + kakaoApiKey); + + // 페이지당 결과 개수는 한번에 최대한 많이 가져오도록 15로 설정 + String url = UriComponentsBuilder.fromHttpUrl(KAKAO_API_BASE_URL + "/search/category.json") + .queryParam("category_group_code", "CE7") // 카페 카테고리 코드 + .queryParam("x", longitude) + .queryParam("y", latitude) + .queryParam("radius", radius) // 반경(미터) + .queryParam("page", page) // 페이지 번호 + .queryParam("size", 15) // 페이지당 15개 결과 + .queryParam("sort", "accuracy") // 정확도 순 정렬 + .build() + .toUriString(); + + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + new HttpEntity<>(headers), + CafeResponse.class + ); + + CafeResponse result = response.getBody(); + if (result != null && result.getDocuments() != null && !result.getDocuments().isEmpty()) { + // 최대 15개까지만 추가 + int remainingSpace = maxResults - allCafes.size(); + int toAdd = Math.min(remainingSpace, result.getDocuments().size()); + + allCafes.addAll(result.getDocuments().subList(0, toAdd)); + + log.info("카페 검색 - 페이지 {}: {}개 데이터 추가 (현재 총 {}개/최대 {}개)", + page, toAdd, allCafes.size(), maxResults); + + // 다음 페이지 존재 여부 확인 또는 이미 15개를 채웠는지 확인 + isEnd = result.getMeta().isEnd() || allCafes.size() >= maxResults; + page++; + + // API 호출 간격 조절 + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } else { + // 결과가 없으면 종료 + isEnd = true; + } } - return Collections.emptyList(); + log.info("총 {}개의 카페 데이터 조회 완료", allCafes.size()); + return allCafes; + } catch (Exception e) { log.error("카페 검색 중 오류: {}", e.getMessage(), e); return Collections.emptyList(); From c991b93011c06ec56b8fbcb985ceb0d86b4cca8d Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 18 Apr 2025 15:07:23 +0900 Subject: [PATCH 043/361] =?UTF-8?q?Fix:=20=EB=8F=84=EB=A1=9C=EC=86=8C?= =?UTF-8?q?=ED=86=B5=20API=20=EC=B6=9C=EB=A0=A5=20=EB=82=B4=EC=9A=A9=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- congestion-service/build.gradle | 6 ++++++ .../src/main/resources/application.properties | 14 ++++++++++++++ .../example/externalinfoservice/dto/RoadDto.java | 6 +++--- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/congestion-service/build.gradle b/congestion-service/build.gradle index 6329d25..899f102 100644 --- a/congestion-service/build.gradle +++ b/congestion-service/build.gradle @@ -24,6 +24,12 @@ repositories { } dependencies { + // JAXB (XML 파싱) + implementation 'javax.xml.bind:jaxb-api:2.3.1' + // JAXB Runtime (XML 파싱을 위해 필요) + implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3' + + // JWT 처리 implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' diff --git a/congestion-service/src/main/resources/application.properties b/congestion-service/src/main/resources/application.properties index a5977bd..7eb681f 100644 --- a/congestion-service/src/main/resources/application.properties +++ b/congestion-service/src/main/resources/application.properties @@ -1 +1,15 @@ spring.application.name=congestion-service + +# postgreSQL ?? +spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db +spring.datasource.username=root +spring.datasource.password=admin +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update + +spring.jpa.defer-datasource-initialization=true +# Hibernate SQL ?? ??? +spring.jpa.show-sql=true +spring.sql.init.mode=always diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java index ae24bad..8918c96 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java @@ -23,19 +23,19 @@ public class RoadDto { public static class CityData { @XmlElement(name = "ROAD_TRAFFIC_STTS") @JsonProperty("road_traffic_stts") - private List roadTrafficSttsList; + private RoadTrafficSttsWrapper roadTrafficSttsWrapper; } @Data @XmlAccessorType(XmlAccessType.FIELD) - public static class RoadTrafficStts { + public static class RoadTrafficSttsWrapper { @XmlElement(name = "AVG_ROAD_DATA") @JsonProperty("avg_road_data") private AvgRoadData avgRoadData; @XmlElement(name = "ROAD_TRAFFIC_STTS") @JsonProperty("road_traffic_stts") - private RoadTrafficDetail roadTrafficDetail; + private List roadTrafficDetailList; } From 515c678e7154efd56295d5278bb4a76972ecc81b Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 18 Apr 2025 15:07:23 +0900 Subject: [PATCH 044/361] =?UTF-8?q?Fix:=20=EB=8F=84=EB=A1=9C=EC=86=8C?= =?UTF-8?q?=ED=86=B5=20API=20=EC=B6=9C=EB=A0=A5=20=EB=82=B4=EC=9A=A9=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- congestion-service/build.gradle | 6 ++++++ .../src/main/resources/application.properties | 14 ++++++++++++++ .../example/externalinfoservice/dto/RoadDto.java | 6 +++--- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/congestion-service/build.gradle b/congestion-service/build.gradle index 6329d25..899f102 100644 --- a/congestion-service/build.gradle +++ b/congestion-service/build.gradle @@ -24,6 +24,12 @@ repositories { } dependencies { + // JAXB (XML 파싱) + implementation 'javax.xml.bind:jaxb-api:2.3.1' + // JAXB Runtime (XML 파싱을 위해 필요) + implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3' + + // JWT 처리 implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' diff --git a/congestion-service/src/main/resources/application.properties b/congestion-service/src/main/resources/application.properties index a5977bd..7eb681f 100644 --- a/congestion-service/src/main/resources/application.properties +++ b/congestion-service/src/main/resources/application.properties @@ -1 +1,15 @@ spring.application.name=congestion-service + +# postgreSQL ?? +spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db +spring.datasource.username=root +spring.datasource.password=admin +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update + +spring.jpa.defer-datasource-initialization=true +# Hibernate SQL ?? ??? +spring.jpa.show-sql=true +spring.sql.init.mode=always diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java index ae24bad..8918c96 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java @@ -23,19 +23,19 @@ public class RoadDto { public static class CityData { @XmlElement(name = "ROAD_TRAFFIC_STTS") @JsonProperty("road_traffic_stts") - private List roadTrafficSttsList; + private RoadTrafficSttsWrapper roadTrafficSttsWrapper; } @Data @XmlAccessorType(XmlAccessType.FIELD) - public static class RoadTrafficStts { + public static class RoadTrafficSttsWrapper { @XmlElement(name = "AVG_ROAD_DATA") @JsonProperty("avg_road_data") private AvgRoadData avgRoadData; @XmlElement(name = "ROAD_TRAFFIC_STTS") @JsonProperty("road_traffic_stts") - private RoadTrafficDetail roadTrafficDetail; + private List roadTrafficDetailList; } From aa75ad51800016b9dc884808438c899c7586af86 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 18 Apr 2025 15:07:23 +0900 Subject: [PATCH 045/361] =?UTF-8?q?Fix:=20=EB=8F=84=EB=A1=9C=EC=86=8C?= =?UTF-8?q?=ED=86=B5=20API=20=EC=B6=9C=EB=A0=A5=20=EB=82=B4=EC=9A=A9=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- congestion-service/build.gradle | 6 ++++++ .../src/main/resources/application.properties | 14 ++++++++++++++ .../example/externalinfoservice/dto/RoadDto.java | 6 +++--- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/congestion-service/build.gradle b/congestion-service/build.gradle index 6329d25..899f102 100644 --- a/congestion-service/build.gradle +++ b/congestion-service/build.gradle @@ -24,6 +24,12 @@ repositories { } dependencies { + // JAXB (XML 파싱) + implementation 'javax.xml.bind:jaxb-api:2.3.1' + // JAXB Runtime (XML 파싱을 위해 필요) + implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3' + + // JWT 처리 implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' diff --git a/congestion-service/src/main/resources/application.properties b/congestion-service/src/main/resources/application.properties index a5977bd..7eb681f 100644 --- a/congestion-service/src/main/resources/application.properties +++ b/congestion-service/src/main/resources/application.properties @@ -1 +1,15 @@ spring.application.name=congestion-service + +# postgreSQL ?? +spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db +spring.datasource.username=root +spring.datasource.password=admin +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update + +spring.jpa.defer-datasource-initialization=true +# Hibernate SQL ?? ??? +spring.jpa.show-sql=true +spring.sql.init.mode=always diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java index ae24bad..8918c96 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java @@ -23,19 +23,19 @@ public class RoadDto { public static class CityData { @XmlElement(name = "ROAD_TRAFFIC_STTS") @JsonProperty("road_traffic_stts") - private List roadTrafficSttsList; + private RoadTrafficSttsWrapper roadTrafficSttsWrapper; } @Data @XmlAccessorType(XmlAccessType.FIELD) - public static class RoadTrafficStts { + public static class RoadTrafficSttsWrapper { @XmlElement(name = "AVG_ROAD_DATA") @JsonProperty("avg_road_data") private AvgRoadData avgRoadData; @XmlElement(name = "ROAD_TRAFFIC_STTS") @JsonProperty("road_traffic_stts") - private RoadTrafficDetail roadTrafficDetail; + private List roadTrafficDetailList; } From abc25b6c9393093b09d56ae90f3da301378286e9 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 18 Apr 2025 15:50:37 +0900 Subject: [PATCH 046/361] =?UTF-8?q?Feat:CAFE=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CafeController.java | 14 ++++- .../controller/MainController.java | 63 +++++++++++++++++++ .../com/example/placeservice/dto/CafeDto.java | 23 +++++++ .../placeservice/service/CafeService.java | 39 ++++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 place-service/src/main/java/com/example/placeservice/controller/MainController.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/CafeDto.java diff --git a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java index 88743fa..0bd9998 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java @@ -1,5 +1,6 @@ package com.example.placeservice.controller; +import com.example.placeservice.dto.CafeDto; import com.example.placeservice.service.CafeService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -12,7 +13,7 @@ @Slf4j @RestController -@RequestMapping("/api/cafes") +@RequestMapping("/cafes") @RequiredArgsConstructor public class CafeController { @@ -41,4 +42,15 @@ public ResponseEntity> processAllAreas() { return ResponseEntity.badRequest().body(response); } } + + /** + * 모든 카페 목록 조회 + */ + @GetMapping + public ResponseEntity> getAllCafes() { + log.info("모든 카페 목록 조회"); + List cafes = cafeService.getAllCafes(); + return ResponseEntity.ok(cafes); + } + } \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/controller/MainController.java b/place-service/src/main/java/com/example/placeservice/controller/MainController.java new file mode 100644 index 0000000..8502c5f --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/MainController.java @@ -0,0 +1,63 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.dto.CafeDto; +import com.example.placeservice.entity.Cafe; +import com.example.placeservice.service.CafeService; +import lombok.RequiredArgsConstructor; +import com.example.placeservice.repository.CafeRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@RestController +@RequestMapping("/main") +@RequiredArgsConstructor +public class MainController { + + private final CafeRepository cafeRepository; + private final CafeService cafeService; + + /** + * 전체 카페 목록 조회 + */ + @GetMapping("/cafe/list") + public ResponseEntity> getCafeList() { + log.info("카페 목록 조회"); + List cafes = cafeRepository.findAll(); + + List> cafeList = cafes.stream() + .map(cafe -> { + Map cafeMap = new HashMap<>(); + cafeMap.put("id", cafe.getId().toString()); + cafeMap.put("name", cafe.getName()); + cafeMap.put("x", cafe.getLon().toString()); + cafeMap.put("y", cafe.getLat().toString()); + return cafeMap; + }) + .collect(Collectors.toList()); + + Map response = new HashMap<>(); + response.put("data", cafeList); + + return ResponseEntity.ok(response); + } + + /** + * 지역 ID를 기준으로 카페 목록 조회 + */ + @GetMapping("/cafe/list/{areaId}") + public ResponseEntity> getCafesByAreaId(@PathVariable Long areaId) { + log.info("지역 ID {}의 카페 목록 조회", areaId); + List cafes = cafeService.getCafesByAreaId(areaId); + return ResponseEntity.ok(cafes); + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/dto/CafeDto.java b/place-service/src/main/java/com/example/placeservice/dto/CafeDto.java new file mode 100644 index 0000000..c959627 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/CafeDto.java @@ -0,0 +1,23 @@ +package com.example.placeservice.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CafeDto { + private Long id; + private String name; + private String address; + private BigDecimal lat; + private BigDecimal lon; + private String phone; + private String kakaomapUrl; + private String categoryCode; +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java index 7c128cf..5375930 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CafeService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java @@ -1,5 +1,6 @@ package com.example.placeservice.service; +import com.example.placeservice.dto.CafeDto; import com.example.placeservice.entity.Area; import com.example.placeservice.entity.Cafe; import com.example.placeservice.repository.AreaRepository; @@ -130,4 +131,42 @@ public void processSingleArea(Area area) { log.error("장소 ID {} 처리 중 오류: {}", area.getAreaId(), e.getMessage(), e); } } + + /** + * 특정 지역의 카페 목록 조회 + */ + @Transactional(readOnly = true) + public List getCafesByAreaId(Long areaId) { + List cafes = cafeRepository.findByAreaId(areaId); + return cafes.stream() + .map(this::convertToDto) + .collect(Collectors.toList()); + } + + /** + * 모든 카페 목록 조회 + */ + @Transactional(readOnly = true) + public List getAllCafes() { + List cafes = cafeRepository.findAll(); + return cafes.stream() + .map(this::convertToDto) + .collect(Collectors.toList()); + } + + /** + * Cafe 엔티티를 DTO로 변환 + */ + private CafeDto convertToDto(Cafe cafe) { + return CafeDto.builder() + .id(cafe.getId()) + .name(cafe.getName()) + .address(cafe.getAddress()) + .lat(cafe.getLat()) + .lon(cafe.getLon()) + .phone(cafe.getPhone()) + .kakaomapUrl(cafe.getKakaomapUrl()) + .categoryCode(cafe.getCategoryCode()) + .build(); + } } \ No newline at end of file From 2d70bd39b0b97318819db3e5760194a7204367cc Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 18 Apr 2025 15:50:37 +0900 Subject: [PATCH 047/361] =?UTF-8?q?Feat:CAFE=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CafeController.java | 14 ++++- .../controller/MainController.java | 63 +++++++++++++++++++ .../com/example/placeservice/dto/CafeDto.java | 23 +++++++ .../placeservice/service/CafeService.java | 39 ++++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 place-service/src/main/java/com/example/placeservice/controller/MainController.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/CafeDto.java diff --git a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java index 88743fa..0bd9998 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java @@ -1,5 +1,6 @@ package com.example.placeservice.controller; +import com.example.placeservice.dto.CafeDto; import com.example.placeservice.service.CafeService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -12,7 +13,7 @@ @Slf4j @RestController -@RequestMapping("/api/cafes") +@RequestMapping("/cafes") @RequiredArgsConstructor public class CafeController { @@ -41,4 +42,15 @@ public ResponseEntity> processAllAreas() { return ResponseEntity.badRequest().body(response); } } + + /** + * 모든 카페 목록 조회 + */ + @GetMapping + public ResponseEntity> getAllCafes() { + log.info("모든 카페 목록 조회"); + List cafes = cafeService.getAllCafes(); + return ResponseEntity.ok(cafes); + } + } \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/controller/MainController.java b/place-service/src/main/java/com/example/placeservice/controller/MainController.java new file mode 100644 index 0000000..8502c5f --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/MainController.java @@ -0,0 +1,63 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.dto.CafeDto; +import com.example.placeservice.entity.Cafe; +import com.example.placeservice.service.CafeService; +import lombok.RequiredArgsConstructor; +import com.example.placeservice.repository.CafeRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@RestController +@RequestMapping("/main") +@RequiredArgsConstructor +public class MainController { + + private final CafeRepository cafeRepository; + private final CafeService cafeService; + + /** + * 전체 카페 목록 조회 + */ + @GetMapping("/cafe/list") + public ResponseEntity> getCafeList() { + log.info("카페 목록 조회"); + List cafes = cafeRepository.findAll(); + + List> cafeList = cafes.stream() + .map(cafe -> { + Map cafeMap = new HashMap<>(); + cafeMap.put("id", cafe.getId().toString()); + cafeMap.put("name", cafe.getName()); + cafeMap.put("x", cafe.getLon().toString()); + cafeMap.put("y", cafe.getLat().toString()); + return cafeMap; + }) + .collect(Collectors.toList()); + + Map response = new HashMap<>(); + response.put("data", cafeList); + + return ResponseEntity.ok(response); + } + + /** + * 지역 ID를 기준으로 카페 목록 조회 + */ + @GetMapping("/cafe/list/{areaId}") + public ResponseEntity> getCafesByAreaId(@PathVariable Long areaId) { + log.info("지역 ID {}의 카페 목록 조회", areaId); + List cafes = cafeService.getCafesByAreaId(areaId); + return ResponseEntity.ok(cafes); + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/dto/CafeDto.java b/place-service/src/main/java/com/example/placeservice/dto/CafeDto.java new file mode 100644 index 0000000..c959627 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/CafeDto.java @@ -0,0 +1,23 @@ +package com.example.placeservice.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CafeDto { + private Long id; + private String name; + private String address; + private BigDecimal lat; + private BigDecimal lon; + private String phone; + private String kakaomapUrl; + private String categoryCode; +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java index 7c128cf..5375930 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CafeService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java @@ -1,5 +1,6 @@ package com.example.placeservice.service; +import com.example.placeservice.dto.CafeDto; import com.example.placeservice.entity.Area; import com.example.placeservice.entity.Cafe; import com.example.placeservice.repository.AreaRepository; @@ -130,4 +131,42 @@ public void processSingleArea(Area area) { log.error("장소 ID {} 처리 중 오류: {}", area.getAreaId(), e.getMessage(), e); } } + + /** + * 특정 지역의 카페 목록 조회 + */ + @Transactional(readOnly = true) + public List getCafesByAreaId(Long areaId) { + List cafes = cafeRepository.findByAreaId(areaId); + return cafes.stream() + .map(this::convertToDto) + .collect(Collectors.toList()); + } + + /** + * 모든 카페 목록 조회 + */ + @Transactional(readOnly = true) + public List getAllCafes() { + List cafes = cafeRepository.findAll(); + return cafes.stream() + .map(this::convertToDto) + .collect(Collectors.toList()); + } + + /** + * Cafe 엔티티를 DTO로 변환 + */ + private CafeDto convertToDto(Cafe cafe) { + return CafeDto.builder() + .id(cafe.getId()) + .name(cafe.getName()) + .address(cafe.getAddress()) + .lat(cafe.getLat()) + .lon(cafe.getLon()) + .phone(cafe.getPhone()) + .kakaomapUrl(cafe.getKakaomapUrl()) + .categoryCode(cafe.getCategoryCode()) + .build(); + } } \ No newline at end of file From ef220ef6b6291a21578ef5818867a356794d857b Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 18 Apr 2025 15:51:12 +0900 Subject: [PATCH 048/361] =?UTF-8?q?Feat:CAFE=20DATA=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 577f366..f80c767 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. ### CAFE DATA 1. postman에서 설정 - Method: POST - - URL: http://localhost:8080/api/cafes/process-all + - URL: http://localhost:8080/cafes/process-all - Auth: Basic Auth - Username: user - Password: docker에서 비번 확인 From ecf774d5b7d7da7d0afcef185b57b44060826ad8 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 18 Apr 2025 15:51:12 +0900 Subject: [PATCH 049/361] =?UTF-8?q?Feat:CAFE=20DATA=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 577f366..f80c767 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. ### CAFE DATA 1. postman에서 설정 - Method: POST - - URL: http://localhost:8080/api/cafes/process-all + - URL: http://localhost:8080/cafes/process-all - Auth: Basic Auth - Username: user - Password: docker에서 비번 확인 From 42cfeed9f25ecf08f158e8dcf343be53292ee017 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 18 Apr 2025 16:52:39 +0900 Subject: [PATCH 050/361] =?UTF-8?q?Feat:CAFE=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CafeController.java | 5 +- .../controller/MainController.java | 19 ++++--- .../placeservice/service/CafeService.java | 52 +++++++------------ 3 files changed, 35 insertions(+), 41 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java index 0bd9998..dbc0fbd 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java @@ -47,10 +47,9 @@ public ResponseEntity> processAllAreas() { * 모든 카페 목록 조회 */ @GetMapping - public ResponseEntity> getAllCafes() { + public ResponseEntity>> getAllCafes() { log.info("모든 카페 목록 조회"); - List cafes = cafeService.getAllCafes(); + List> cafes = cafeService.getAllCafesWithLimitedInfo(); return ResponseEntity.ok(cafes); } - } \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/controller/MainController.java b/place-service/src/main/java/com/example/placeservice/controller/MainController.java index 8502c5f..f7a7222 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/MainController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/MainController.java @@ -3,6 +3,7 @@ import com.example.placeservice.dto.CafeDto; import com.example.placeservice.entity.Cafe; import com.example.placeservice.service.CafeService; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import com.example.placeservice.repository.CafeRepository; import lombok.extern.slf4j.Slf4j; @@ -12,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -30,11 +32,13 @@ public class MainController { * 전체 카페 목록 조회 */ @GetMapping("/cafe/list") - public ResponseEntity> getCafeList() { + public ResponseEntity>> getCafeList() { log.info("카페 목록 조회"); List cafes = cafeRepository.findAll(); + // ID를 기준으로 정렬 List> cafeList = cafes.stream() + .sorted(Comparator.comparing(Cafe::getId)) .map(cafe -> { Map cafeMap = new HashMap<>(); cafeMap.put("id", cafe.getId().toString()); @@ -45,10 +49,7 @@ public ResponseEntity> getCafeList() { }) .collect(Collectors.toList()); - Map response = new HashMap<>(); - response.put("data", cafeList); - - return ResponseEntity.ok(response); + return ResponseEntity.ok(cafeList); } /** @@ -58,6 +59,12 @@ public ResponseEntity> getCafeList() { public ResponseEntity> getCafesByAreaId(@PathVariable Long areaId) { log.info("지역 ID {}의 카페 목록 조회", areaId); List cafes = cafeService.getCafesByAreaId(areaId); - return ResponseEntity.ok(cafes); + + // ID 기준으로 정렬 + List sortedCafes = cafes.stream() + .sorted(Comparator.comparing(CafeDto::getId)) + .collect(Collectors.toList()); + + return ResponseEntity.ok(sortedCafes); } } \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java index 5375930..da4e1d6 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CafeService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java @@ -12,7 +12,9 @@ import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Slf4j @@ -50,38 +52,6 @@ public void processAllAreas() { log.info("모든 장소 처리 완료."); } - /** - * 특정 ID 목록의 장소만 처리 - */ - @Async - public void processAreasByIds(List areaIds) { - log.info("{}개 지정된 장소에 대한 처리를 시작합니다.", areaIds.size()); - - for (int i = 0; i < areaIds.size(); i++) { - Long areaId = areaIds.get(i); - Area area = areaRepository.findById(areaId).orElse(null); - - if (area == null) { - log.warn("ID {}인 장소를 찾을 수 없습니다.", areaId); - continue; - } - - log.info("[{}/{}] '{}' (ID: {}) 처리 중...", - i + 1, areaIds.size(), area.getName(), area.getAreaId()); - - processSingleArea(area); - - // API 호출 간격 조절 - try { - Thread.sleep(300); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - log.info("지정된 모든 장소 처리 완료."); - } - /** * 단일 장소의 주변 카페 정보 처리 */ @@ -154,6 +124,24 @@ public List getAllCafes() { .collect(Collectors.toList()); } + /** + * ID, 이름, 위도, 경도만 포함한 모든 카페 목록 조회 + */ + @Transactional(readOnly = true) + public List> getAllCafesWithLimitedInfo() { + List cafes = cafeRepository.findAll(); + return cafes.stream() + .map(cafe -> { + Map cafeMap = new HashMap<>(); + cafeMap.put("id", cafe.getId()); + cafeMap.put("name", cafe.getName()); + cafeMap.put("lat", cafe.getLat()); + cafeMap.put("lon", cafe.getLon()); + return cafeMap; + }) + .collect(Collectors.toList()); + } + /** * Cafe 엔티티를 DTO로 변환 */ From f05f2a2e18a377e6188d8de482753ad18fa2ee04 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 18 Apr 2025 16:52:39 +0900 Subject: [PATCH 051/361] =?UTF-8?q?Feat:CAFE=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CafeController.java | 5 +- .../controller/MainController.java | 19 ++++--- .../placeservice/service/CafeService.java | 52 +++++++------------ 3 files changed, 35 insertions(+), 41 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java index 0bd9998..dbc0fbd 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java @@ -47,10 +47,9 @@ public ResponseEntity> processAllAreas() { * 모든 카페 목록 조회 */ @GetMapping - public ResponseEntity> getAllCafes() { + public ResponseEntity>> getAllCafes() { log.info("모든 카페 목록 조회"); - List cafes = cafeService.getAllCafes(); + List> cafes = cafeService.getAllCafesWithLimitedInfo(); return ResponseEntity.ok(cafes); } - } \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/controller/MainController.java b/place-service/src/main/java/com/example/placeservice/controller/MainController.java index 8502c5f..f7a7222 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/MainController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/MainController.java @@ -3,6 +3,7 @@ import com.example.placeservice.dto.CafeDto; import com.example.placeservice.entity.Cafe; import com.example.placeservice.service.CafeService; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import com.example.placeservice.repository.CafeRepository; import lombok.extern.slf4j.Slf4j; @@ -12,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -30,11 +32,13 @@ public class MainController { * 전체 카페 목록 조회 */ @GetMapping("/cafe/list") - public ResponseEntity> getCafeList() { + public ResponseEntity>> getCafeList() { log.info("카페 목록 조회"); List cafes = cafeRepository.findAll(); + // ID를 기준으로 정렬 List> cafeList = cafes.stream() + .sorted(Comparator.comparing(Cafe::getId)) .map(cafe -> { Map cafeMap = new HashMap<>(); cafeMap.put("id", cafe.getId().toString()); @@ -45,10 +49,7 @@ public ResponseEntity> getCafeList() { }) .collect(Collectors.toList()); - Map response = new HashMap<>(); - response.put("data", cafeList); - - return ResponseEntity.ok(response); + return ResponseEntity.ok(cafeList); } /** @@ -58,6 +59,12 @@ public ResponseEntity> getCafeList() { public ResponseEntity> getCafesByAreaId(@PathVariable Long areaId) { log.info("지역 ID {}의 카페 목록 조회", areaId); List cafes = cafeService.getCafesByAreaId(areaId); - return ResponseEntity.ok(cafes); + + // ID 기준으로 정렬 + List sortedCafes = cafes.stream() + .sorted(Comparator.comparing(CafeDto::getId)) + .collect(Collectors.toList()); + + return ResponseEntity.ok(sortedCafes); } } \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java index 5375930..da4e1d6 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CafeService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java @@ -12,7 +12,9 @@ import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Slf4j @@ -50,38 +52,6 @@ public void processAllAreas() { log.info("모든 장소 처리 완료."); } - /** - * 특정 ID 목록의 장소만 처리 - */ - @Async - public void processAreasByIds(List areaIds) { - log.info("{}개 지정된 장소에 대한 처리를 시작합니다.", areaIds.size()); - - for (int i = 0; i < areaIds.size(); i++) { - Long areaId = areaIds.get(i); - Area area = areaRepository.findById(areaId).orElse(null); - - if (area == null) { - log.warn("ID {}인 장소를 찾을 수 없습니다.", areaId); - continue; - } - - log.info("[{}/{}] '{}' (ID: {}) 처리 중...", - i + 1, areaIds.size(), area.getName(), area.getAreaId()); - - processSingleArea(area); - - // API 호출 간격 조절 - try { - Thread.sleep(300); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - log.info("지정된 모든 장소 처리 완료."); - } - /** * 단일 장소의 주변 카페 정보 처리 */ @@ -154,6 +124,24 @@ public List getAllCafes() { .collect(Collectors.toList()); } + /** + * ID, 이름, 위도, 경도만 포함한 모든 카페 목록 조회 + */ + @Transactional(readOnly = true) + public List> getAllCafesWithLimitedInfo() { + List cafes = cafeRepository.findAll(); + return cafes.stream() + .map(cafe -> { + Map cafeMap = new HashMap<>(); + cafeMap.put("id", cafe.getId()); + cafeMap.put("name", cafe.getName()); + cafeMap.put("lat", cafe.getLat()); + cafeMap.put("lon", cafe.getLon()); + return cafeMap; + }) + .collect(Collectors.toList()); + } + /** * Cafe 엔티티를 DTO로 변환 */ From 1d80192bec47eaf1dbfdf75570c87d87d3c667c2 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 18 Apr 2025 16:55:40 +0900 Subject: [PATCH 052/361] =?UTF-8?q?Feat:=20=EA=B4=80=EA=B4=91=EC=A7=80?= =?UTF-8?q?=EB=B3=84=20=ED=98=BC=EC=9E=A1=EB=8F=84=20API=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 --- .../placeservice/config/AppConfig.java | 14 ++ .../controller/AttractionController.java | 4 +- .../controller/CongestionController.java | 21 +++ .../{ => attraction}/AreaAttractionsDto.java | 2 +- .../dto/{ => attraction}/AttractionDto.java | 2 +- .../{ => attraction}/AttractionInfoDto.java | 4 +- .../{ => attraction}/AttractionListDto.java | 2 +- .../dto/congestion/CongestionDto.java | 146 ++++++++++++++++++ .../service/AttractionService.java | 8 +- .../service/CongestionService.java | 53 +++++++ 10 files changed, 244 insertions(+), 12 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/config/AppConfig.java create mode 100644 place-service/src/main/java/com/example/placeservice/controller/CongestionController.java rename place-service/src/main/java/com/example/placeservice/dto/{ => attraction}/AreaAttractionsDto.java (79%) rename place-service/src/main/java/com/example/placeservice/dto/{ => attraction}/AttractionDto.java (96%) rename place-service/src/main/java/com/example/placeservice/dto/{ => attraction}/AttractionInfoDto.java (87%) rename place-service/src/main/java/com/example/placeservice/dto/{ => attraction}/AttractionListDto.java (91%) create mode 100644 place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/CongestionService.java diff --git a/place-service/src/main/java/com/example/placeservice/config/AppConfig.java b/place-service/src/main/java/com/example/placeservice/config/AppConfig.java new file mode 100644 index 0000000..4b97730 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/AppConfig.java @@ -0,0 +1,14 @@ +package com.example.placeservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + +} diff --git a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java index b508e6e..67d6e02 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java @@ -1,7 +1,7 @@ package com.example.placeservice.controller; -import com.example.placeservice.dto.AreaAttractionsDto; -import com.example.placeservice.dto.AttractionInfoDto; +import com.example.placeservice.dto.attraction.AreaAttractionsDto; +import com.example.placeservice.dto.attraction.AttractionInfoDto; import com.example.placeservice.service.AttractionService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; diff --git a/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java b/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java new file mode 100644 index 0000000..971be0b --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java @@ -0,0 +1,21 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.service.CongestionService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/admin/congestion") +@RequiredArgsConstructor +public class CongestionController { + private final CongestionService congestionService; + + @GetMapping("/{area-id}") + public String getCongestionData(@PathVariable("area-id") String areaName) { + // 서울 실시간 도시데이터 OpenAPI 데이터 중 인구 혼잡도 데이터 이용 + return congestionService.getCongestion(areaName); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AreaAttractionsDto.java similarity index 79% rename from place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java rename to place-service/src/main/java/com/example/placeservice/dto/attraction/AreaAttractionsDto.java index b79c097..62d5963 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AreaAttractionsDto.java @@ -1,4 +1,4 @@ -package com.example.placeservice.dto; +package com.example.placeservice.dto.attraction; import lombok.Data; diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionDto.java similarity index 96% rename from place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java rename to place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionDto.java index 92348ab..0323622 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionDto.java @@ -1,4 +1,4 @@ -package com.example.placeservice.dto; +package com.example.placeservice.dto.attraction; import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java similarity index 87% rename from place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java rename to place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java index 91a0464..f5ff08e 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java @@ -1,7 +1,5 @@ -package com.example.placeservice.dto; +package com.example.placeservice.dto.attraction; -import com.example.placeservice.entity.Attraction; -import jakarta.persistence.Column; import lombok.Data; import java.math.BigDecimal; diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java similarity index 91% rename from place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java rename to place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java index 516636e..79e169f 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java @@ -1,4 +1,4 @@ -package com.example.placeservice.dto; +package com.example.placeservice.dto.attraction; import lombok.Data; diff --git a/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java b/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java new file mode 100644 index 0000000..903e55b --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java @@ -0,0 +1,146 @@ +package com.example.placeservice.dto.congestion; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import lombok.Data; + +import java.util.List; + +@Data +@XmlRootElement(name = "Map") +@XmlAccessorType(XmlAccessType.FIELD) +public class CongestionDto { + + @XmlElement(name="SeoulRtd.citydata_ppltn") + @JsonProperty("ppltn_data") + private PpltnData ppltnData; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class PpltnData { + + @XmlElement(name = "AREA_NM") + @JsonProperty("area_nm") + private String areaName; + + @XmlElement(name = "AREA_CD") + @JsonProperty("area_cd") + private String areaCode; + + @XmlElement(name = "AREA_CONGEST_LVL") + @JsonProperty("area_congest_lvl") + private String areaCongestLvl; + + @XmlElement(name = "AREA_CONGEST_MSG") + @JsonProperty("area_congest_msg") + private String areaCongestionMsg; + + @XmlElement(name = "AREA_PPLTN_MIN") + @JsonProperty("area_ppltn_min") + private int areaPpltnMin; + + @XmlElement(name = "AREA_PPLTN_MAX") + @JsonProperty("area_ppltn_max") + private int areaPpltnMax; + + @XmlElement(name = "MALE_PPLTN_RATE") + @JsonProperty("male_ppltn_rate") + private double malePpltnRate; + + @XmlElement(name = "FEMALE_PPLTN_RATE") + @JsonProperty("female_ppltn_rate") + private double femalePpltnRate; + + @XmlElement(name = "PPLTN_RATE_0") + @JsonProperty("ppltn_rate_0") + private double PpltnRate0; + + @XmlElement(name = "PPLTN_RATE_10") + @JsonProperty("ppltn_rate_10") + private double PpltnRate10; + + @XmlElement(name = "PPLTN_RATE_20") + @JsonProperty("ppltn_rate_20") + private double PpltnRate20; + + @XmlElement(name = "PPLTN_RATE_30") + @JsonProperty("ppltn_rate_30") + private double PpltnRate30; + + @XmlElement(name = "PPLTN_RATE_40") + @JsonProperty("ppltn_rate_40") + private double PpltnRate40; + + @XmlElement(name = "PPLTN_RATE_50") + @JsonProperty("ppltn_rate_50") + private double PpltnRate50; + + @XmlElement(name = "PPLTN_RATE_60") + @JsonProperty("ppltn_rate_60") + private double PpltnRate60; + + @XmlElement(name = "PPLTN_RATE_70") + @JsonProperty("ppltn_rate_70") + private double PpltnRate70; + + @XmlElement(name = "RESNT_PPLTN_RATE") + @JsonProperty("resnt_ppltn_rate") + private double ResntPpltnRate; + + @XmlElement(name = "NON_RESNT_PPLTN_RATE") + @JsonProperty("non_resnt_ppltn_rate") + private double NonResntPpltnRate; + + @XmlElement(name = "REPLACE_YN") + @JsonProperty("replace_yn") + private String replaceYn; + + @XmlElement(name = "PPLTN_TIME") + @JsonProperty("ppltn_time") + private String ppltnTime; + + @XmlElement(name = "FCST_YN") + @JsonProperty("fcst_yn") + private String fcstYn; + + @XmlElement(name = "FCST_PPLTN") + @JsonProperty("fcst_ppltn_wrapper") + private FcstPpltnWrapper FcstPpltnWrapper; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class FcstPpltnWrapper { + @XmlElement(name = "FCST_PPLTN") + @JsonProperty("fcst_ppltn") + private List FcstPpltn; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class FcstPpltn { + @XmlElement(name = "FCST_TIME") + @JsonProperty("fcst_time") + private String fcstTime; + + @XmlElement(name = "FCST_CONGEST_LVL") + @JsonProperty("fcst_congest_lvl") + private String fcstCongestLvl; + + @XmlElement(name = "FCST_PPLTN_MIN") + @JsonProperty("fcst_ppltn_min") + private int fcstPpltnMin; + + @XmlElement(name = "FCST_PPLTN_MAX") + @JsonProperty("fcst_ppltn_max") + private int fcstPpltnMax; + + } + + + + +} diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index 3c3b78f..87e694c 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -1,9 +1,9 @@ package com.example.placeservice.service; -import com.example.placeservice.dto.AreaAttractionsDto; -import com.example.placeservice.dto.AttractionDto; -import com.example.placeservice.dto.AttractionInfoDto; -import com.example.placeservice.dto.AttractionListDto; +import com.example.placeservice.dto.attraction.AreaAttractionsDto; +import com.example.placeservice.dto.attraction.AttractionDto; +import com.example.placeservice.dto.attraction.AttractionInfoDto; +import com.example.placeservice.dto.attraction.AttractionListDto; import com.example.placeservice.entity.Area; import com.example.placeservice.entity.Attraction; import com.example.placeservice.repository.AreaRepository; diff --git a/place-service/src/main/java/com/example/placeservice/service/CongestionService.java b/place-service/src/main/java/com/example/placeservice/service/CongestionService.java new file mode 100644 index 0000000..6f31875 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/CongestionService.java @@ -0,0 +1,53 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.congestion.CongestionDto; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.Unmarshaller; +import org.apache.kafka.common.errors.InvalidRequestException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.io.StringReader; + +@Service +public class CongestionService { + + @Value("http://openapi.seoul.go.kr:8088/sample/xml/citydata_ppltn/1/5/") + private String openApiUrl; + + private final RestTemplate restTemplate; + + public CongestionService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public String getCongestion(String areaName) { + try{ + // 1. 외부 OpenAPI에서 XML 데이터 받아오기 + String xmlResponse = restTemplate.getForObject(openApiUrl + areaName, String.class); + + // 2. XML 데이터를 DTO로 변환 -> JSON + ObjectMapper objectMapper = new ObjectMapper(); + String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); + + return json; + + }catch (InvalidRequestException e) { + throw new InvalidRequestException(e.getMessage(), e); + } catch(IOException e){ + throw new RuntimeException("외부 API 오류",e); + } catch (Exception e) { + throw new RuntimeException("예상치 못한 오류",e); + } + } + + private CongestionDto xmlToDto(String xml) throws Exception { + // JAXB를 사용해서 XML을 DTO로 변환 + JAXBContext context = JAXBContext.newInstance(CongestionDto.class); + Unmarshaller unmarshaller = context.createUnmarshaller(); + return (CongestionDto) unmarshaller.unmarshal(new StringReader(xml)); + } +} From b0b9a7c3bac198d7070060d88d1ded588116fb64 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 18 Apr 2025 16:55:40 +0900 Subject: [PATCH 053/361] =?UTF-8?q?Feat:=20=EA=B4=80=EA=B4=91=EC=A7=80?= =?UTF-8?q?=EB=B3=84=20=ED=98=BC=EC=9E=A1=EB=8F=84=20API=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 --- .../placeservice/config/AppConfig.java | 14 ++ .../controller/AttractionController.java | 4 +- .../controller/CongestionController.java | 21 +++ .../{ => attraction}/AreaAttractionsDto.java | 2 +- .../dto/{ => attraction}/AttractionDto.java | 2 +- .../{ => attraction}/AttractionInfoDto.java | 4 +- .../{ => attraction}/AttractionListDto.java | 2 +- .../dto/congestion/CongestionDto.java | 146 ++++++++++++++++++ .../service/AttractionService.java | 8 +- .../service/CongestionService.java | 53 +++++++ 10 files changed, 244 insertions(+), 12 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/config/AppConfig.java create mode 100644 place-service/src/main/java/com/example/placeservice/controller/CongestionController.java rename place-service/src/main/java/com/example/placeservice/dto/{ => attraction}/AreaAttractionsDto.java (79%) rename place-service/src/main/java/com/example/placeservice/dto/{ => attraction}/AttractionDto.java (96%) rename place-service/src/main/java/com/example/placeservice/dto/{ => attraction}/AttractionInfoDto.java (87%) rename place-service/src/main/java/com/example/placeservice/dto/{ => attraction}/AttractionListDto.java (91%) create mode 100644 place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/CongestionService.java diff --git a/place-service/src/main/java/com/example/placeservice/config/AppConfig.java b/place-service/src/main/java/com/example/placeservice/config/AppConfig.java new file mode 100644 index 0000000..4b97730 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/AppConfig.java @@ -0,0 +1,14 @@ +package com.example.placeservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + +} diff --git a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java index b508e6e..67d6e02 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java @@ -1,7 +1,7 @@ package com.example.placeservice.controller; -import com.example.placeservice.dto.AreaAttractionsDto; -import com.example.placeservice.dto.AttractionInfoDto; +import com.example.placeservice.dto.attraction.AreaAttractionsDto; +import com.example.placeservice.dto.attraction.AttractionInfoDto; import com.example.placeservice.service.AttractionService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; diff --git a/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java b/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java new file mode 100644 index 0000000..971be0b --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java @@ -0,0 +1,21 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.service.CongestionService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/admin/congestion") +@RequiredArgsConstructor +public class CongestionController { + private final CongestionService congestionService; + + @GetMapping("/{area-id}") + public String getCongestionData(@PathVariable("area-id") String areaName) { + // 서울 실시간 도시데이터 OpenAPI 데이터 중 인구 혼잡도 데이터 이용 + return congestionService.getCongestion(areaName); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AreaAttractionsDto.java similarity index 79% rename from place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java rename to place-service/src/main/java/com/example/placeservice/dto/attraction/AreaAttractionsDto.java index b79c097..62d5963 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AreaAttractionsDto.java @@ -1,4 +1,4 @@ -package com.example.placeservice.dto; +package com.example.placeservice.dto.attraction; import lombok.Data; diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionDto.java similarity index 96% rename from place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java rename to place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionDto.java index 92348ab..0323622 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionDto.java @@ -1,4 +1,4 @@ -package com.example.placeservice.dto; +package com.example.placeservice.dto.attraction; import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java similarity index 87% rename from place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java rename to place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java index 91a0464..f5ff08e 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java @@ -1,7 +1,5 @@ -package com.example.placeservice.dto; +package com.example.placeservice.dto.attraction; -import com.example.placeservice.entity.Attraction; -import jakarta.persistence.Column; import lombok.Data; import java.math.BigDecimal; diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java similarity index 91% rename from place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java rename to place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java index 516636e..79e169f 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java @@ -1,4 +1,4 @@ -package com.example.placeservice.dto; +package com.example.placeservice.dto.attraction; import lombok.Data; diff --git a/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java b/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java new file mode 100644 index 0000000..903e55b --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java @@ -0,0 +1,146 @@ +package com.example.placeservice.dto.congestion; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import lombok.Data; + +import java.util.List; + +@Data +@XmlRootElement(name = "Map") +@XmlAccessorType(XmlAccessType.FIELD) +public class CongestionDto { + + @XmlElement(name="SeoulRtd.citydata_ppltn") + @JsonProperty("ppltn_data") + private PpltnData ppltnData; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class PpltnData { + + @XmlElement(name = "AREA_NM") + @JsonProperty("area_nm") + private String areaName; + + @XmlElement(name = "AREA_CD") + @JsonProperty("area_cd") + private String areaCode; + + @XmlElement(name = "AREA_CONGEST_LVL") + @JsonProperty("area_congest_lvl") + private String areaCongestLvl; + + @XmlElement(name = "AREA_CONGEST_MSG") + @JsonProperty("area_congest_msg") + private String areaCongestionMsg; + + @XmlElement(name = "AREA_PPLTN_MIN") + @JsonProperty("area_ppltn_min") + private int areaPpltnMin; + + @XmlElement(name = "AREA_PPLTN_MAX") + @JsonProperty("area_ppltn_max") + private int areaPpltnMax; + + @XmlElement(name = "MALE_PPLTN_RATE") + @JsonProperty("male_ppltn_rate") + private double malePpltnRate; + + @XmlElement(name = "FEMALE_PPLTN_RATE") + @JsonProperty("female_ppltn_rate") + private double femalePpltnRate; + + @XmlElement(name = "PPLTN_RATE_0") + @JsonProperty("ppltn_rate_0") + private double PpltnRate0; + + @XmlElement(name = "PPLTN_RATE_10") + @JsonProperty("ppltn_rate_10") + private double PpltnRate10; + + @XmlElement(name = "PPLTN_RATE_20") + @JsonProperty("ppltn_rate_20") + private double PpltnRate20; + + @XmlElement(name = "PPLTN_RATE_30") + @JsonProperty("ppltn_rate_30") + private double PpltnRate30; + + @XmlElement(name = "PPLTN_RATE_40") + @JsonProperty("ppltn_rate_40") + private double PpltnRate40; + + @XmlElement(name = "PPLTN_RATE_50") + @JsonProperty("ppltn_rate_50") + private double PpltnRate50; + + @XmlElement(name = "PPLTN_RATE_60") + @JsonProperty("ppltn_rate_60") + private double PpltnRate60; + + @XmlElement(name = "PPLTN_RATE_70") + @JsonProperty("ppltn_rate_70") + private double PpltnRate70; + + @XmlElement(name = "RESNT_PPLTN_RATE") + @JsonProperty("resnt_ppltn_rate") + private double ResntPpltnRate; + + @XmlElement(name = "NON_RESNT_PPLTN_RATE") + @JsonProperty("non_resnt_ppltn_rate") + private double NonResntPpltnRate; + + @XmlElement(name = "REPLACE_YN") + @JsonProperty("replace_yn") + private String replaceYn; + + @XmlElement(name = "PPLTN_TIME") + @JsonProperty("ppltn_time") + private String ppltnTime; + + @XmlElement(name = "FCST_YN") + @JsonProperty("fcst_yn") + private String fcstYn; + + @XmlElement(name = "FCST_PPLTN") + @JsonProperty("fcst_ppltn_wrapper") + private FcstPpltnWrapper FcstPpltnWrapper; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class FcstPpltnWrapper { + @XmlElement(name = "FCST_PPLTN") + @JsonProperty("fcst_ppltn") + private List FcstPpltn; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class FcstPpltn { + @XmlElement(name = "FCST_TIME") + @JsonProperty("fcst_time") + private String fcstTime; + + @XmlElement(name = "FCST_CONGEST_LVL") + @JsonProperty("fcst_congest_lvl") + private String fcstCongestLvl; + + @XmlElement(name = "FCST_PPLTN_MIN") + @JsonProperty("fcst_ppltn_min") + private int fcstPpltnMin; + + @XmlElement(name = "FCST_PPLTN_MAX") + @JsonProperty("fcst_ppltn_max") + private int fcstPpltnMax; + + } + + + + +} diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index 3c3b78f..87e694c 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -1,9 +1,9 @@ package com.example.placeservice.service; -import com.example.placeservice.dto.AreaAttractionsDto; -import com.example.placeservice.dto.AttractionDto; -import com.example.placeservice.dto.AttractionInfoDto; -import com.example.placeservice.dto.AttractionListDto; +import com.example.placeservice.dto.attraction.AreaAttractionsDto; +import com.example.placeservice.dto.attraction.AttractionDto; +import com.example.placeservice.dto.attraction.AttractionInfoDto; +import com.example.placeservice.dto.attraction.AttractionListDto; import com.example.placeservice.entity.Area; import com.example.placeservice.entity.Attraction; import com.example.placeservice.repository.AreaRepository; diff --git a/place-service/src/main/java/com/example/placeservice/service/CongestionService.java b/place-service/src/main/java/com/example/placeservice/service/CongestionService.java new file mode 100644 index 0000000..6f31875 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/CongestionService.java @@ -0,0 +1,53 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.congestion.CongestionDto; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.Unmarshaller; +import org.apache.kafka.common.errors.InvalidRequestException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.io.StringReader; + +@Service +public class CongestionService { + + @Value("http://openapi.seoul.go.kr:8088/sample/xml/citydata_ppltn/1/5/") + private String openApiUrl; + + private final RestTemplate restTemplate; + + public CongestionService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public String getCongestion(String areaName) { + try{ + // 1. 외부 OpenAPI에서 XML 데이터 받아오기 + String xmlResponse = restTemplate.getForObject(openApiUrl + areaName, String.class); + + // 2. XML 데이터를 DTO로 변환 -> JSON + ObjectMapper objectMapper = new ObjectMapper(); + String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); + + return json; + + }catch (InvalidRequestException e) { + throw new InvalidRequestException(e.getMessage(), e); + } catch(IOException e){ + throw new RuntimeException("외부 API 오류",e); + } catch (Exception e) { + throw new RuntimeException("예상치 못한 오류",e); + } + } + + private CongestionDto xmlToDto(String xml) throws Exception { + // JAXB를 사용해서 XML을 DTO로 변환 + JAXBContext context = JAXBContext.newInstance(CongestionDto.class); + Unmarshaller unmarshaller = context.createUnmarshaller(); + return (CongestionDto) unmarshaller.unmarshal(new StringReader(xml)); + } +} From 740971c952278772391bb468bf22373da185ddda Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 18 Apr 2025 16:55:40 +0900 Subject: [PATCH 054/361] =?UTF-8?q?Feat:=20=EA=B4=80=EA=B4=91=EC=A7=80?= =?UTF-8?q?=EB=B3=84=20=ED=98=BC=EC=9E=A1=EB=8F=84=20API=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 --- .../placeservice/config/AppConfig.java | 14 ++ .../controller/AttractionController.java | 4 +- .../controller/CongestionController.java | 21 +++ .../{ => attraction}/AreaAttractionsDto.java | 2 +- .../dto/{ => attraction}/AttractionDto.java | 2 +- .../{ => attraction}/AttractionInfoDto.java | 4 +- .../{ => attraction}/AttractionListDto.java | 2 +- .../dto/congestion/CongestionDto.java | 146 ++++++++++++++++++ .../service/AttractionService.java | 8 +- .../service/CongestionService.java | 53 +++++++ 10 files changed, 244 insertions(+), 12 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/config/AppConfig.java create mode 100644 place-service/src/main/java/com/example/placeservice/controller/CongestionController.java rename place-service/src/main/java/com/example/placeservice/dto/{ => attraction}/AreaAttractionsDto.java (79%) rename place-service/src/main/java/com/example/placeservice/dto/{ => attraction}/AttractionDto.java (96%) rename place-service/src/main/java/com/example/placeservice/dto/{ => attraction}/AttractionInfoDto.java (87%) rename place-service/src/main/java/com/example/placeservice/dto/{ => attraction}/AttractionListDto.java (91%) create mode 100644 place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/CongestionService.java diff --git a/place-service/src/main/java/com/example/placeservice/config/AppConfig.java b/place-service/src/main/java/com/example/placeservice/config/AppConfig.java new file mode 100644 index 0000000..4b97730 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/AppConfig.java @@ -0,0 +1,14 @@ +package com.example.placeservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + +} diff --git a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java index b508e6e..67d6e02 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java @@ -1,7 +1,7 @@ package com.example.placeservice.controller; -import com.example.placeservice.dto.AreaAttractionsDto; -import com.example.placeservice.dto.AttractionInfoDto; +import com.example.placeservice.dto.attraction.AreaAttractionsDto; +import com.example.placeservice.dto.attraction.AttractionInfoDto; import com.example.placeservice.service.AttractionService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; diff --git a/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java b/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java new file mode 100644 index 0000000..971be0b --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java @@ -0,0 +1,21 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.service.CongestionService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/admin/congestion") +@RequiredArgsConstructor +public class CongestionController { + private final CongestionService congestionService; + + @GetMapping("/{area-id}") + public String getCongestionData(@PathVariable("area-id") String areaName) { + // 서울 실시간 도시데이터 OpenAPI 데이터 중 인구 혼잡도 데이터 이용 + return congestionService.getCongestion(areaName); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AreaAttractionsDto.java similarity index 79% rename from place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java rename to place-service/src/main/java/com/example/placeservice/dto/attraction/AreaAttractionsDto.java index b79c097..62d5963 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/AreaAttractionsDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AreaAttractionsDto.java @@ -1,4 +1,4 @@ -package com.example.placeservice.dto; +package com.example.placeservice.dto.attraction; import lombok.Data; diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionDto.java similarity index 96% rename from place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java rename to place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionDto.java index 92348ab..0323622 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/AttractionDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionDto.java @@ -1,4 +1,4 @@ -package com.example.placeservice.dto; +package com.example.placeservice.dto.attraction; import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java similarity index 87% rename from place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java rename to place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java index 91a0464..f5ff08e 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/AttractionInfoDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java @@ -1,7 +1,5 @@ -package com.example.placeservice.dto; +package com.example.placeservice.dto.attraction; -import com.example.placeservice.entity.Attraction; -import jakarta.persistence.Column; import lombok.Data; import java.math.BigDecimal; diff --git a/place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java similarity index 91% rename from place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java rename to place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java index 516636e..79e169f 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/AttractionListDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java @@ -1,4 +1,4 @@ -package com.example.placeservice.dto; +package com.example.placeservice.dto.attraction; import lombok.Data; diff --git a/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java b/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java new file mode 100644 index 0000000..903e55b --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java @@ -0,0 +1,146 @@ +package com.example.placeservice.dto.congestion; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import lombok.Data; + +import java.util.List; + +@Data +@XmlRootElement(name = "Map") +@XmlAccessorType(XmlAccessType.FIELD) +public class CongestionDto { + + @XmlElement(name="SeoulRtd.citydata_ppltn") + @JsonProperty("ppltn_data") + private PpltnData ppltnData; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class PpltnData { + + @XmlElement(name = "AREA_NM") + @JsonProperty("area_nm") + private String areaName; + + @XmlElement(name = "AREA_CD") + @JsonProperty("area_cd") + private String areaCode; + + @XmlElement(name = "AREA_CONGEST_LVL") + @JsonProperty("area_congest_lvl") + private String areaCongestLvl; + + @XmlElement(name = "AREA_CONGEST_MSG") + @JsonProperty("area_congest_msg") + private String areaCongestionMsg; + + @XmlElement(name = "AREA_PPLTN_MIN") + @JsonProperty("area_ppltn_min") + private int areaPpltnMin; + + @XmlElement(name = "AREA_PPLTN_MAX") + @JsonProperty("area_ppltn_max") + private int areaPpltnMax; + + @XmlElement(name = "MALE_PPLTN_RATE") + @JsonProperty("male_ppltn_rate") + private double malePpltnRate; + + @XmlElement(name = "FEMALE_PPLTN_RATE") + @JsonProperty("female_ppltn_rate") + private double femalePpltnRate; + + @XmlElement(name = "PPLTN_RATE_0") + @JsonProperty("ppltn_rate_0") + private double PpltnRate0; + + @XmlElement(name = "PPLTN_RATE_10") + @JsonProperty("ppltn_rate_10") + private double PpltnRate10; + + @XmlElement(name = "PPLTN_RATE_20") + @JsonProperty("ppltn_rate_20") + private double PpltnRate20; + + @XmlElement(name = "PPLTN_RATE_30") + @JsonProperty("ppltn_rate_30") + private double PpltnRate30; + + @XmlElement(name = "PPLTN_RATE_40") + @JsonProperty("ppltn_rate_40") + private double PpltnRate40; + + @XmlElement(name = "PPLTN_RATE_50") + @JsonProperty("ppltn_rate_50") + private double PpltnRate50; + + @XmlElement(name = "PPLTN_RATE_60") + @JsonProperty("ppltn_rate_60") + private double PpltnRate60; + + @XmlElement(name = "PPLTN_RATE_70") + @JsonProperty("ppltn_rate_70") + private double PpltnRate70; + + @XmlElement(name = "RESNT_PPLTN_RATE") + @JsonProperty("resnt_ppltn_rate") + private double ResntPpltnRate; + + @XmlElement(name = "NON_RESNT_PPLTN_RATE") + @JsonProperty("non_resnt_ppltn_rate") + private double NonResntPpltnRate; + + @XmlElement(name = "REPLACE_YN") + @JsonProperty("replace_yn") + private String replaceYn; + + @XmlElement(name = "PPLTN_TIME") + @JsonProperty("ppltn_time") + private String ppltnTime; + + @XmlElement(name = "FCST_YN") + @JsonProperty("fcst_yn") + private String fcstYn; + + @XmlElement(name = "FCST_PPLTN") + @JsonProperty("fcst_ppltn_wrapper") + private FcstPpltnWrapper FcstPpltnWrapper; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class FcstPpltnWrapper { + @XmlElement(name = "FCST_PPLTN") + @JsonProperty("fcst_ppltn") + private List FcstPpltn; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class FcstPpltn { + @XmlElement(name = "FCST_TIME") + @JsonProperty("fcst_time") + private String fcstTime; + + @XmlElement(name = "FCST_CONGEST_LVL") + @JsonProperty("fcst_congest_lvl") + private String fcstCongestLvl; + + @XmlElement(name = "FCST_PPLTN_MIN") + @JsonProperty("fcst_ppltn_min") + private int fcstPpltnMin; + + @XmlElement(name = "FCST_PPLTN_MAX") + @JsonProperty("fcst_ppltn_max") + private int fcstPpltnMax; + + } + + + + +} diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index 3c3b78f..87e694c 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -1,9 +1,9 @@ package com.example.placeservice.service; -import com.example.placeservice.dto.AreaAttractionsDto; -import com.example.placeservice.dto.AttractionDto; -import com.example.placeservice.dto.AttractionInfoDto; -import com.example.placeservice.dto.AttractionListDto; +import com.example.placeservice.dto.attraction.AreaAttractionsDto; +import com.example.placeservice.dto.attraction.AttractionDto; +import com.example.placeservice.dto.attraction.AttractionInfoDto; +import com.example.placeservice.dto.attraction.AttractionListDto; import com.example.placeservice.entity.Area; import com.example.placeservice.entity.Attraction; import com.example.placeservice.repository.AreaRepository; diff --git a/place-service/src/main/java/com/example/placeservice/service/CongestionService.java b/place-service/src/main/java/com/example/placeservice/service/CongestionService.java new file mode 100644 index 0000000..6f31875 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/CongestionService.java @@ -0,0 +1,53 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.congestion.CongestionDto; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.Unmarshaller; +import org.apache.kafka.common.errors.InvalidRequestException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.io.StringReader; + +@Service +public class CongestionService { + + @Value("http://openapi.seoul.go.kr:8088/sample/xml/citydata_ppltn/1/5/") + private String openApiUrl; + + private final RestTemplate restTemplate; + + public CongestionService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public String getCongestion(String areaName) { + try{ + // 1. 외부 OpenAPI에서 XML 데이터 받아오기 + String xmlResponse = restTemplate.getForObject(openApiUrl + areaName, String.class); + + // 2. XML 데이터를 DTO로 변환 -> JSON + ObjectMapper objectMapper = new ObjectMapper(); + String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); + + return json; + + }catch (InvalidRequestException e) { + throw new InvalidRequestException(e.getMessage(), e); + } catch(IOException e){ + throw new RuntimeException("외부 API 오류",e); + } catch (Exception e) { + throw new RuntimeException("예상치 못한 오류",e); + } + } + + private CongestionDto xmlToDto(String xml) throws Exception { + // JAXB를 사용해서 XML을 DTO로 변환 + JAXBContext context = JAXBContext.newInstance(CongestionDto.class); + Unmarshaller unmarshaller = context.createUnmarshaller(); + return (CongestionDto) unmarshaller.unmarshal(new StringReader(xml)); + } +} From fe66f7895e71e36f37c353cdf8e644970696c0ed Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Sat, 19 Apr 2025 14:44:21 +0900 Subject: [PATCH 055/361] =?UTF-8?q?Feat:CAFE=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/MainController.java | 32 ++++++++++++++++--- .../repository/CafeRepository.java | 3 ++ .../placeservice/service/CafeService.java | 22 +++++++++++++ 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/controller/MainController.java b/place-service/src/main/java/com/example/placeservice/controller/MainController.java index f7a7222..a799a9d 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/MainController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/MainController.java @@ -13,10 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; @Slf4j @@ -40,7 +37,7 @@ public ResponseEntity>> getCafeList() { List> cafeList = cafes.stream() .sorted(Comparator.comparing(Cafe::getId)) .map(cafe -> { - Map cafeMap = new HashMap<>(); + Map cafeMap = new LinkedHashMap<>(); cafeMap.put("id", cafe.getId().toString()); cafeMap.put("name", cafe.getName()); cafeMap.put("x", cafe.getLon().toString()); @@ -67,4 +64,29 @@ public ResponseEntity> getCafesByAreaId(@PathVariable Long areaId) return ResponseEntity.ok(sortedCafes); } + + @GetMapping("/info/cafe/{cafeId}") + public ResponseEntity> getCafeInfo(@PathVariable Long cafeId) { + log.info("카페 ID {}의 상세 정보 조회", cafeId); + + // 카페 ID로 상세 정보 조회 + Optional cafeOptional = cafeRepository.findById(cafeId); + + if (cafeOptional.isPresent()) { + Cafe cafe = cafeOptional.get(); + Map cafeInfo = new HashMap<>(); + cafeInfo.put("id", cafe.getId()); + cafeInfo.put("name", cafe.getName()); + cafeInfo.put("address", cafe.getAddress()); + cafeInfo.put("phone", cafe.getPhone()); + cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl()); + cafeInfo.put("lat", cafe.getLat()); + cafeInfo.put("lon", cafe.getLon()); + cafeInfo.put("category_code", cafe.getCategoryCode()); + + return ResponseEntity.ok(Map.of("data", cafeInfo)); + } + + return ResponseEntity.notFound().build(); + } } \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java index eb8e1eb..b14fb72 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Optional; public interface CafeRepository extends JpaRepository { @Query("SELECT c FROM Cafe c WHERE c.area.area_id = :areaId") @@ -15,4 +16,6 @@ public interface CafeRepository extends JpaRepository { @Modifying @Query("DELETE FROM Cafe c WHERE c.area.area_id = :areaId") void deleteByAreaId(Long areaId); + + Optional findByKakaomapUrl(String placeCode); } diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java index da4e1d6..ef4c974 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CafeService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java @@ -15,6 +15,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; @Slf4j @@ -157,4 +158,25 @@ private CafeDto convertToDto(Cafe cafe) { .categoryCode(cafe.getCategoryCode()) .build(); } + + @Transactional(readOnly = true) + public Map getCafeDetailByPlaceCode(String placeCode) { + Optional cafeOptional = cafeRepository.findByKakaomapUrl(placeCode); + + if (cafeOptional.isPresent()) { + Cafe cafe = cafeOptional.get(); + Map cafeInfo = new HashMap<>(); + cafeInfo.put("name", cafe.getName()); + cafeInfo.put("address", cafe.getAddress()); + cafeInfo.put("phone", cafe.getPhone()); + cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl()); + cafeInfo.put("lat", cafe.getLat()); + cafeInfo.put("lon", cafe.getLon()); + cafeInfo.put("category_code", cafe.getCategoryCode()); + + return cafeInfo; + } + + return null; + } } \ No newline at end of file From 48ee63f02f14a1060b73ccdd430ccd9c27595dfd Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Sat, 19 Apr 2025 14:44:21 +0900 Subject: [PATCH 056/361] =?UTF-8?q?Feat:CAFE=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/MainController.java | 32 ++++++++++++++++--- .../repository/CafeRepository.java | 3 ++ .../placeservice/service/CafeService.java | 22 +++++++++++++ 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/controller/MainController.java b/place-service/src/main/java/com/example/placeservice/controller/MainController.java index f7a7222..a799a9d 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/MainController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/MainController.java @@ -13,10 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; @Slf4j @@ -40,7 +37,7 @@ public ResponseEntity>> getCafeList() { List> cafeList = cafes.stream() .sorted(Comparator.comparing(Cafe::getId)) .map(cafe -> { - Map cafeMap = new HashMap<>(); + Map cafeMap = new LinkedHashMap<>(); cafeMap.put("id", cafe.getId().toString()); cafeMap.put("name", cafe.getName()); cafeMap.put("x", cafe.getLon().toString()); @@ -67,4 +64,29 @@ public ResponseEntity> getCafesByAreaId(@PathVariable Long areaId) return ResponseEntity.ok(sortedCafes); } + + @GetMapping("/info/cafe/{cafeId}") + public ResponseEntity> getCafeInfo(@PathVariable Long cafeId) { + log.info("카페 ID {}의 상세 정보 조회", cafeId); + + // 카페 ID로 상세 정보 조회 + Optional cafeOptional = cafeRepository.findById(cafeId); + + if (cafeOptional.isPresent()) { + Cafe cafe = cafeOptional.get(); + Map cafeInfo = new HashMap<>(); + cafeInfo.put("id", cafe.getId()); + cafeInfo.put("name", cafe.getName()); + cafeInfo.put("address", cafe.getAddress()); + cafeInfo.put("phone", cafe.getPhone()); + cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl()); + cafeInfo.put("lat", cafe.getLat()); + cafeInfo.put("lon", cafe.getLon()); + cafeInfo.put("category_code", cafe.getCategoryCode()); + + return ResponseEntity.ok(Map.of("data", cafeInfo)); + } + + return ResponseEntity.notFound().build(); + } } \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java index eb8e1eb..b14fb72 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Optional; public interface CafeRepository extends JpaRepository { @Query("SELECT c FROM Cafe c WHERE c.area.area_id = :areaId") @@ -15,4 +16,6 @@ public interface CafeRepository extends JpaRepository { @Modifying @Query("DELETE FROM Cafe c WHERE c.area.area_id = :areaId") void deleteByAreaId(Long areaId); + + Optional findByKakaomapUrl(String placeCode); } diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java index da4e1d6..ef4c974 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CafeService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java @@ -15,6 +15,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; @Slf4j @@ -157,4 +158,25 @@ private CafeDto convertToDto(Cafe cafe) { .categoryCode(cafe.getCategoryCode()) .build(); } + + @Transactional(readOnly = true) + public Map getCafeDetailByPlaceCode(String placeCode) { + Optional cafeOptional = cafeRepository.findByKakaomapUrl(placeCode); + + if (cafeOptional.isPresent()) { + Cafe cafe = cafeOptional.get(); + Map cafeInfo = new HashMap<>(); + cafeInfo.put("name", cafe.getName()); + cafeInfo.put("address", cafe.getAddress()); + cafeInfo.put("phone", cafe.getPhone()); + cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl()); + cafeInfo.put("lat", cafe.getLat()); + cafeInfo.put("lon", cafe.getLon()); + cafeInfo.put("category_code", cafe.getCategoryCode()); + + return cafeInfo; + } + + return null; + } } \ No newline at end of file From 5ecca205ccb7dab2962dacf7bceaa1f43be8dab0 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Sat, 19 Apr 2025 14:53:34 +0900 Subject: [PATCH 057/361] =?UTF-8?q?Feat:CAFE=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/placeservice/controller/MainController.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/controller/MainController.java b/place-service/src/main/java/com/example/placeservice/controller/MainController.java index a799a9d..70a749c 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/MainController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/MainController.java @@ -74,17 +74,20 @@ public ResponseEntity> getCafeInfo(@PathVariable Long cafeId if (cafeOptional.isPresent()) { Cafe cafe = cafeOptional.get(); - Map cafeInfo = new HashMap<>(); + Map cafeInfo = new LinkedHashMap<>(); cafeInfo.put("id", cafe.getId()); cafeInfo.put("name", cafe.getName()); cafeInfo.put("address", cafe.getAddress()); cafeInfo.put("phone", cafe.getPhone()); + cafeInfo.put("category_code", cafe.getCategoryCode()); cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl()); cafeInfo.put("lat", cafe.getLat()); cafeInfo.put("lon", cafe.getLon()); - cafeInfo.put("category_code", cafe.getCategoryCode()); - return ResponseEntity.ok(Map.of("data", cafeInfo)); + Map response = new LinkedHashMap<>(); + response.put("data", cafeInfo); + + return ResponseEntity.ok(response); } return ResponseEntity.notFound().build(); From f4c510e94dcfc5d4d410c66e8cf062db882cb773 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Sat, 19 Apr 2025 14:53:34 +0900 Subject: [PATCH 058/361] =?UTF-8?q?Feat:CAFE=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/placeservice/controller/MainController.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/controller/MainController.java b/place-service/src/main/java/com/example/placeservice/controller/MainController.java index a799a9d..70a749c 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/MainController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/MainController.java @@ -74,17 +74,20 @@ public ResponseEntity> getCafeInfo(@PathVariable Long cafeId if (cafeOptional.isPresent()) { Cafe cafe = cafeOptional.get(); - Map cafeInfo = new HashMap<>(); + Map cafeInfo = new LinkedHashMap<>(); cafeInfo.put("id", cafe.getId()); cafeInfo.put("name", cafe.getName()); cafeInfo.put("address", cafe.getAddress()); cafeInfo.put("phone", cafe.getPhone()); + cafeInfo.put("category_code", cafe.getCategoryCode()); cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl()); cafeInfo.put("lat", cafe.getLat()); cafeInfo.put("lon", cafe.getLon()); - cafeInfo.put("category_code", cafe.getCategoryCode()); - return ResponseEntity.ok(Map.of("data", cafeInfo)); + Map response = new LinkedHashMap<>(); + response.put("data", cafeInfo); + + return ResponseEntity.ok(response); } return ResponseEntity.notFound().build(); From 3076b880f47d31bfb40b07416c067d4f3c30e83b Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Sun, 20 Apr 2025 23:05:26 +0900 Subject: [PATCH 059/361] =?UTF-8?q?=EC=9D=8C=EC=8B=9D=EC=A0=90=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A0=80=EC=9E=A5=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeservice/config/SecurityConfig.java | 24 ++++ .../controller/RestaurantController.java | 41 ++++++ .../dto/RestaurantDetailResponse.java | 45 +++++++ .../dto/RestaurantListResponse.java | 23 ++++ .../com/example/placeservice/entity/Area.java | 35 +++++- .../placeservice/entity/Restaurant.java | 53 ++++++++ .../repository/RestaurantRepository.java | 8 ++ .../placeservice/service/KakaoMapClient.java | 48 +++++++ .../service/RestaurantListService.java | 34 +++++ .../service/RestaurantService.java | 95 ++++++++++++++ .../src/main/resources/application.properties | 15 ++- place-service/src/main/resources/data.sql | 117 ++++++++++++++++++ 12 files changed, 530 insertions(+), 8 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java create mode 100644 place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/RestaurantListResponse.java create mode 100644 place-service/src/main/java/com/example/placeservice/entity/Restaurant.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/RestaurantService.java create mode 100644 place-service/src/main/resources/data.sql diff --git a/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java new file mode 100644 index 0000000..682569d --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java @@ -0,0 +1,24 @@ +package com.example.placeservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import static org.springframework.security.config.Customizer.withDefaults; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() // 모든 요청 허용 + ); + + return http.build(); + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java b/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java new file mode 100644 index 0000000..3b79a1a --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java @@ -0,0 +1,41 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.dto.RestaurantDetailResponse; +import com.example.placeservice.dto.RestaurantListResponse; +import com.example.placeservice.service.RestaurantListService; +import com.example.placeservice.service.RestaurantService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/main") +@RequiredArgsConstructor +public class RestaurantController { + + private final RestaurantService restaurantService; + private final RestaurantListService restaurantListService; + + // 전체 지역 기준으로 음식점 정보 저장 + @PostMapping("/fetch") + public ResponseEntity fetchRestaurants() { + restaurantService.fetchAndSaveRestaurants(); + return ResponseEntity.ok("음식점 데이터 저장 완료!"); + } + + //음식점 목록 제공 + @GetMapping("/restaurant/list") + public ResponseEntity> getRestaurantList() { + List restaurantList = restaurantListService.getRestaurantList(); + return ResponseEntity.ok(restaurantList); + } + + //음식점 상세 정보 제공 + @GetMapping("/info/restaurant/{restaurantId}") + public ResponseEntity getRestaurantDetail(@PathVariable Long restaurantId) { + RestaurantDetailResponse restaurantDetail = restaurantListService.getRestaurantDetail(restaurantId); + return ResponseEntity.ok(restaurantDetail); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java b/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java new file mode 100644 index 0000000..e9ce8d5 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java @@ -0,0 +1,45 @@ +//추가 +package com.example.placeservice.dto; + +import com.example.placeservice.entity.Restaurant; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class RestaurantDetailResponse { + + private String address_name; + private String category_group_code; + private String category_group_name; // 고정 X, DB 값 + private String category_name; // 고정 X, DB 값 + private String distance; // 빈 값 유지 + private String id; + private String phone; + private String place_name; + private String place_url; + private String road_address_name; + private String lat; + private String lon; + private String categoryGroupName; // 추가 + private String categoryName; // 추가 + + public static RestaurantDetailResponse fromEntity(Restaurant restaurant) { + return RestaurantDetailResponse.builder() + .address_name(restaurant.getAddress()) + .category_group_code(restaurant.getCategory_code()) + .category_group_name(restaurant.getCategoryGroupName()) // 고정X (DB) + .category_name(restaurant.getCategoryName()) // 고정X (DB) + .distance("") // 그대로 비워두기 + .id(String.valueOf(restaurant.getRestaurantId())) + .phone(restaurant.getPhone()) + .place_name(restaurant.getName()) + .place_url(restaurant.getKakaomap_url()) + .categoryGroupName(restaurant.getCategoryGroupName()) // 추가 + .categoryName(restaurant.getCategoryName()) // 추가 + .road_address_name(restaurant.getAddress()) // 현재 Address랑 Road Address가 같게 저장된 경우 + .lat(restaurant.getLat().toString()) + .lon(restaurant.getLon().toString()) + .build(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/RestaurantListResponse.java b/place-service/src/main/java/com/example/placeservice/dto/RestaurantListResponse.java new file mode 100644 index 0000000..2a1f5c5 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/RestaurantListResponse.java @@ -0,0 +1,23 @@ +package com.example.placeservice.dto; +//추가 +import com.example.placeservice.entity.Restaurant; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class RestaurantListResponse { + private Long id; + private String name; + private String lat; + private String lon; + + public static RestaurantListResponse fromEntity(Restaurant restaurant) { + return RestaurantListResponse.builder() + .id(restaurant.getRestaurantId()) + .name(restaurant.getName()) + .lat(restaurant.getLat().toString()) + .lon(restaurant.getLon().toString()) + .build(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index faa2087..edac94f 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -1,27 +1,52 @@ package com.example.placeservice.entity; import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; import java.math.BigDecimal; @Entity +@Getter +@Setter public class Area { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "area_id") private Long area_id; + @Column(name = "seoul_id", length = 10, nullable = false) + private String seoul_id; + @Column(name = "name", length = 50, nullable = false) private String name; // 지역명 + @Column(name = "name_eng", length = 100, nullable = false) + private String name_eng; + @Column(name = "category", length = 20, nullable = false) private String category; // 구분 - @Column(name = "x", precision = 10, scale = 6, nullable = false) - private BigDecimal x; // 위도 + @Column(name = "lat", precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 + + @Column(name = "lon", precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 + + + public Object getName() { + return this.name; + } - @Column(name = "y", precision = 10, scale = 6, nullable = false) - private BigDecimal y; // 경도 + public Long getAreaId() { + return this.area_id; + } + public Number getY() { + return this.lon; + } -} + public Number getX() { + return this.lat; + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java b/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java new file mode 100644 index 0000000..36ae9a3 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java @@ -0,0 +1,53 @@ +package com.example.placeservice.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; + +@Entity +@Getter +@Setter +@Table(name = "restaurant") // 테이블명 명시 +public class Restaurant { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "restaurant_id") + private Long restaurantId; // PK + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "area_id", nullable = false) + private Area area; // Area 엔티티와 다대일 관계 + + @Column(length = 50, nullable = false) + private String name; // 음식점 이름 + + @Column(length = 200) + private String address; // 음식점 주소 + + @Column(precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 + + @Column(precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 + + @Column(length = 20) + private String phone; // 전화번호 + + @Column(length = 200) + private String kakaomap_url; // 카카오맵 URL + + @Column(length = 20) + private String category_code; // 카테고리 코드 (FD6: 음식점) + + + //추가 + @Column(length = 100) + private String categoryGroupName; // (예시) 음식점 + + @Column(length = 200) + private String categoryName; // (예시) 음식점 > 한식 > 육류,고기 + +} diff --git a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java new file mode 100644 index 0000000..ba08e9f --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java @@ -0,0 +1,8 @@ +package com.example.placeservice.repository; + +import com.example.placeservice.entity.Restaurant; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RestaurantRepository extends JpaRepository { + // 기본 CRUD 사용 (findAll, findById, save 등) +} diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java new file mode 100644 index 0000000..0df3c35 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java @@ -0,0 +1,48 @@ +package com.example.placeservice.service; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.math.BigDecimal; + +@Service +@RequiredArgsConstructor +public class KakaoMapClient { + + @Value("${kakao.api.key}") + private String kakaoApiKey; + + public JsonNode searchRestaurantsByCategory(BigDecimal lon, BigDecimal lat, int page) { + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "KakaoAK " + kakaoApiKey); + + // Kakao API 호출 URL 세팅 + UriComponentsBuilder builder = UriComponentsBuilder + .fromHttpUrl("https://dapi.kakao.com/v2/local/search/category.json") + .queryParam("category_group_code", "FD6") // FD6: 음식점 카테고리 + .queryParam("x", lon) // x: 경도(lon) + .queryParam("y", lat) // y: 위도(lat) + .queryParam("sort", "accuracy") // 정확도순 정렬 + .queryParam("page", page) // 요청 페이지 + .queryParam("size", 15); // 한 페이지에 최대 15개 결과 + + HttpEntity entity = new HttpEntity<>(headers); + + // REST API 호출 + ResponseEntity response = restTemplate.exchange( + builder.toUriString(), + HttpMethod.GET, + entity, + JsonNode.class + ); + + return response.getBody(); // JSON 응답 반환 + } +} diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java new file mode 100644 index 0000000..d092009 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java @@ -0,0 +1,34 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.RestaurantDetailResponse; +import com.example.placeservice.dto.RestaurantListResponse; +import com.example.placeservice.entity.Restaurant; +import com.example.placeservice.repository.RestaurantRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class RestaurantListService { + + private final RestaurantRepository restaurantRepository; + + @Transactional(readOnly = true) + public List getRestaurantList() { + return restaurantRepository.findAll() + .stream() + .map(RestaurantListResponse::fromEntity) + .collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public RestaurantDetailResponse getRestaurantDetail(Long id) { + Restaurant restaurant = restaurantRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("해당 음식점이 존재하지 않습니다. id=" + id)); + return RestaurantDetailResponse.fromEntity(restaurant); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java new file mode 100644 index 0000000..7195059 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java @@ -0,0 +1,95 @@ +package com.example.placeservice.service; + +import com.example.placeservice.entity.Area; +import com.example.placeservice.entity.Restaurant; +import com.example.placeservice.repository.AreaRepository; +import com.example.placeservice.repository.RestaurantRepository; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class RestaurantService { + + private final AreaRepository areaRepository; + private final RestaurantRepository restaurantRepository; + private final KakaoMapClient kakaoMapClient; + + // 특정 Area 기준으로 주변 음식점 저장 + public List saveRestaurantsNearbyArea(Area area) { + List savedRestaurants = new ArrayList<>(); + int page = 1; + boolean isEnd = false; + double searchRadius = 1000.0; // 반경 1km + + while (!isEnd) { + // Area의 lat, lon 순서를 반대로 호출해야 함 (DB상에 값이 반대임) + JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLat(), area.getLon(), page); + + if (response.has("documents")) { + for (JsonNode doc : response.get("documents")) { + double restaurantLon = Double.parseDouble(doc.get("x").asText()); + double restaurantLat = Double.parseDouble(doc.get("y").asText()); + + // 거리 계산할 때도 lat, lon 순서 조심 (Area에서 꺼낼 때) + double distance = calculateDistance( + area.getLon().doubleValue(), area.getLat().doubleValue(), + restaurantLat, restaurantLon + ); + + // 1km 이내 음식점만 저장 + if (distance <= searchRadius) { + Restaurant restaurant = new Restaurant(); + restaurant.setArea(area); + restaurant.setName(doc.get("place_name").asText()); + restaurant.setAddress(doc.get("road_address_name").asText()); + restaurant.setLat(new BigDecimal(doc.get("y").asText())); // Kakao API에서 y: lat + restaurant.setLon(new BigDecimal(doc.get("x").asText())); // Kakao API에서 x: lon + restaurant.setPhone(doc.has("phone") ? doc.get("phone").asText() : null); + restaurant.setKakaomap_url(doc.get("place_url").asText()); + restaurant.setCategory_code(doc.has("category_group_code") ? doc.get("category_group_code").asText() : null); + + savedRestaurants.add(restaurant); + restaurantRepository.save(restaurant); + } + } + + isEnd = response.get("meta").get("is_end").asBoolean(); + page++; + } else { + isEnd = true; + } + } + return savedRestaurants; + } + + // 전체 Area 대상 음식점 저장 (fetch & save) + @Transactional + public void fetchAndSaveRestaurants() { + List areas = areaRepository.findAll(); // 모든 지역 조회 + for (Area area : areas) { + saveRestaurantsNearbyArea(area); // 각 지역별 저장 + } + } + + // Haversine 거리 계산 공식 + private double calculateDistance(double lat1, double lon1, double lat2, double lon2) { + final int EARTH_RADIUS = 6371; // 지구 반지름 (km) + + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + + double a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) + * Math.sin(dLon/2) * Math.sin(dLon/2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + + return EARTH_RADIUS * c * 1000; // meter 단위로 반환 + } +} diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index d5cbebf..439bd92 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -2,10 +2,19 @@ spring.application.name=place-service # postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/star -spring.datasource.username=admin +spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db +spring.datasource.username=root spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update +spring.jpa.defer-datasource-initialization=true +spring.jpa.hibernate.ddl-auto=create +spring.jpa.show-sql=true +spring.sql.init.mode=always + +# ??? API ? ?? +kakao.api.key=585808366c299a6686e7104a193017bb + +# ?? ?? +logging.level.com.example.placeservice=INFO \ No newline at end of file diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql new file mode 100644 index 0000000..6ecb39a --- /dev/null +++ b/place-service/src/main/resources/data.sql @@ -0,0 +1,117 @@ +INSERT INTO area (seoul_id, name, name_eng, category, lon, lat) VALUES + ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), + ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), + ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), + ('POI004', '이태원 관광특구', 'Itaewon Special Tourist Zone', '관광특구', 37.53475718815058, 126.99591956660342), + ('POI005', '잠실 관광특구', 'Jamsil Special Tourist Zone', '관광특구', 37.51868923685722, 127.11694379878114), + ('POI006', '종로·청계 관광특구', 'Jongno Cheonggye Special Toruist Zone', '관광특구', 37.56984229024498, 126.99866678943675), + ('POI007', '홍대 관광특구', 'HongDae Culture & Arts Special Tourist Zone', '관광특구', 37.55506170187393, 126.92159115672915), + ('POI008', '경복궁', 'Gyeongbokgung Palace', '고궁·문화유산', 37.5809150911806, 126.97764726625209), + ('POI009', '광화문·덕수궁', 'Gwanghwamun & Deoksugung Palace', '고궁·문화유산', 37.56847081447724, 126.97620594802834), + ('POI010', '보신각', 'Bosingak', '고궁·문화유산', 37.57089797616037, 126.98337607511172), + ('POI011', '서울 암사동 유적', 'Amsa Prehistoric Settlement Site', '고궁·문화유산', 37.56102809827398, 127.13078491934499), + ('POI012', '창덕궁·종묘', 'Changdeokgung Palace & Jongmyo Shrine', '고궁·문화유산', 37.5795304952381, 126.99416802285715), + ('POI013', '가산디지털단지역', 'Gasan Digital Complex station', '인구밀집지역', 37.48267268376068, 126.88014526210826), + ('POI014', '강남역', 'Gangnam station', '인구밀집지역', 37.50019794543511, 127.02822145618981), + ('POI015', '건대입구역', 'Konkuk University station', '인구밀집지역', 37.540005312302846, 127.06718185804415), + ('POI016', '고덕역', 'Godeok station', '인구밀집지역', 37.553377141111774, 127.15503341723486), + ('POI017', '고속터미널역', 'Express Bus Terminal station', '인구밀집지역', 37.50472316731823, 127.00542276570697), + ('POI018', '교대역', 'Seoul National University of Education station', '인구밀집지역', 37.49185444557083, 127.01456630812736), + ('POI019', '구로디지털단지역', 'Guro Digital Complex station', '인구밀집지역', 37.48349491938897, 126.89557915984915), + ('POI020', '구로역', 'Guro station', '인구밀집지역', 37.501517014484925, 126.88285531403136), + ('POI021', '군자역', 'Gunja station', '인구밀집지역', 37.555916439038526, 127.08008933356267), + ('POI022', '남구로역', 'Namguro station', '인구밀집지역', 37.484184300878496, 126.88539683418863), + ('POI023', '대림역', 'Daerim station', '인구밀집지역', 37.492818237753816, 126.89567307890546), + ('POI024', '동대문역', 'Dongdaemun station', '인구밀집지역', 37.57127393968462, 127.0096854515538), + ('POI025', '뚝섬역', 'Ttukseom station', '인구밀집지역', 37.548249670448335, 127.04603079475764), + ('POI026', '미아사거리역', 'Miasageori station', '인구밀집지역', 37.61244898963818, 127.0305835226811), + ('POI027', '발산역', 'Balsan station', '인구밀집지역', 37.55796811527854, 126.84179723439551), + ('POI028', '북한산우이역', 'Bukhansan Ui station', '인구밀집지역', 37.66281154490492, 127.01070580250341), + ('POI029', '사당역', 'Sadang station', '인구밀집지역', 37.47635049189678, 126.98129519152415), + ('POI030', '삼각지역', 'Samgakji station', '인구밀집지역', 37.535323919468816, 126.97392611189508), + ('POI031', '서울대입구역', 'Seoul National University station', '인구밀집지역', 37.48060991955129, 126.95360407634334), + ('POI032', '서울식물원·마곡나루역', 'Seoul Botanic Park·Magongnaru station', '인구밀집지역', 37.56745047572558, 126.82986126533689), + ('POI033', '서울역', 'Seoul station', '인구밀집지역', 37.55662336923077, 126.97216878769233), + ('POI034', '선릉역', 'Seolleung station', '인구밀집지역', 37.50584166753116, 127.0489100692203), + ('POI035', '성신여대입구역', 'Sungshin Women''s University station', '인구밀집지역', 37.59262801654558, 127.01658994286346), + ('POI036', '수유역', 'Suyu station', '인구밀집지역', 37.641215514705884, 127.02565393627451), + ('POI037', '신논현역·논현역', 'Sinnonhyeon·Nonhyeon station', '인구밀집지역', 37.50848054554439, 127.02291493850254), + ('POI038', '신도림역', 'Sindorim station', '인구밀집지역', 37.50941493666961, 126.89096014067083), + ('POI039', '신림역', 'Sillim station', '인구밀집지역', 37.48396968904173, 126.92987028995233), + ('POI040', '신촌·이대역', 'Sinchon·Ewha Womans University station', '인구밀집지역', 37.55564146783625, 126.93761215497075), + ('POI041', '양재역', 'Yangjae station', '인구밀집지역', 37.48451422353994, 127.03551115644439), + ('POI042', '역삼역', 'Yeoksam station', '인구밀집지역', 37.50011177778084, 127.03721339389585), + ('POI043', '연신내역', 'Yeonsinnae station', '인구밀집지역', 37.61903547457627, 126.92180184322034), + ('POI044', '오목교역·목동운동장', 'Omokgyo·Mok-dong Stadium station', '인구밀집지역', 37.53174788866626, 126.87924768246015), + ('POI045', '왕십리역', 'Wangsimni station', '인구밀집지역', 37.561636745463275, 127.03857508984515), + ('POI046', '용산역', 'Yongsan station', '인구밀집지역', 37.53059674809017, 126.95953798376128), + ('POI047', '이태원역', 'Itaewon station', '인구밀집지역', 37.534847968613946, 126.99261390234051), + ('POI048', '장지역', 'Jangji station', '인구밀집지역', 37.47861018962738, 127.12424263037641), + ('POI049', '장한평역', 'Janghanpyeong station', '인구밀집지역', 37.562849061361526, 127.06235115915185), + ('POI050', '천호역', 'Cheonho station', '인구밀집지역', 37.53921065146112, 127.12470374198884), + ('POI051', '총신대입구(이수)역', 'Chongshin University(Isu) station', '인구밀집지역', 37.48677821828824, 126.9807539648491), + ('POI052', '충정로역', 'Chungjeongno station', '인구밀집지역', 37.559524839900476, 126.9642059673645), + ('POI053', '합정역', 'Hapjeong station', '인구밀집지역', 37.54857307399022, 126.91084543497917), + ('POI054', '혜화역', 'Hyehwa station', '인구밀집지역', 37.58237426286851, 127.0014404389915), + ('POI055', '홍대입구역(2호선)', 'Hongik University station(Line 2)', '인구밀집지역', 37.5560688250626, 126.92329766229786), + ('POI056', '회기역', 'Hoegi station', '인구밀집지역', 37.590538258503685, 127.05700039395965), + ('POI057', '4·19 카페거리', '4·19 Cafe street', '발달상권', 37.64481185893745, 127.00739701908337), + ('POI058', '가락시장', 'Garak Market', '발달상권', 37.49445476721721, 127.11054976163454), + ('POI059', '가로수길', 'Garosu-gil', '발달상권', 37.522662622516556, 127.02543305960263), + ('POI060', '광장(전통)시장', 'Gwangjang(Traditional) Market', '발달상권', 37.569817036518465, 127.00014893169192), + ('POI061', '김포공항', 'Gimpo Airport', '발달상권', 37.56228914604604, 126.80289965159135), + ('POI062', '낙산공원·이화마을', 'Naksan Park·Ihwa Village', '발달상권', 37.580295002146926, 127.00692469978675), + ('POI063', '노량진', 'Noryangjin', '발달상권', 37.513690699957316, 126.94907070192423), + ('POI064', '덕수궁길·정동길', 'Deoksugung-gil·Jeongdong-gil', '발달상권', 37.5659744042236, 126.97244482634493), + ('POI065', '방배역 먹자골목', 'Bangbae food alley', '발달상권', 37.48234102283986, 126.99597969571558), + ('POI066', '북촌한옥마을', 'Bukchon Hanok Village', '발달상권', 37.58425649416171, 126.98386049085175), + ('POI067', '서촌', 'Seochon', '발달상권', 37.58018833392947, 126.9666234325548), + ('POI068', '성수카페거리', 'Seongsu Cafe Street', '발달상권', 37.54227657813654, 127.05560237183015), + ('POI069', '수유리 먹자골목', 'Suyuri food alley', '발달상권', 37.64270823057644, 127.02733855833522), + ('POI070', '쌍문동 맛집거리', 'Ssangmun-dong restaurant street', '발달상권', 37.6477285443038, 127.03242528481013), + ('POI071', '압구정로데오거리', 'Apgujeong Rodeo Street', '발달상권', 37.52560441860466, 127.04040171511626), + ('POI072', '여의도', 'Yeouido', '발달상권', 37.52549260943663, 126.92338937129487), + ('POI073', '연남동', 'Yeonnam-dong', '발달상권', 37.561426426204264, 126.92406273242905), + ('POI074', '영등포 타임스퀘어', 'Yeongdeungpo Time square', '발달상권', 37.51653179760669, 126.90523931614634), + ('POI075', '외대앞', 'Hankuk University of Foreign Studies', '발달상권', 37.59442412843283, 127.06153833102397), + ('POI076', '용리단길', 'Yongnidan-gil', '발달상권', 37.5311056439386, 126.97147359358237), + ('POI077', '이태원 앤틱가구거리', 'Itaewon Antiques street', '발달상권', 37.53187270743738, 126.99343376513602), + ('POI078', '인사동·익선동', 'Insa-dong·Ikseon-dong', '발달상권', 37.5741577489609, 126.98736374162822), + ('POI079', '창동 신경제 중심지', 'Changdong New Economic Center', '발달상권', 37.656167961335676, 127.05328808787348), + ('POI080', '청담동 명품거리', 'Cheongdam-dong Luxury Fashion street', '발달상권', 37.52645537152884, 127.04354058968364), + ('POI081', '청량리 제기동 일대 전통시장', 'Traditional market in Cheongnyangni Jegi-dong', '발달상권', 37.579977738703214, 127.0382790126348), + ('POI082', '해방촌·경리단길', 'Haebangchon·Gyeongnidan-gil', '발달상권', 37.54143829714048, 126.98836800969178), + ('POI083', 'DDP(동대문디자인플라자)', 'DDP(Dongdaemun Design Plaza)', '발달상권', 37.56654201564661, 127.00993045294622), + ('POI084', 'DMC(디지털미디어시티)', 'DMC(Digital Media City)', '발달상권', 37.577407110655734, 126.8936691147541), + ('POI085', '강서한강공원', 'Gangseo Hangang Park', '공원', 37.5858708295553, 126.82002478578644), + ('POI086', '고척돔', 'Gocheok Dome', '공원', 37.49776790791694, 126.86713336195083), + ('POI087', '광나루한강공원', 'Gwangnaru Hangang Park', '공원', 37.550866089267345, 127.12370647922764), + ('POI088', '광화문광장', 'Gwanghwamun Square', '공원', 37.57505062349029, 126.97680679071502), + ('POI089', '국립중앙박물관·용산가족공원', 'The National Museum of Korea·Yongsan Family Park', '공원', 37.52202119313341, 126.9824679850178), + ('POI090', '난지한강공원', 'Nanji Hangang Park', '공원', 37.566316740172454, 126.87770001597329), + ('POI091', '남산공원', 'Namsan Park', '공원', 37.555070650369004, 126.99365012730628), + ('POI092', '노들섬', 'Nodeul island', '공원', 37.517556872131706, 126.95871397855296), + ('POI093', '뚝섬한강공원', 'Ttukseom Hangang Park', '공원', 37.52882503002481, 127.07460742502332), + ('POI094', '망원한강공원', 'Mangwon Hangang Park', '공원', 37.55480087656761, 126.89823749216892), + ('POI095', '반포한강공원', 'Banpo Hangang Park', '공원', 37.51065462735107, 126.99457550649858), + ('POI096', '북서울꿈의숲', 'Dream Forest', '공원', 37.62244979881656, 127.04069568441815), + ('POI097', '불광천', 'Bulgwangcheon River', '공원', 37.58940646530798, 126.91194122899687), + ('POI098', '서리풀공원·몽마르뜨공원', 'Seoripul Park·Montmartre Park', '공원', 37.49022054658995, 127.00173655775917), + ('POI100', '서울대공원', 'Seoul Grand Park', '공원', 37.42839607329216, 127.01863790236284), + ('POI101', '서울숲공원', 'Seoul Forest', '공원', 37.54402680167043, 127.03673083133076), + ('POI099', '서울광장', 'Seoul Plaza', '공원', 37.5655010296151, 126.97798702124584), + ('POI102', '아차산', 'Achasan', '공원', 37.56307021845223, 127.10047918048643), + ('POI103', '양화한강공원', 'Yanghwa Hangang Park', '공원', 37.54029653900909, 126.89963558394624), + ('POI104', '어린이대공원', 'Children''s Grand Park', '공원', 37.54957100980681, 127.0824313180458), + ('POI105', '여의도한강공원', 'Yeouido Hangang Park', '공원', 37.52868909088513, 126.92627587984478), + ('POI106', '월드컵공원', 'World Cup Park', '공원', 37.5660391150387, 126.88740201899091), + ('POI107', '응봉산', 'Eungbongsan', '공원', 37.54797346368485, 127.02904643771303), + ('POI108', '이촌한강공원', 'Ichon Hangang Park', '공원', 37.52139035897837, 126.96356631928725), + ('POI109', '잠실종합운동장', 'Jamsil (Seoul) Sports Complex', '공원', 37.51610665876664, 127.07230325047283), + ('POI110', '잠실한강공원', 'Jamsil Hangang Park', '공원', 37.51981181577704, 127.08507449607517), + ('POI111', '잠원한강공원', 'Jamwon Hangang Park', '공원', 37.52336245003122, 127.01397131593632), + ('POI112', '청계산', 'Cheonggyesan', '공원', 37.443365424959026, 127.0469697170313), + ('POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264), + ('POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616), + ('POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821), + ('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286); \ No newline at end of file From b890318532cca90f88c1894c941d422cfc05017b Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Sun, 20 Apr 2025 23:05:26 +0900 Subject: [PATCH 060/361] =?UTF-8?q?=EC=9D=8C=EC=8B=9D=EC=A0=90=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A0=80=EC=9E=A5=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeservice/config/SecurityConfig.java | 24 ++++ .../controller/RestaurantController.java | 41 ++++++ .../dto/RestaurantDetailResponse.java | 45 +++++++ .../dto/RestaurantListResponse.java | 23 ++++ .../com/example/placeservice/entity/Area.java | 35 +++++- .../placeservice/entity/Restaurant.java | 53 ++++++++ .../repository/RestaurantRepository.java | 8 ++ .../placeservice/service/KakaoMapClient.java | 48 +++++++ .../service/RestaurantListService.java | 34 +++++ .../service/RestaurantService.java | 95 ++++++++++++++ .../src/main/resources/application.properties | 15 ++- place-service/src/main/resources/data.sql | 117 ++++++++++++++++++ 12 files changed, 530 insertions(+), 8 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java create mode 100644 place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/RestaurantListResponse.java create mode 100644 place-service/src/main/java/com/example/placeservice/entity/Restaurant.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/RestaurantService.java create mode 100644 place-service/src/main/resources/data.sql diff --git a/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java new file mode 100644 index 0000000..682569d --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java @@ -0,0 +1,24 @@ +package com.example.placeservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import static org.springframework.security.config.Customizer.withDefaults; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() // 모든 요청 허용 + ); + + return http.build(); + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java b/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java new file mode 100644 index 0000000..3b79a1a --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java @@ -0,0 +1,41 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.dto.RestaurantDetailResponse; +import com.example.placeservice.dto.RestaurantListResponse; +import com.example.placeservice.service.RestaurantListService; +import com.example.placeservice.service.RestaurantService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/main") +@RequiredArgsConstructor +public class RestaurantController { + + private final RestaurantService restaurantService; + private final RestaurantListService restaurantListService; + + // 전체 지역 기준으로 음식점 정보 저장 + @PostMapping("/fetch") + public ResponseEntity fetchRestaurants() { + restaurantService.fetchAndSaveRestaurants(); + return ResponseEntity.ok("음식점 데이터 저장 완료!"); + } + + //음식점 목록 제공 + @GetMapping("/restaurant/list") + public ResponseEntity> getRestaurantList() { + List restaurantList = restaurantListService.getRestaurantList(); + return ResponseEntity.ok(restaurantList); + } + + //음식점 상세 정보 제공 + @GetMapping("/info/restaurant/{restaurantId}") + public ResponseEntity getRestaurantDetail(@PathVariable Long restaurantId) { + RestaurantDetailResponse restaurantDetail = restaurantListService.getRestaurantDetail(restaurantId); + return ResponseEntity.ok(restaurantDetail); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java b/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java new file mode 100644 index 0000000..e9ce8d5 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java @@ -0,0 +1,45 @@ +//추가 +package com.example.placeservice.dto; + +import com.example.placeservice.entity.Restaurant; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class RestaurantDetailResponse { + + private String address_name; + private String category_group_code; + private String category_group_name; // 고정 X, DB 값 + private String category_name; // 고정 X, DB 값 + private String distance; // 빈 값 유지 + private String id; + private String phone; + private String place_name; + private String place_url; + private String road_address_name; + private String lat; + private String lon; + private String categoryGroupName; // 추가 + private String categoryName; // 추가 + + public static RestaurantDetailResponse fromEntity(Restaurant restaurant) { + return RestaurantDetailResponse.builder() + .address_name(restaurant.getAddress()) + .category_group_code(restaurant.getCategory_code()) + .category_group_name(restaurant.getCategoryGroupName()) // 고정X (DB) + .category_name(restaurant.getCategoryName()) // 고정X (DB) + .distance("") // 그대로 비워두기 + .id(String.valueOf(restaurant.getRestaurantId())) + .phone(restaurant.getPhone()) + .place_name(restaurant.getName()) + .place_url(restaurant.getKakaomap_url()) + .categoryGroupName(restaurant.getCategoryGroupName()) // 추가 + .categoryName(restaurant.getCategoryName()) // 추가 + .road_address_name(restaurant.getAddress()) // 현재 Address랑 Road Address가 같게 저장된 경우 + .lat(restaurant.getLat().toString()) + .lon(restaurant.getLon().toString()) + .build(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/RestaurantListResponse.java b/place-service/src/main/java/com/example/placeservice/dto/RestaurantListResponse.java new file mode 100644 index 0000000..2a1f5c5 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/RestaurantListResponse.java @@ -0,0 +1,23 @@ +package com.example.placeservice.dto; +//추가 +import com.example.placeservice.entity.Restaurant; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class RestaurantListResponse { + private Long id; + private String name; + private String lat; + private String lon; + + public static RestaurantListResponse fromEntity(Restaurant restaurant) { + return RestaurantListResponse.builder() + .id(restaurant.getRestaurantId()) + .name(restaurant.getName()) + .lat(restaurant.getLat().toString()) + .lon(restaurant.getLon().toString()) + .build(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index faa2087..edac94f 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -1,27 +1,52 @@ package com.example.placeservice.entity; import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; import java.math.BigDecimal; @Entity +@Getter +@Setter public class Area { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "area_id") private Long area_id; + @Column(name = "seoul_id", length = 10, nullable = false) + private String seoul_id; + @Column(name = "name", length = 50, nullable = false) private String name; // 지역명 + @Column(name = "name_eng", length = 100, nullable = false) + private String name_eng; + @Column(name = "category", length = 20, nullable = false) private String category; // 구분 - @Column(name = "x", precision = 10, scale = 6, nullable = false) - private BigDecimal x; // 위도 + @Column(name = "lat", precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 + + @Column(name = "lon", precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 + + + public Object getName() { + return this.name; + } - @Column(name = "y", precision = 10, scale = 6, nullable = false) - private BigDecimal y; // 경도 + public Long getAreaId() { + return this.area_id; + } + public Number getY() { + return this.lon; + } -} + public Number getX() { + return this.lat; + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java b/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java new file mode 100644 index 0000000..36ae9a3 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java @@ -0,0 +1,53 @@ +package com.example.placeservice.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; + +@Entity +@Getter +@Setter +@Table(name = "restaurant") // 테이블명 명시 +public class Restaurant { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "restaurant_id") + private Long restaurantId; // PK + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "area_id", nullable = false) + private Area area; // Area 엔티티와 다대일 관계 + + @Column(length = 50, nullable = false) + private String name; // 음식점 이름 + + @Column(length = 200) + private String address; // 음식점 주소 + + @Column(precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 + + @Column(precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 + + @Column(length = 20) + private String phone; // 전화번호 + + @Column(length = 200) + private String kakaomap_url; // 카카오맵 URL + + @Column(length = 20) + private String category_code; // 카테고리 코드 (FD6: 음식점) + + + //추가 + @Column(length = 100) + private String categoryGroupName; // (예시) 음식점 + + @Column(length = 200) + private String categoryName; // (예시) 음식점 > 한식 > 육류,고기 + +} diff --git a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java new file mode 100644 index 0000000..ba08e9f --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java @@ -0,0 +1,8 @@ +package com.example.placeservice.repository; + +import com.example.placeservice.entity.Restaurant; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RestaurantRepository extends JpaRepository { + // 기본 CRUD 사용 (findAll, findById, save 등) +} diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java new file mode 100644 index 0000000..0df3c35 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java @@ -0,0 +1,48 @@ +package com.example.placeservice.service; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.math.BigDecimal; + +@Service +@RequiredArgsConstructor +public class KakaoMapClient { + + @Value("${kakao.api.key}") + private String kakaoApiKey; + + public JsonNode searchRestaurantsByCategory(BigDecimal lon, BigDecimal lat, int page) { + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "KakaoAK " + kakaoApiKey); + + // Kakao API 호출 URL 세팅 + UriComponentsBuilder builder = UriComponentsBuilder + .fromHttpUrl("https://dapi.kakao.com/v2/local/search/category.json") + .queryParam("category_group_code", "FD6") // FD6: 음식점 카테고리 + .queryParam("x", lon) // x: 경도(lon) + .queryParam("y", lat) // y: 위도(lat) + .queryParam("sort", "accuracy") // 정확도순 정렬 + .queryParam("page", page) // 요청 페이지 + .queryParam("size", 15); // 한 페이지에 최대 15개 결과 + + HttpEntity entity = new HttpEntity<>(headers); + + // REST API 호출 + ResponseEntity response = restTemplate.exchange( + builder.toUriString(), + HttpMethod.GET, + entity, + JsonNode.class + ); + + return response.getBody(); // JSON 응답 반환 + } +} diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java new file mode 100644 index 0000000..d092009 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java @@ -0,0 +1,34 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.RestaurantDetailResponse; +import com.example.placeservice.dto.RestaurantListResponse; +import com.example.placeservice.entity.Restaurant; +import com.example.placeservice.repository.RestaurantRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class RestaurantListService { + + private final RestaurantRepository restaurantRepository; + + @Transactional(readOnly = true) + public List getRestaurantList() { + return restaurantRepository.findAll() + .stream() + .map(RestaurantListResponse::fromEntity) + .collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public RestaurantDetailResponse getRestaurantDetail(Long id) { + Restaurant restaurant = restaurantRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("해당 음식점이 존재하지 않습니다. id=" + id)); + return RestaurantDetailResponse.fromEntity(restaurant); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java new file mode 100644 index 0000000..7195059 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java @@ -0,0 +1,95 @@ +package com.example.placeservice.service; + +import com.example.placeservice.entity.Area; +import com.example.placeservice.entity.Restaurant; +import com.example.placeservice.repository.AreaRepository; +import com.example.placeservice.repository.RestaurantRepository; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class RestaurantService { + + private final AreaRepository areaRepository; + private final RestaurantRepository restaurantRepository; + private final KakaoMapClient kakaoMapClient; + + // 특정 Area 기준으로 주변 음식점 저장 + public List saveRestaurantsNearbyArea(Area area) { + List savedRestaurants = new ArrayList<>(); + int page = 1; + boolean isEnd = false; + double searchRadius = 1000.0; // 반경 1km + + while (!isEnd) { + // Area의 lat, lon 순서를 반대로 호출해야 함 (DB상에 값이 반대임) + JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLat(), area.getLon(), page); + + if (response.has("documents")) { + for (JsonNode doc : response.get("documents")) { + double restaurantLon = Double.parseDouble(doc.get("x").asText()); + double restaurantLat = Double.parseDouble(doc.get("y").asText()); + + // 거리 계산할 때도 lat, lon 순서 조심 (Area에서 꺼낼 때) + double distance = calculateDistance( + area.getLon().doubleValue(), area.getLat().doubleValue(), + restaurantLat, restaurantLon + ); + + // 1km 이내 음식점만 저장 + if (distance <= searchRadius) { + Restaurant restaurant = new Restaurant(); + restaurant.setArea(area); + restaurant.setName(doc.get("place_name").asText()); + restaurant.setAddress(doc.get("road_address_name").asText()); + restaurant.setLat(new BigDecimal(doc.get("y").asText())); // Kakao API에서 y: lat + restaurant.setLon(new BigDecimal(doc.get("x").asText())); // Kakao API에서 x: lon + restaurant.setPhone(doc.has("phone") ? doc.get("phone").asText() : null); + restaurant.setKakaomap_url(doc.get("place_url").asText()); + restaurant.setCategory_code(doc.has("category_group_code") ? doc.get("category_group_code").asText() : null); + + savedRestaurants.add(restaurant); + restaurantRepository.save(restaurant); + } + } + + isEnd = response.get("meta").get("is_end").asBoolean(); + page++; + } else { + isEnd = true; + } + } + return savedRestaurants; + } + + // 전체 Area 대상 음식점 저장 (fetch & save) + @Transactional + public void fetchAndSaveRestaurants() { + List areas = areaRepository.findAll(); // 모든 지역 조회 + for (Area area : areas) { + saveRestaurantsNearbyArea(area); // 각 지역별 저장 + } + } + + // Haversine 거리 계산 공식 + private double calculateDistance(double lat1, double lon1, double lat2, double lon2) { + final int EARTH_RADIUS = 6371; // 지구 반지름 (km) + + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + + double a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) + * Math.sin(dLon/2) * Math.sin(dLon/2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + + return EARTH_RADIUS * c * 1000; // meter 단위로 반환 + } +} diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index d5cbebf..439bd92 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -2,10 +2,19 @@ spring.application.name=place-service # postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/star -spring.datasource.username=admin +spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db +spring.datasource.username=root spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update +spring.jpa.defer-datasource-initialization=true +spring.jpa.hibernate.ddl-auto=create +spring.jpa.show-sql=true +spring.sql.init.mode=always + +# ??? API ? ?? +kakao.api.key=585808366c299a6686e7104a193017bb + +# ?? ?? +logging.level.com.example.placeservice=INFO \ No newline at end of file diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql new file mode 100644 index 0000000..6ecb39a --- /dev/null +++ b/place-service/src/main/resources/data.sql @@ -0,0 +1,117 @@ +INSERT INTO area (seoul_id, name, name_eng, category, lon, lat) VALUES + ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), + ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), + ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), + ('POI004', '이태원 관광특구', 'Itaewon Special Tourist Zone', '관광특구', 37.53475718815058, 126.99591956660342), + ('POI005', '잠실 관광특구', 'Jamsil Special Tourist Zone', '관광특구', 37.51868923685722, 127.11694379878114), + ('POI006', '종로·청계 관광특구', 'Jongno Cheonggye Special Toruist Zone', '관광특구', 37.56984229024498, 126.99866678943675), + ('POI007', '홍대 관광특구', 'HongDae Culture & Arts Special Tourist Zone', '관광특구', 37.55506170187393, 126.92159115672915), + ('POI008', '경복궁', 'Gyeongbokgung Palace', '고궁·문화유산', 37.5809150911806, 126.97764726625209), + ('POI009', '광화문·덕수궁', 'Gwanghwamun & Deoksugung Palace', '고궁·문화유산', 37.56847081447724, 126.97620594802834), + ('POI010', '보신각', 'Bosingak', '고궁·문화유산', 37.57089797616037, 126.98337607511172), + ('POI011', '서울 암사동 유적', 'Amsa Prehistoric Settlement Site', '고궁·문화유산', 37.56102809827398, 127.13078491934499), + ('POI012', '창덕궁·종묘', 'Changdeokgung Palace & Jongmyo Shrine', '고궁·문화유산', 37.5795304952381, 126.99416802285715), + ('POI013', '가산디지털단지역', 'Gasan Digital Complex station', '인구밀집지역', 37.48267268376068, 126.88014526210826), + ('POI014', '강남역', 'Gangnam station', '인구밀집지역', 37.50019794543511, 127.02822145618981), + ('POI015', '건대입구역', 'Konkuk University station', '인구밀집지역', 37.540005312302846, 127.06718185804415), + ('POI016', '고덕역', 'Godeok station', '인구밀집지역', 37.553377141111774, 127.15503341723486), + ('POI017', '고속터미널역', 'Express Bus Terminal station', '인구밀집지역', 37.50472316731823, 127.00542276570697), + ('POI018', '교대역', 'Seoul National University of Education station', '인구밀집지역', 37.49185444557083, 127.01456630812736), + ('POI019', '구로디지털단지역', 'Guro Digital Complex station', '인구밀집지역', 37.48349491938897, 126.89557915984915), + ('POI020', '구로역', 'Guro station', '인구밀집지역', 37.501517014484925, 126.88285531403136), + ('POI021', '군자역', 'Gunja station', '인구밀집지역', 37.555916439038526, 127.08008933356267), + ('POI022', '남구로역', 'Namguro station', '인구밀집지역', 37.484184300878496, 126.88539683418863), + ('POI023', '대림역', 'Daerim station', '인구밀집지역', 37.492818237753816, 126.89567307890546), + ('POI024', '동대문역', 'Dongdaemun station', '인구밀집지역', 37.57127393968462, 127.0096854515538), + ('POI025', '뚝섬역', 'Ttukseom station', '인구밀집지역', 37.548249670448335, 127.04603079475764), + ('POI026', '미아사거리역', 'Miasageori station', '인구밀집지역', 37.61244898963818, 127.0305835226811), + ('POI027', '발산역', 'Balsan station', '인구밀집지역', 37.55796811527854, 126.84179723439551), + ('POI028', '북한산우이역', 'Bukhansan Ui station', '인구밀집지역', 37.66281154490492, 127.01070580250341), + ('POI029', '사당역', 'Sadang station', '인구밀집지역', 37.47635049189678, 126.98129519152415), + ('POI030', '삼각지역', 'Samgakji station', '인구밀집지역', 37.535323919468816, 126.97392611189508), + ('POI031', '서울대입구역', 'Seoul National University station', '인구밀집지역', 37.48060991955129, 126.95360407634334), + ('POI032', '서울식물원·마곡나루역', 'Seoul Botanic Park·Magongnaru station', '인구밀집지역', 37.56745047572558, 126.82986126533689), + ('POI033', '서울역', 'Seoul station', '인구밀집지역', 37.55662336923077, 126.97216878769233), + ('POI034', '선릉역', 'Seolleung station', '인구밀집지역', 37.50584166753116, 127.0489100692203), + ('POI035', '성신여대입구역', 'Sungshin Women''s University station', '인구밀집지역', 37.59262801654558, 127.01658994286346), + ('POI036', '수유역', 'Suyu station', '인구밀집지역', 37.641215514705884, 127.02565393627451), + ('POI037', '신논현역·논현역', 'Sinnonhyeon·Nonhyeon station', '인구밀집지역', 37.50848054554439, 127.02291493850254), + ('POI038', '신도림역', 'Sindorim station', '인구밀집지역', 37.50941493666961, 126.89096014067083), + ('POI039', '신림역', 'Sillim station', '인구밀집지역', 37.48396968904173, 126.92987028995233), + ('POI040', '신촌·이대역', 'Sinchon·Ewha Womans University station', '인구밀집지역', 37.55564146783625, 126.93761215497075), + ('POI041', '양재역', 'Yangjae station', '인구밀집지역', 37.48451422353994, 127.03551115644439), + ('POI042', '역삼역', 'Yeoksam station', '인구밀집지역', 37.50011177778084, 127.03721339389585), + ('POI043', '연신내역', 'Yeonsinnae station', '인구밀집지역', 37.61903547457627, 126.92180184322034), + ('POI044', '오목교역·목동운동장', 'Omokgyo·Mok-dong Stadium station', '인구밀집지역', 37.53174788866626, 126.87924768246015), + ('POI045', '왕십리역', 'Wangsimni station', '인구밀집지역', 37.561636745463275, 127.03857508984515), + ('POI046', '용산역', 'Yongsan station', '인구밀집지역', 37.53059674809017, 126.95953798376128), + ('POI047', '이태원역', 'Itaewon station', '인구밀집지역', 37.534847968613946, 126.99261390234051), + ('POI048', '장지역', 'Jangji station', '인구밀집지역', 37.47861018962738, 127.12424263037641), + ('POI049', '장한평역', 'Janghanpyeong station', '인구밀집지역', 37.562849061361526, 127.06235115915185), + ('POI050', '천호역', 'Cheonho station', '인구밀집지역', 37.53921065146112, 127.12470374198884), + ('POI051', '총신대입구(이수)역', 'Chongshin University(Isu) station', '인구밀집지역', 37.48677821828824, 126.9807539648491), + ('POI052', '충정로역', 'Chungjeongno station', '인구밀집지역', 37.559524839900476, 126.9642059673645), + ('POI053', '합정역', 'Hapjeong station', '인구밀집지역', 37.54857307399022, 126.91084543497917), + ('POI054', '혜화역', 'Hyehwa station', '인구밀집지역', 37.58237426286851, 127.0014404389915), + ('POI055', '홍대입구역(2호선)', 'Hongik University station(Line 2)', '인구밀집지역', 37.5560688250626, 126.92329766229786), + ('POI056', '회기역', 'Hoegi station', '인구밀집지역', 37.590538258503685, 127.05700039395965), + ('POI057', '4·19 카페거리', '4·19 Cafe street', '발달상권', 37.64481185893745, 127.00739701908337), + ('POI058', '가락시장', 'Garak Market', '발달상권', 37.49445476721721, 127.11054976163454), + ('POI059', '가로수길', 'Garosu-gil', '발달상권', 37.522662622516556, 127.02543305960263), + ('POI060', '광장(전통)시장', 'Gwangjang(Traditional) Market', '발달상권', 37.569817036518465, 127.00014893169192), + ('POI061', '김포공항', 'Gimpo Airport', '발달상권', 37.56228914604604, 126.80289965159135), + ('POI062', '낙산공원·이화마을', 'Naksan Park·Ihwa Village', '발달상권', 37.580295002146926, 127.00692469978675), + ('POI063', '노량진', 'Noryangjin', '발달상권', 37.513690699957316, 126.94907070192423), + ('POI064', '덕수궁길·정동길', 'Deoksugung-gil·Jeongdong-gil', '발달상권', 37.5659744042236, 126.97244482634493), + ('POI065', '방배역 먹자골목', 'Bangbae food alley', '발달상권', 37.48234102283986, 126.99597969571558), + ('POI066', '북촌한옥마을', 'Bukchon Hanok Village', '발달상권', 37.58425649416171, 126.98386049085175), + ('POI067', '서촌', 'Seochon', '발달상권', 37.58018833392947, 126.9666234325548), + ('POI068', '성수카페거리', 'Seongsu Cafe Street', '발달상권', 37.54227657813654, 127.05560237183015), + ('POI069', '수유리 먹자골목', 'Suyuri food alley', '발달상권', 37.64270823057644, 127.02733855833522), + ('POI070', '쌍문동 맛집거리', 'Ssangmun-dong restaurant street', '발달상권', 37.6477285443038, 127.03242528481013), + ('POI071', '압구정로데오거리', 'Apgujeong Rodeo Street', '발달상권', 37.52560441860466, 127.04040171511626), + ('POI072', '여의도', 'Yeouido', '발달상권', 37.52549260943663, 126.92338937129487), + ('POI073', '연남동', 'Yeonnam-dong', '발달상권', 37.561426426204264, 126.92406273242905), + ('POI074', '영등포 타임스퀘어', 'Yeongdeungpo Time square', '발달상권', 37.51653179760669, 126.90523931614634), + ('POI075', '외대앞', 'Hankuk University of Foreign Studies', '발달상권', 37.59442412843283, 127.06153833102397), + ('POI076', '용리단길', 'Yongnidan-gil', '발달상권', 37.5311056439386, 126.97147359358237), + ('POI077', '이태원 앤틱가구거리', 'Itaewon Antiques street', '발달상권', 37.53187270743738, 126.99343376513602), + ('POI078', '인사동·익선동', 'Insa-dong·Ikseon-dong', '발달상권', 37.5741577489609, 126.98736374162822), + ('POI079', '창동 신경제 중심지', 'Changdong New Economic Center', '발달상권', 37.656167961335676, 127.05328808787348), + ('POI080', '청담동 명품거리', 'Cheongdam-dong Luxury Fashion street', '발달상권', 37.52645537152884, 127.04354058968364), + ('POI081', '청량리 제기동 일대 전통시장', 'Traditional market in Cheongnyangni Jegi-dong', '발달상권', 37.579977738703214, 127.0382790126348), + ('POI082', '해방촌·경리단길', 'Haebangchon·Gyeongnidan-gil', '발달상권', 37.54143829714048, 126.98836800969178), + ('POI083', 'DDP(동대문디자인플라자)', 'DDP(Dongdaemun Design Plaza)', '발달상권', 37.56654201564661, 127.00993045294622), + ('POI084', 'DMC(디지털미디어시티)', 'DMC(Digital Media City)', '발달상권', 37.577407110655734, 126.8936691147541), + ('POI085', '강서한강공원', 'Gangseo Hangang Park', '공원', 37.5858708295553, 126.82002478578644), + ('POI086', '고척돔', 'Gocheok Dome', '공원', 37.49776790791694, 126.86713336195083), + ('POI087', '광나루한강공원', 'Gwangnaru Hangang Park', '공원', 37.550866089267345, 127.12370647922764), + ('POI088', '광화문광장', 'Gwanghwamun Square', '공원', 37.57505062349029, 126.97680679071502), + ('POI089', '국립중앙박물관·용산가족공원', 'The National Museum of Korea·Yongsan Family Park', '공원', 37.52202119313341, 126.9824679850178), + ('POI090', '난지한강공원', 'Nanji Hangang Park', '공원', 37.566316740172454, 126.87770001597329), + ('POI091', '남산공원', 'Namsan Park', '공원', 37.555070650369004, 126.99365012730628), + ('POI092', '노들섬', 'Nodeul island', '공원', 37.517556872131706, 126.95871397855296), + ('POI093', '뚝섬한강공원', 'Ttukseom Hangang Park', '공원', 37.52882503002481, 127.07460742502332), + ('POI094', '망원한강공원', 'Mangwon Hangang Park', '공원', 37.55480087656761, 126.89823749216892), + ('POI095', '반포한강공원', 'Banpo Hangang Park', '공원', 37.51065462735107, 126.99457550649858), + ('POI096', '북서울꿈의숲', 'Dream Forest', '공원', 37.62244979881656, 127.04069568441815), + ('POI097', '불광천', 'Bulgwangcheon River', '공원', 37.58940646530798, 126.91194122899687), + ('POI098', '서리풀공원·몽마르뜨공원', 'Seoripul Park·Montmartre Park', '공원', 37.49022054658995, 127.00173655775917), + ('POI100', '서울대공원', 'Seoul Grand Park', '공원', 37.42839607329216, 127.01863790236284), + ('POI101', '서울숲공원', 'Seoul Forest', '공원', 37.54402680167043, 127.03673083133076), + ('POI099', '서울광장', 'Seoul Plaza', '공원', 37.5655010296151, 126.97798702124584), + ('POI102', '아차산', 'Achasan', '공원', 37.56307021845223, 127.10047918048643), + ('POI103', '양화한강공원', 'Yanghwa Hangang Park', '공원', 37.54029653900909, 126.89963558394624), + ('POI104', '어린이대공원', 'Children''s Grand Park', '공원', 37.54957100980681, 127.0824313180458), + ('POI105', '여의도한강공원', 'Yeouido Hangang Park', '공원', 37.52868909088513, 126.92627587984478), + ('POI106', '월드컵공원', 'World Cup Park', '공원', 37.5660391150387, 126.88740201899091), + ('POI107', '응봉산', 'Eungbongsan', '공원', 37.54797346368485, 127.02904643771303), + ('POI108', '이촌한강공원', 'Ichon Hangang Park', '공원', 37.52139035897837, 126.96356631928725), + ('POI109', '잠실종합운동장', 'Jamsil (Seoul) Sports Complex', '공원', 37.51610665876664, 127.07230325047283), + ('POI110', '잠실한강공원', 'Jamsil Hangang Park', '공원', 37.51981181577704, 127.08507449607517), + ('POI111', '잠원한강공원', 'Jamwon Hangang Park', '공원', 37.52336245003122, 127.01397131593632), + ('POI112', '청계산', 'Cheonggyesan', '공원', 37.443365424959026, 127.0469697170313), + ('POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264), + ('POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616), + ('POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821), + ('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286); \ No newline at end of file From 0f2cbd6b44cf17ce19f67530eb3f251c3b134d9c Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Sun, 20 Apr 2025 23:32:12 +0900 Subject: [PATCH 061/361] =?UTF-8?q?=EC=9D=8C=EC=8B=9D=EC=A0=90=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=20=EC=A7=80=EC=97=AD=EB=8B=B9=2015?= =?UTF-8?q?=EA=B0=9C=EC=94=A9=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeservice/service/KakaoMapClient.java | 1 + .../placeservice/service/RestaurantService.java | 15 +++++++++++---- .../src/main/resources/application.properties | 16 +--------------- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java index 0df3c35..82be371 100644 --- a/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java @@ -45,4 +45,5 @@ public JsonNode searchRestaurantsByCategory(BigDecimal lon, BigDecimal lat, int return response.getBody(); // JSON 응답 반환 } + } diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java index 7195059..27d8741 100644 --- a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java +++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java @@ -27,17 +27,21 @@ public List saveRestaurantsNearbyArea(Area area) { int page = 1; boolean isEnd = false; double searchRadius = 1000.0; // 반경 1km + int savedCount = 0; // 저장한 음식점 수 while (!isEnd) { - // Area의 lat, lon 순서를 반대로 호출해야 함 (DB상에 값이 반대임) JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLat(), area.getLon(), page); if (response.has("documents")) { for (JsonNode doc : response.get("documents")) { + if (savedCount >= 15) { // 15개 저장했으면 바로 종료 + isEnd = true; + break; + } + double restaurantLon = Double.parseDouble(doc.get("x").asText()); double restaurantLat = Double.parseDouble(doc.get("y").asText()); - // 거리 계산할 때도 lat, lon 순서 조심 (Area에서 꺼낼 때) double distance = calculateDistance( area.getLon().doubleValue(), area.getLat().doubleValue(), restaurantLat, restaurantLon @@ -57,11 +61,14 @@ public List saveRestaurantsNearbyArea(Area area) { savedRestaurants.add(restaurant); restaurantRepository.save(restaurant); + savedCount++; // 저장한 개수 카운트 } } - isEnd = response.get("meta").get("is_end").asBoolean(); - page++; + if (!isEnd) { + isEnd = response.get("meta").get("is_end").asBoolean(); + page++; + } } else { isEnd = true; } diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index a9f5838..439bd92 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -2,19 +2,12 @@ spring.application.name=place-service # postgreSQL ?? -<<<<<<< HEAD spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db spring.datasource.username=root -======= - -spring.datasource.url=jdbc:postgresql://localhost:5432/star -spring.datasource.username=admin ->>>>>>> ebefadd531298c100e6deedf74e58ab745216674 spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -<<<<<<< HEAD spring.jpa.defer-datasource-initialization=true spring.jpa.hibernate.ddl-auto=create spring.jpa.show-sql=true @@ -24,11 +17,4 @@ spring.sql.init.mode=always kakao.api.key=585808366c299a6686e7104a193017bb # ?? ?? -logging.level.com.example.placeservice=INFO -======= -spring.jpa.hibernate.ddl-auto=update - - -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update ->>>>>>> ebefadd531298c100e6deedf74e58ab745216674 +logging.level.com.example.placeservice=INFO \ No newline at end of file From 5d5fb7c029917f5692958fb08b86857c3c0cac9c Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Sun, 20 Apr 2025 23:32:12 +0900 Subject: [PATCH 062/361] =?UTF-8?q?=EC=9D=8C=EC=8B=9D=EC=A0=90=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=20=EC=A7=80=EC=97=AD=EB=8B=B9=2015?= =?UTF-8?q?=EA=B0=9C=EC=94=A9=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeservice/service/KakaoMapClient.java | 1 + .../placeservice/service/RestaurantService.java | 15 +++++++++++---- .../src/main/resources/application.properties | 16 +--------------- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java index 0df3c35..82be371 100644 --- a/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java @@ -45,4 +45,5 @@ public JsonNode searchRestaurantsByCategory(BigDecimal lon, BigDecimal lat, int return response.getBody(); // JSON 응답 반환 } + } diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java index 7195059..27d8741 100644 --- a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java +++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java @@ -27,17 +27,21 @@ public List saveRestaurantsNearbyArea(Area area) { int page = 1; boolean isEnd = false; double searchRadius = 1000.0; // 반경 1km + int savedCount = 0; // 저장한 음식점 수 while (!isEnd) { - // Area의 lat, lon 순서를 반대로 호출해야 함 (DB상에 값이 반대임) JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLat(), area.getLon(), page); if (response.has("documents")) { for (JsonNode doc : response.get("documents")) { + if (savedCount >= 15) { // 15개 저장했으면 바로 종료 + isEnd = true; + break; + } + double restaurantLon = Double.parseDouble(doc.get("x").asText()); double restaurantLat = Double.parseDouble(doc.get("y").asText()); - // 거리 계산할 때도 lat, lon 순서 조심 (Area에서 꺼낼 때) double distance = calculateDistance( area.getLon().doubleValue(), area.getLat().doubleValue(), restaurantLat, restaurantLon @@ -57,11 +61,14 @@ public List saveRestaurantsNearbyArea(Area area) { savedRestaurants.add(restaurant); restaurantRepository.save(restaurant); + savedCount++; // 저장한 개수 카운트 } } - isEnd = response.get("meta").get("is_end").asBoolean(); - page++; + if (!isEnd) { + isEnd = response.get("meta").get("is_end").asBoolean(); + page++; + } } else { isEnd = true; } diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index a9f5838..439bd92 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -2,19 +2,12 @@ spring.application.name=place-service # postgreSQL ?? -<<<<<<< HEAD spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db spring.datasource.username=root -======= - -spring.datasource.url=jdbc:postgresql://localhost:5432/star -spring.datasource.username=admin ->>>>>>> ebefadd531298c100e6deedf74e58ab745216674 spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -<<<<<<< HEAD spring.jpa.defer-datasource-initialization=true spring.jpa.hibernate.ddl-auto=create spring.jpa.show-sql=true @@ -24,11 +17,4 @@ spring.sql.init.mode=always kakao.api.key=585808366c299a6686e7104a193017bb # ?? ?? -logging.level.com.example.placeservice=INFO -======= -spring.jpa.hibernate.ddl-auto=update - - -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update ->>>>>>> ebefadd531298c100e6deedf74e58ab745216674 +logging.level.com.example.placeservice=INFO \ No newline at end of file From bad142538864827231017f54fd83670910f2c221 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Sun, 20 Apr 2025 23:50:18 +0900 Subject: [PATCH 063/361] =?UTF-8?q?=EC=9D=8C=EC=8B=9D=EC=A0=90=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RestaurantService.java | 206 +++++++++--------- 1 file changed, 104 insertions(+), 102 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java index 27d8741..d40c820 100644 --- a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java +++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java @@ -1,102 +1,104 @@ -package com.example.placeservice.service; - -import com.example.placeservice.entity.Area; -import com.example.placeservice.entity.Restaurant; -import com.example.placeservice.repository.AreaRepository; -import com.example.placeservice.repository.RestaurantRepository; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; - -@Service -@RequiredArgsConstructor -public class RestaurantService { - - private final AreaRepository areaRepository; - private final RestaurantRepository restaurantRepository; - private final KakaoMapClient kakaoMapClient; - - // 특정 Area 기준으로 주변 음식점 저장 - public List saveRestaurantsNearbyArea(Area area) { - List savedRestaurants = new ArrayList<>(); - int page = 1; - boolean isEnd = false; - double searchRadius = 1000.0; // 반경 1km - int savedCount = 0; // 저장한 음식점 수 - - while (!isEnd) { - JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLat(), area.getLon(), page); - - if (response.has("documents")) { - for (JsonNode doc : response.get("documents")) { - if (savedCount >= 15) { // 15개 저장했으면 바로 종료 - isEnd = true; - break; - } - - double restaurantLon = Double.parseDouble(doc.get("x").asText()); - double restaurantLat = Double.parseDouble(doc.get("y").asText()); - - double distance = calculateDistance( - area.getLon().doubleValue(), area.getLat().doubleValue(), - restaurantLat, restaurantLon - ); - - // 1km 이내 음식점만 저장 - if (distance <= searchRadius) { - Restaurant restaurant = new Restaurant(); - restaurant.setArea(area); - restaurant.setName(doc.get("place_name").asText()); - restaurant.setAddress(doc.get("road_address_name").asText()); - restaurant.setLat(new BigDecimal(doc.get("y").asText())); // Kakao API에서 y: lat - restaurant.setLon(new BigDecimal(doc.get("x").asText())); // Kakao API에서 x: lon - restaurant.setPhone(doc.has("phone") ? doc.get("phone").asText() : null); - restaurant.setKakaomap_url(doc.get("place_url").asText()); - restaurant.setCategory_code(doc.has("category_group_code") ? doc.get("category_group_code").asText() : null); - - savedRestaurants.add(restaurant); - restaurantRepository.save(restaurant); - savedCount++; // 저장한 개수 카운트 - } - } - - if (!isEnd) { - isEnd = response.get("meta").get("is_end").asBoolean(); - page++; - } - } else { - isEnd = true; - } - } - return savedRestaurants; - } - - // 전체 Area 대상 음식점 저장 (fetch & save) - @Transactional - public void fetchAndSaveRestaurants() { - List areas = areaRepository.findAll(); // 모든 지역 조회 - for (Area area : areas) { - saveRestaurantsNearbyArea(area); // 각 지역별 저장 - } - } - - // Haversine 거리 계산 공식 - private double calculateDistance(double lat1, double lon1, double lat2, double lon2) { - final int EARTH_RADIUS = 6371; // 지구 반지름 (km) - - double dLat = Math.toRadians(lat2 - lat1); - double dLon = Math.toRadians(lon2 - lon1); - - double a = Math.sin(dLat/2) * Math.sin(dLat/2) - + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) - * Math.sin(dLon/2) * Math.sin(dLon/2); - double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); - - return EARTH_RADIUS * c * 1000; // meter 단위로 반환 - } -} +package com.example.placeservice.service; + +import com.example.placeservice.entity.Area; +import com.example.placeservice.entity.Restaurant; +import com.example.placeservice.repository.AreaRepository; +import com.example.placeservice.repository.RestaurantRepository; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class RestaurantService { + + private final AreaRepository areaRepository; + private final RestaurantRepository restaurantRepository; + private final KakaoMapClient kakaoMapClient; + + // 특정 Area 기준으로 주변 음식점 저장 + public List saveRestaurantsNearbyArea(Area area) { + List savedRestaurants = new ArrayList<>(); + int page = 1; + boolean isEnd = false; + double searchRadius = 1000.0; // 반경 1km + int savedCount = 0; // 저장한 음식점 수 + + while (!isEnd) { + JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLat(), area.getLon(), page); + + if (response.has("documents")) { + for (JsonNode doc : response.get("documents")) { + if (savedCount >= 15) { // 15개 저장했으면 바로 종료 + isEnd = true; + break; + } + + double restaurantLon = Double.parseDouble(doc.get("x").asText()); + double restaurantLat = Double.parseDouble(doc.get("y").asText()); + + double distance = calculateDistance( + area.getLon().doubleValue(), area.getLat().doubleValue(), + restaurantLat, restaurantLon + ); + + // 1km 이내 음식점만 저장 + if (distance <= searchRadius) { + Restaurant restaurant = new Restaurant(); + restaurant.setArea(area); + restaurant.setName(doc.get("place_name").asText()); + restaurant.setAddress(doc.get("road_address_name").asText()); + restaurant.setLat(new BigDecimal(doc.get("y").asText())); // Kakao API에서 y: lat + restaurant.setLon(new BigDecimal(doc.get("x").asText())); // Kakao API에서 x: lon + restaurant.setPhone(doc.has("phone") ? doc.get("phone").asText() : null); + restaurant.setKakaomap_url(doc.get("place_url").asText()); + restaurant.setCategory_code(doc.has("category_group_code") ? doc.get("category_group_code").asText() : null); + restaurant.setCategoryGroupName(doc.has("category_group_name") ? doc.get("category_group_name").asText() : null); + restaurant.setCategoryName(doc.has("category_name") ? doc.get("category_name").asText() : null); + + savedRestaurants.add(restaurant); + restaurantRepository.save(restaurant); + savedCount++; // 저장한 개수 카운트 + } + } + + if (!isEnd) { + isEnd = response.get("meta").get("is_end").asBoolean(); + page++; + } + } else { + isEnd = true; + } + } + return savedRestaurants; + } + + // 전체 Area 대상 음식점 저장 (fetch & save) + @Transactional + public void fetchAndSaveRestaurants() { + List areas = areaRepository.findAll(); // 모든 지역 조회 + for (Area area : areas) { + saveRestaurantsNearbyArea(area); // 각 지역별 저장 + } + } + + // Haversine 거리 계산 공식 + private double calculateDistance(double lat1, double lon1, double lat2, double lon2) { + final int EARTH_RADIUS = 6371; // 지구 반지름 (km) + + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + + double a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) + * Math.sin(dLon/2) * Math.sin(dLon/2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + + return EARTH_RADIUS * c * 1000; // meter 단위로 반환 + } +} From 94b8dbff7ea879a07135af11c83e5dfe07f37a15 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Sun, 20 Apr 2025 23:50:18 +0900 Subject: [PATCH 064/361] =?UTF-8?q?=EC=9D=8C=EC=8B=9D=EC=A0=90=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RestaurantService.java | 206 +++++++++--------- 1 file changed, 104 insertions(+), 102 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java index 27d8741..d40c820 100644 --- a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java +++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java @@ -1,102 +1,104 @@ -package com.example.placeservice.service; - -import com.example.placeservice.entity.Area; -import com.example.placeservice.entity.Restaurant; -import com.example.placeservice.repository.AreaRepository; -import com.example.placeservice.repository.RestaurantRepository; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; - -@Service -@RequiredArgsConstructor -public class RestaurantService { - - private final AreaRepository areaRepository; - private final RestaurantRepository restaurantRepository; - private final KakaoMapClient kakaoMapClient; - - // 특정 Area 기준으로 주변 음식점 저장 - public List saveRestaurantsNearbyArea(Area area) { - List savedRestaurants = new ArrayList<>(); - int page = 1; - boolean isEnd = false; - double searchRadius = 1000.0; // 반경 1km - int savedCount = 0; // 저장한 음식점 수 - - while (!isEnd) { - JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLat(), area.getLon(), page); - - if (response.has("documents")) { - for (JsonNode doc : response.get("documents")) { - if (savedCount >= 15) { // 15개 저장했으면 바로 종료 - isEnd = true; - break; - } - - double restaurantLon = Double.parseDouble(doc.get("x").asText()); - double restaurantLat = Double.parseDouble(doc.get("y").asText()); - - double distance = calculateDistance( - area.getLon().doubleValue(), area.getLat().doubleValue(), - restaurantLat, restaurantLon - ); - - // 1km 이내 음식점만 저장 - if (distance <= searchRadius) { - Restaurant restaurant = new Restaurant(); - restaurant.setArea(area); - restaurant.setName(doc.get("place_name").asText()); - restaurant.setAddress(doc.get("road_address_name").asText()); - restaurant.setLat(new BigDecimal(doc.get("y").asText())); // Kakao API에서 y: lat - restaurant.setLon(new BigDecimal(doc.get("x").asText())); // Kakao API에서 x: lon - restaurant.setPhone(doc.has("phone") ? doc.get("phone").asText() : null); - restaurant.setKakaomap_url(doc.get("place_url").asText()); - restaurant.setCategory_code(doc.has("category_group_code") ? doc.get("category_group_code").asText() : null); - - savedRestaurants.add(restaurant); - restaurantRepository.save(restaurant); - savedCount++; // 저장한 개수 카운트 - } - } - - if (!isEnd) { - isEnd = response.get("meta").get("is_end").asBoolean(); - page++; - } - } else { - isEnd = true; - } - } - return savedRestaurants; - } - - // 전체 Area 대상 음식점 저장 (fetch & save) - @Transactional - public void fetchAndSaveRestaurants() { - List areas = areaRepository.findAll(); // 모든 지역 조회 - for (Area area : areas) { - saveRestaurantsNearbyArea(area); // 각 지역별 저장 - } - } - - // Haversine 거리 계산 공식 - private double calculateDistance(double lat1, double lon1, double lat2, double lon2) { - final int EARTH_RADIUS = 6371; // 지구 반지름 (km) - - double dLat = Math.toRadians(lat2 - lat1); - double dLon = Math.toRadians(lon2 - lon1); - - double a = Math.sin(dLat/2) * Math.sin(dLat/2) - + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) - * Math.sin(dLon/2) * Math.sin(dLon/2); - double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); - - return EARTH_RADIUS * c * 1000; // meter 단위로 반환 - } -} +package com.example.placeservice.service; + +import com.example.placeservice.entity.Area; +import com.example.placeservice.entity.Restaurant; +import com.example.placeservice.repository.AreaRepository; +import com.example.placeservice.repository.RestaurantRepository; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class RestaurantService { + + private final AreaRepository areaRepository; + private final RestaurantRepository restaurantRepository; + private final KakaoMapClient kakaoMapClient; + + // 특정 Area 기준으로 주변 음식점 저장 + public List saveRestaurantsNearbyArea(Area area) { + List savedRestaurants = new ArrayList<>(); + int page = 1; + boolean isEnd = false; + double searchRadius = 1000.0; // 반경 1km + int savedCount = 0; // 저장한 음식점 수 + + while (!isEnd) { + JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLat(), area.getLon(), page); + + if (response.has("documents")) { + for (JsonNode doc : response.get("documents")) { + if (savedCount >= 15) { // 15개 저장했으면 바로 종료 + isEnd = true; + break; + } + + double restaurantLon = Double.parseDouble(doc.get("x").asText()); + double restaurantLat = Double.parseDouble(doc.get("y").asText()); + + double distance = calculateDistance( + area.getLon().doubleValue(), area.getLat().doubleValue(), + restaurantLat, restaurantLon + ); + + // 1km 이내 음식점만 저장 + if (distance <= searchRadius) { + Restaurant restaurant = new Restaurant(); + restaurant.setArea(area); + restaurant.setName(doc.get("place_name").asText()); + restaurant.setAddress(doc.get("road_address_name").asText()); + restaurant.setLat(new BigDecimal(doc.get("y").asText())); // Kakao API에서 y: lat + restaurant.setLon(new BigDecimal(doc.get("x").asText())); // Kakao API에서 x: lon + restaurant.setPhone(doc.has("phone") ? doc.get("phone").asText() : null); + restaurant.setKakaomap_url(doc.get("place_url").asText()); + restaurant.setCategory_code(doc.has("category_group_code") ? doc.get("category_group_code").asText() : null); + restaurant.setCategoryGroupName(doc.has("category_group_name") ? doc.get("category_group_name").asText() : null); + restaurant.setCategoryName(doc.has("category_name") ? doc.get("category_name").asText() : null); + + savedRestaurants.add(restaurant); + restaurantRepository.save(restaurant); + savedCount++; // 저장한 개수 카운트 + } + } + + if (!isEnd) { + isEnd = response.get("meta").get("is_end").asBoolean(); + page++; + } + } else { + isEnd = true; + } + } + return savedRestaurants; + } + + // 전체 Area 대상 음식점 저장 (fetch & save) + @Transactional + public void fetchAndSaveRestaurants() { + List areas = areaRepository.findAll(); // 모든 지역 조회 + for (Area area : areas) { + saveRestaurantsNearbyArea(area); // 각 지역별 저장 + } + } + + // Haversine 거리 계산 공식 + private double calculateDistance(double lat1, double lon1, double lat2, double lon2) { + final int EARTH_RADIUS = 6371; // 지구 반지름 (km) + + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + + double a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) + * Math.sin(dLon/2) * Math.sin(dLon/2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + + return EARTH_RADIUS * c * 1000; // meter 단위로 반환 + } +} From bfbe3543de869a6182c10d8ce358da39582ae09c Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 23 Apr 2025 10:39:49 +0900 Subject: [PATCH 065/361] =?UTF-8?q?feat:=20Attraction=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=EC=97=90=20seoulAttractionId=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20API=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- congestion-service/build.gradle | 3 + .../service/RoadService.java | 1 - .../controller/AttractionController.java | 4 +- .../controller/CongestionController.java | 21 --- .../dto/attraction/AttractionInfoDto.java | 4 +- .../dto/attraction/AttractionListDto.java | 4 +- .../dto/congestion/CongestionDto.java | 146 ------------------ .../placeservice/entity/Attraction.java | 13 +- .../repository/AreaRepository.java | 5 + .../repository/AttractionRepository.java | 5 +- .../service/AttractionService.java | 28 ++-- .../service/CongestionService.java | 53 ------- 13 files changed, 42 insertions(+), 249 deletions(-) delete mode 100644 place-service/src/main/java/com/example/placeservice/controller/CongestionController.java delete mode 100644 place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java delete mode 100644 place-service/src/main/java/com/example/placeservice/service/CongestionService.java diff --git a/README.md b/README.md index 69a8d28..c7a05ff 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. 3. Spring Boot 애플리케이션 설정 - 이제, Spring Boot 애플리케이션에서 PostgreSQL과 연결 설정. application.properties에 PostgreSQL 데이터베이스 설정 추가 ``` - spring.datasource.url=jdbc:postgresql://localhost:5432/star - spring.datasource.username=admin + spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db + spring.datasource.username=root spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect diff --git a/congestion-service/build.gradle b/congestion-service/build.gradle index 899f102..61be6f8 100644 --- a/congestion-service/build.gradle +++ b/congestion-service/build.gradle @@ -24,6 +24,9 @@ repositories { } dependencies { + // 비동기 처리를 위한 webClient 추가 + implementation 'org.springframework.boot:spring-boot-starter-webflux' + // JAXB (XML 파싱) implementation 'javax.xml.bind:jaxb-api:2.3.1' // JAXB Runtime (XML 파싱을 위해 필요) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java index 131b5fd..2415816 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java @@ -1,7 +1,6 @@ package com.example.externalinfoservice.service; import com.example.externalinfoservice.dto.RoadDto; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.Unmarshaller; diff --git a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java index 67d6e02..8205118 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java @@ -26,7 +26,7 @@ public List getAttraction() { // 관광지 정보 조회(place-code) @GetMapping("info/attraction/{place-code}") - public AttractionInfoDto getAttractionInfo(@PathVariable("place-code") long placeCode) { - return attractionService.getAttractionInfoData(placeCode); + public AttractionInfoDto getAttractionInfo(@PathVariable("place-code") long attractionId) { + return attractionService.getAttractionInfoData(attractionId); } } diff --git a/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java b/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java deleted file mode 100644 index 971be0b..0000000 --- a/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.placeservice.controller; - -import com.example.placeservice.service.CongestionService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/admin/congestion") -@RequiredArgsConstructor -public class CongestionController { - private final CongestionService congestionService; - - @GetMapping("/{area-id}") - public String getCongestionData(@PathVariable("area-id") String areaName) { - // 서울 실시간 도시데이터 OpenAPI 데이터 중 인구 혼잡도 데이터 이용 - return congestionService.getCongestion(areaName); - } -} diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java index f5ff08e..29357e9 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java @@ -7,6 +7,7 @@ @Data public class AttractionInfoDto { private Long attraction_id; + private String seoul_attraction_id; private String name; private String address; private BigDecimal lat; @@ -16,8 +17,9 @@ public class AttractionInfoDto { private String close_day; private String use_time; - public AttractionInfoDto(Long attractionId, String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time) { + public AttractionInfoDto(Long attractionId, String seoul_attraction_id,String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time) { this.attraction_id = attractionId; + this.seoul_attraction_id = seoul_attraction_id; this.name = name; this.address = address; this.lat = lat; diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java index 79e169f..1b5b2b9 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java @@ -7,14 +7,16 @@ @Data public class AttractionListDto { private Long attraction_id; + private String seoul_attraction_id; private String name; private String address; private BigDecimal lat; private BigDecimal lon; - public AttractionListDto(Long attraction_id, String name, String address, BigDecimal lat, BigDecimal lon) { + public AttractionListDto(Long attraction_id,String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon) { this.attraction_id = attraction_id; + this.seoul_attraction_id = seoul_attraction_id; this.name = name; this.address = address; this.lat = lat; diff --git a/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java b/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java deleted file mode 100644 index 903e55b..0000000 --- a/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java +++ /dev/null @@ -1,146 +0,0 @@ -package com.example.placeservice.dto.congestion; - -import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.xml.bind.annotation.XmlAccessType; -import jakarta.xml.bind.annotation.XmlAccessorType; -import jakarta.xml.bind.annotation.XmlElement; -import jakarta.xml.bind.annotation.XmlRootElement; -import lombok.Data; - -import java.util.List; - -@Data -@XmlRootElement(name = "Map") -@XmlAccessorType(XmlAccessType.FIELD) -public class CongestionDto { - - @XmlElement(name="SeoulRtd.citydata_ppltn") - @JsonProperty("ppltn_data") - private PpltnData ppltnData; - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class PpltnData { - - @XmlElement(name = "AREA_NM") - @JsonProperty("area_nm") - private String areaName; - - @XmlElement(name = "AREA_CD") - @JsonProperty("area_cd") - private String areaCode; - - @XmlElement(name = "AREA_CONGEST_LVL") - @JsonProperty("area_congest_lvl") - private String areaCongestLvl; - - @XmlElement(name = "AREA_CONGEST_MSG") - @JsonProperty("area_congest_msg") - private String areaCongestionMsg; - - @XmlElement(name = "AREA_PPLTN_MIN") - @JsonProperty("area_ppltn_min") - private int areaPpltnMin; - - @XmlElement(name = "AREA_PPLTN_MAX") - @JsonProperty("area_ppltn_max") - private int areaPpltnMax; - - @XmlElement(name = "MALE_PPLTN_RATE") - @JsonProperty("male_ppltn_rate") - private double malePpltnRate; - - @XmlElement(name = "FEMALE_PPLTN_RATE") - @JsonProperty("female_ppltn_rate") - private double femalePpltnRate; - - @XmlElement(name = "PPLTN_RATE_0") - @JsonProperty("ppltn_rate_0") - private double PpltnRate0; - - @XmlElement(name = "PPLTN_RATE_10") - @JsonProperty("ppltn_rate_10") - private double PpltnRate10; - - @XmlElement(name = "PPLTN_RATE_20") - @JsonProperty("ppltn_rate_20") - private double PpltnRate20; - - @XmlElement(name = "PPLTN_RATE_30") - @JsonProperty("ppltn_rate_30") - private double PpltnRate30; - - @XmlElement(name = "PPLTN_RATE_40") - @JsonProperty("ppltn_rate_40") - private double PpltnRate40; - - @XmlElement(name = "PPLTN_RATE_50") - @JsonProperty("ppltn_rate_50") - private double PpltnRate50; - - @XmlElement(name = "PPLTN_RATE_60") - @JsonProperty("ppltn_rate_60") - private double PpltnRate60; - - @XmlElement(name = "PPLTN_RATE_70") - @JsonProperty("ppltn_rate_70") - private double PpltnRate70; - - @XmlElement(name = "RESNT_PPLTN_RATE") - @JsonProperty("resnt_ppltn_rate") - private double ResntPpltnRate; - - @XmlElement(name = "NON_RESNT_PPLTN_RATE") - @JsonProperty("non_resnt_ppltn_rate") - private double NonResntPpltnRate; - - @XmlElement(name = "REPLACE_YN") - @JsonProperty("replace_yn") - private String replaceYn; - - @XmlElement(name = "PPLTN_TIME") - @JsonProperty("ppltn_time") - private String ppltnTime; - - @XmlElement(name = "FCST_YN") - @JsonProperty("fcst_yn") - private String fcstYn; - - @XmlElement(name = "FCST_PPLTN") - @JsonProperty("fcst_ppltn_wrapper") - private FcstPpltnWrapper FcstPpltnWrapper; - } - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class FcstPpltnWrapper { - @XmlElement(name = "FCST_PPLTN") - @JsonProperty("fcst_ppltn") - private List FcstPpltn; - } - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class FcstPpltn { - @XmlElement(name = "FCST_TIME") - @JsonProperty("fcst_time") - private String fcstTime; - - @XmlElement(name = "FCST_CONGEST_LVL") - @JsonProperty("fcst_congest_lvl") - private String fcstCongestLvl; - - @XmlElement(name = "FCST_PPLTN_MIN") - @JsonProperty("fcst_ppltn_min") - private int fcstPpltnMin; - - @XmlElement(name = "FCST_PPLTN_MAX") - @JsonProperty("fcst_ppltn_max") - private int fcstPpltnMax; - - } - - - - -} diff --git a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java index e8a22a1..9b7b8b9 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java @@ -1,6 +1,5 @@ package com.example.placeservice.entity; -import com.fasterxml.jackson.annotation.JsonBackReference; import jakarta.persistence.*; import lombok.Data; @@ -12,10 +11,10 @@ public class Attraction { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "attraction_id") - private Long attraction_id; + private Long attractionId; - @Column(name = "visit_id", length = 10, nullable = false, unique = true) - private String visitId; + @Column(name = "seoul_attraction_id", length = 10, nullable = false, unique = true) + private String seoulAttractionId; @Column(name="name", length = 200, nullable = false) private String name; @@ -34,13 +33,13 @@ public class Attraction { private String phone; @Column(name="homepage_url", columnDefinition = "TEXT") - private String homepage_url; + private String homepageUrl; @Column(name="close_day", columnDefinition = "TEXT") - private String close_day; + private String closeDay; @Column(name="use_time", columnDefinition = "TEXT") - private String use_time; + private String useTime; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "area_id", nullable = false) diff --git a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java index 13d7a2b..2ac5555 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java @@ -2,6 +2,11 @@ import com.example.placeservice.entity.Area; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; public interface AreaRepository extends JpaRepository { + @Query("SELECT a.name FROM Area a") + List findAllAreaNames(); } diff --git a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java index 2374e53..f43538e 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java @@ -5,7 +5,8 @@ import java.util.Optional; + public interface AttractionRepository extends JpaRepository { - boolean existsByVisitId(String visitId); - Optional findById(Long id); + boolean existsBySeoulAttractionId(String seoul_attraction_id); + Optional findByAttractionId(Long attraction_id); } diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index 87e694c..f4fa117 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -52,23 +52,23 @@ public List fetchDataFromVisitSeoul() { // 3. DTO → Entity로 변환 (매핑) List entities = dto.getAttractions().stream() .map(table -> { - // visit_id 중복 체크 - if (attractionRepository.existsByVisitId(table.getId())) return null; + // seoul_attraction_id 중복 체크 + if (attractionRepository.existsBySeoulAttractionId(table.getId())) return null; // 👇: 조건에 따라 Area 객체를 지정 Area area = findAreaByCondition(table, areaList); // 예: 주소나 지역코드 등으로 판단 if (area == null) return null; Attraction attraction = new Attraction(); - attraction.setVisitId(table.getId()); + attraction.setSeoulAttractionId(table.getId()); attraction.setName(table.getTitle()); attraction.setAddress(table.getNewAddress()); attraction.setLat(new BigDecimal(table.getMapX())); attraction.setLon(new BigDecimal(table.getMapY())); attraction.setPhone(table.getTel()); - attraction.setHomepage_url(table.getHomepage()); - attraction.setClose_day(table.getCloseDay()); - attraction.setUse_time(table.getUseTime()); + attraction.setHomepageUrl(table.getHomepage()); + attraction.setCloseDay(table.getCloseDay()); + attraction.setUseTime(table.getUseTime()); attraction.setArea(area); @@ -113,7 +113,8 @@ public List getAttractionData() { .map(area -> { List attractions = area.getAttractions().stream() .map(attraction -> new AttractionListDto( - attraction.getAttraction_id(), + attraction.getAttractionId(), + attraction.getSeoulAttractionId(), attraction.getName(), attraction.getAddress(), attraction.getLat(), @@ -132,21 +133,22 @@ public List getAttractionData() { } } - public AttractionInfoDto getAttractionInfoData(long placeCode) { + public AttractionInfoDto getAttractionInfoData(long attractionId) { try{ - Attraction attraction = attractionRepository.findById(placeCode) + Attraction attraction = attractionRepository.findByAttractionId(attractionId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "해당 관광지 정보를 찾을 수 없습니다.")); return new AttractionInfoDto( - attraction.getAttraction_id(), + attraction.getAttractionId(), + attraction.getSeoulAttractionId(), attraction.getName(), attraction.getAddress(), attraction.getLat(), attraction.getLon(), attraction.getPhone(), - attraction.getHomepage_url(), - attraction.getClose_day(), - attraction.getUse_time() + attraction.getHomepageUrl(), + attraction.getCloseDay(), + attraction.getUseTime() ); } catch (RuntimeException e) { diff --git a/place-service/src/main/java/com/example/placeservice/service/CongestionService.java b/place-service/src/main/java/com/example/placeservice/service/CongestionService.java deleted file mode 100644 index 6f31875..0000000 --- a/place-service/src/main/java/com/example/placeservice/service/CongestionService.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.placeservice.service; - -import com.example.placeservice.dto.congestion.CongestionDto; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.Unmarshaller; -import org.apache.kafka.common.errors.InvalidRequestException; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.io.IOException; -import java.io.StringReader; - -@Service -public class CongestionService { - - @Value("http://openapi.seoul.go.kr:8088/sample/xml/citydata_ppltn/1/5/") - private String openApiUrl; - - private final RestTemplate restTemplate; - - public CongestionService(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } - - public String getCongestion(String areaName) { - try{ - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String xmlResponse = restTemplate.getForObject(openApiUrl + areaName, String.class); - - // 2. XML 데이터를 DTO로 변환 -> JSON - ObjectMapper objectMapper = new ObjectMapper(); - String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); - - return json; - - }catch (InvalidRequestException e) { - throw new InvalidRequestException(e.getMessage(), e); - } catch(IOException e){ - throw new RuntimeException("외부 API 오류",e); - } catch (Exception e) { - throw new RuntimeException("예상치 못한 오류",e); - } - } - - private CongestionDto xmlToDto(String xml) throws Exception { - // JAXB를 사용해서 XML을 DTO로 변환 - JAXBContext context = JAXBContext.newInstance(CongestionDto.class); - Unmarshaller unmarshaller = context.createUnmarshaller(); - return (CongestionDto) unmarshaller.unmarshal(new StringReader(xml)); - } -} From f87c7e62a7718910d4248505545301598cc500d0 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 23 Apr 2025 10:39:49 +0900 Subject: [PATCH 066/361] =?UTF-8?q?feat:=20Attraction=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=EC=97=90=20seoulAttractionId=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20API=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- congestion-service/build.gradle | 3 + .../service/RoadService.java | 1 - .../controller/AttractionController.java | 4 +- .../controller/CongestionController.java | 21 --- .../dto/attraction/AttractionInfoDto.java | 4 +- .../dto/attraction/AttractionListDto.java | 4 +- .../dto/congestion/CongestionDto.java | 146 ------------------ .../placeservice/entity/Attraction.java | 13 +- .../repository/AreaRepository.java | 5 + .../repository/AttractionRepository.java | 5 +- .../service/AttractionService.java | 28 ++-- .../service/CongestionService.java | 53 ------- 13 files changed, 42 insertions(+), 249 deletions(-) delete mode 100644 place-service/src/main/java/com/example/placeservice/controller/CongestionController.java delete mode 100644 place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java delete mode 100644 place-service/src/main/java/com/example/placeservice/service/CongestionService.java diff --git a/README.md b/README.md index 69a8d28..c7a05ff 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. 3. Spring Boot 애플리케이션 설정 - 이제, Spring Boot 애플리케이션에서 PostgreSQL과 연결 설정. application.properties에 PostgreSQL 데이터베이스 설정 추가 ``` - spring.datasource.url=jdbc:postgresql://localhost:5432/star - spring.datasource.username=admin + spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db + spring.datasource.username=root spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect diff --git a/congestion-service/build.gradle b/congestion-service/build.gradle index 899f102..61be6f8 100644 --- a/congestion-service/build.gradle +++ b/congestion-service/build.gradle @@ -24,6 +24,9 @@ repositories { } dependencies { + // 비동기 처리를 위한 webClient 추가 + implementation 'org.springframework.boot:spring-boot-starter-webflux' + // JAXB (XML 파싱) implementation 'javax.xml.bind:jaxb-api:2.3.1' // JAXB Runtime (XML 파싱을 위해 필요) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java index 131b5fd..2415816 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java @@ -1,7 +1,6 @@ package com.example.externalinfoservice.service; import com.example.externalinfoservice.dto.RoadDto; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.Unmarshaller; diff --git a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java index 67d6e02..8205118 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java @@ -26,7 +26,7 @@ public List getAttraction() { // 관광지 정보 조회(place-code) @GetMapping("info/attraction/{place-code}") - public AttractionInfoDto getAttractionInfo(@PathVariable("place-code") long placeCode) { - return attractionService.getAttractionInfoData(placeCode); + public AttractionInfoDto getAttractionInfo(@PathVariable("place-code") long attractionId) { + return attractionService.getAttractionInfoData(attractionId); } } diff --git a/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java b/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java deleted file mode 100644 index 971be0b..0000000 --- a/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.placeservice.controller; - -import com.example.placeservice.service.CongestionService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/admin/congestion") -@RequiredArgsConstructor -public class CongestionController { - private final CongestionService congestionService; - - @GetMapping("/{area-id}") - public String getCongestionData(@PathVariable("area-id") String areaName) { - // 서울 실시간 도시데이터 OpenAPI 데이터 중 인구 혼잡도 데이터 이용 - return congestionService.getCongestion(areaName); - } -} diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java index f5ff08e..29357e9 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java @@ -7,6 +7,7 @@ @Data public class AttractionInfoDto { private Long attraction_id; + private String seoul_attraction_id; private String name; private String address; private BigDecimal lat; @@ -16,8 +17,9 @@ public class AttractionInfoDto { private String close_day; private String use_time; - public AttractionInfoDto(Long attractionId, String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time) { + public AttractionInfoDto(Long attractionId, String seoul_attraction_id,String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time) { this.attraction_id = attractionId; + this.seoul_attraction_id = seoul_attraction_id; this.name = name; this.address = address; this.lat = lat; diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java index 79e169f..1b5b2b9 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java @@ -7,14 +7,16 @@ @Data public class AttractionListDto { private Long attraction_id; + private String seoul_attraction_id; private String name; private String address; private BigDecimal lat; private BigDecimal lon; - public AttractionListDto(Long attraction_id, String name, String address, BigDecimal lat, BigDecimal lon) { + public AttractionListDto(Long attraction_id,String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon) { this.attraction_id = attraction_id; + this.seoul_attraction_id = seoul_attraction_id; this.name = name; this.address = address; this.lat = lat; diff --git a/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java b/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java deleted file mode 100644 index 903e55b..0000000 --- a/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java +++ /dev/null @@ -1,146 +0,0 @@ -package com.example.placeservice.dto.congestion; - -import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.xml.bind.annotation.XmlAccessType; -import jakarta.xml.bind.annotation.XmlAccessorType; -import jakarta.xml.bind.annotation.XmlElement; -import jakarta.xml.bind.annotation.XmlRootElement; -import lombok.Data; - -import java.util.List; - -@Data -@XmlRootElement(name = "Map") -@XmlAccessorType(XmlAccessType.FIELD) -public class CongestionDto { - - @XmlElement(name="SeoulRtd.citydata_ppltn") - @JsonProperty("ppltn_data") - private PpltnData ppltnData; - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class PpltnData { - - @XmlElement(name = "AREA_NM") - @JsonProperty("area_nm") - private String areaName; - - @XmlElement(name = "AREA_CD") - @JsonProperty("area_cd") - private String areaCode; - - @XmlElement(name = "AREA_CONGEST_LVL") - @JsonProperty("area_congest_lvl") - private String areaCongestLvl; - - @XmlElement(name = "AREA_CONGEST_MSG") - @JsonProperty("area_congest_msg") - private String areaCongestionMsg; - - @XmlElement(name = "AREA_PPLTN_MIN") - @JsonProperty("area_ppltn_min") - private int areaPpltnMin; - - @XmlElement(name = "AREA_PPLTN_MAX") - @JsonProperty("area_ppltn_max") - private int areaPpltnMax; - - @XmlElement(name = "MALE_PPLTN_RATE") - @JsonProperty("male_ppltn_rate") - private double malePpltnRate; - - @XmlElement(name = "FEMALE_PPLTN_RATE") - @JsonProperty("female_ppltn_rate") - private double femalePpltnRate; - - @XmlElement(name = "PPLTN_RATE_0") - @JsonProperty("ppltn_rate_0") - private double PpltnRate0; - - @XmlElement(name = "PPLTN_RATE_10") - @JsonProperty("ppltn_rate_10") - private double PpltnRate10; - - @XmlElement(name = "PPLTN_RATE_20") - @JsonProperty("ppltn_rate_20") - private double PpltnRate20; - - @XmlElement(name = "PPLTN_RATE_30") - @JsonProperty("ppltn_rate_30") - private double PpltnRate30; - - @XmlElement(name = "PPLTN_RATE_40") - @JsonProperty("ppltn_rate_40") - private double PpltnRate40; - - @XmlElement(name = "PPLTN_RATE_50") - @JsonProperty("ppltn_rate_50") - private double PpltnRate50; - - @XmlElement(name = "PPLTN_RATE_60") - @JsonProperty("ppltn_rate_60") - private double PpltnRate60; - - @XmlElement(name = "PPLTN_RATE_70") - @JsonProperty("ppltn_rate_70") - private double PpltnRate70; - - @XmlElement(name = "RESNT_PPLTN_RATE") - @JsonProperty("resnt_ppltn_rate") - private double ResntPpltnRate; - - @XmlElement(name = "NON_RESNT_PPLTN_RATE") - @JsonProperty("non_resnt_ppltn_rate") - private double NonResntPpltnRate; - - @XmlElement(name = "REPLACE_YN") - @JsonProperty("replace_yn") - private String replaceYn; - - @XmlElement(name = "PPLTN_TIME") - @JsonProperty("ppltn_time") - private String ppltnTime; - - @XmlElement(name = "FCST_YN") - @JsonProperty("fcst_yn") - private String fcstYn; - - @XmlElement(name = "FCST_PPLTN") - @JsonProperty("fcst_ppltn_wrapper") - private FcstPpltnWrapper FcstPpltnWrapper; - } - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class FcstPpltnWrapper { - @XmlElement(name = "FCST_PPLTN") - @JsonProperty("fcst_ppltn") - private List FcstPpltn; - } - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class FcstPpltn { - @XmlElement(name = "FCST_TIME") - @JsonProperty("fcst_time") - private String fcstTime; - - @XmlElement(name = "FCST_CONGEST_LVL") - @JsonProperty("fcst_congest_lvl") - private String fcstCongestLvl; - - @XmlElement(name = "FCST_PPLTN_MIN") - @JsonProperty("fcst_ppltn_min") - private int fcstPpltnMin; - - @XmlElement(name = "FCST_PPLTN_MAX") - @JsonProperty("fcst_ppltn_max") - private int fcstPpltnMax; - - } - - - - -} diff --git a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java index e8a22a1..9b7b8b9 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java @@ -1,6 +1,5 @@ package com.example.placeservice.entity; -import com.fasterxml.jackson.annotation.JsonBackReference; import jakarta.persistence.*; import lombok.Data; @@ -12,10 +11,10 @@ public class Attraction { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "attraction_id") - private Long attraction_id; + private Long attractionId; - @Column(name = "visit_id", length = 10, nullable = false, unique = true) - private String visitId; + @Column(name = "seoul_attraction_id", length = 10, nullable = false, unique = true) + private String seoulAttractionId; @Column(name="name", length = 200, nullable = false) private String name; @@ -34,13 +33,13 @@ public class Attraction { private String phone; @Column(name="homepage_url", columnDefinition = "TEXT") - private String homepage_url; + private String homepageUrl; @Column(name="close_day", columnDefinition = "TEXT") - private String close_day; + private String closeDay; @Column(name="use_time", columnDefinition = "TEXT") - private String use_time; + private String useTime; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "area_id", nullable = false) diff --git a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java index 13d7a2b..2ac5555 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java @@ -2,6 +2,11 @@ import com.example.placeservice.entity.Area; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; public interface AreaRepository extends JpaRepository { + @Query("SELECT a.name FROM Area a") + List findAllAreaNames(); } diff --git a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java index 2374e53..f43538e 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java @@ -5,7 +5,8 @@ import java.util.Optional; + public interface AttractionRepository extends JpaRepository { - boolean existsByVisitId(String visitId); - Optional findById(Long id); + boolean existsBySeoulAttractionId(String seoul_attraction_id); + Optional findByAttractionId(Long attraction_id); } diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index 87e694c..f4fa117 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -52,23 +52,23 @@ public List fetchDataFromVisitSeoul() { // 3. DTO → Entity로 변환 (매핑) List entities = dto.getAttractions().stream() .map(table -> { - // visit_id 중복 체크 - if (attractionRepository.existsByVisitId(table.getId())) return null; + // seoul_attraction_id 중복 체크 + if (attractionRepository.existsBySeoulAttractionId(table.getId())) return null; // 👇: 조건에 따라 Area 객체를 지정 Area area = findAreaByCondition(table, areaList); // 예: 주소나 지역코드 등으로 판단 if (area == null) return null; Attraction attraction = new Attraction(); - attraction.setVisitId(table.getId()); + attraction.setSeoulAttractionId(table.getId()); attraction.setName(table.getTitle()); attraction.setAddress(table.getNewAddress()); attraction.setLat(new BigDecimal(table.getMapX())); attraction.setLon(new BigDecimal(table.getMapY())); attraction.setPhone(table.getTel()); - attraction.setHomepage_url(table.getHomepage()); - attraction.setClose_day(table.getCloseDay()); - attraction.setUse_time(table.getUseTime()); + attraction.setHomepageUrl(table.getHomepage()); + attraction.setCloseDay(table.getCloseDay()); + attraction.setUseTime(table.getUseTime()); attraction.setArea(area); @@ -113,7 +113,8 @@ public List getAttractionData() { .map(area -> { List attractions = area.getAttractions().stream() .map(attraction -> new AttractionListDto( - attraction.getAttraction_id(), + attraction.getAttractionId(), + attraction.getSeoulAttractionId(), attraction.getName(), attraction.getAddress(), attraction.getLat(), @@ -132,21 +133,22 @@ public List getAttractionData() { } } - public AttractionInfoDto getAttractionInfoData(long placeCode) { + public AttractionInfoDto getAttractionInfoData(long attractionId) { try{ - Attraction attraction = attractionRepository.findById(placeCode) + Attraction attraction = attractionRepository.findByAttractionId(attractionId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "해당 관광지 정보를 찾을 수 없습니다.")); return new AttractionInfoDto( - attraction.getAttraction_id(), + attraction.getAttractionId(), + attraction.getSeoulAttractionId(), attraction.getName(), attraction.getAddress(), attraction.getLat(), attraction.getLon(), attraction.getPhone(), - attraction.getHomepage_url(), - attraction.getClose_day(), - attraction.getUse_time() + attraction.getHomepageUrl(), + attraction.getCloseDay(), + attraction.getUseTime() ); } catch (RuntimeException e) { diff --git a/place-service/src/main/java/com/example/placeservice/service/CongestionService.java b/place-service/src/main/java/com/example/placeservice/service/CongestionService.java deleted file mode 100644 index 6f31875..0000000 --- a/place-service/src/main/java/com/example/placeservice/service/CongestionService.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.placeservice.service; - -import com.example.placeservice.dto.congestion.CongestionDto; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.Unmarshaller; -import org.apache.kafka.common.errors.InvalidRequestException; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.io.IOException; -import java.io.StringReader; - -@Service -public class CongestionService { - - @Value("http://openapi.seoul.go.kr:8088/sample/xml/citydata_ppltn/1/5/") - private String openApiUrl; - - private final RestTemplate restTemplate; - - public CongestionService(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } - - public String getCongestion(String areaName) { - try{ - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String xmlResponse = restTemplate.getForObject(openApiUrl + areaName, String.class); - - // 2. XML 데이터를 DTO로 변환 -> JSON - ObjectMapper objectMapper = new ObjectMapper(); - String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); - - return json; - - }catch (InvalidRequestException e) { - throw new InvalidRequestException(e.getMessage(), e); - } catch(IOException e){ - throw new RuntimeException("외부 API 오류",e); - } catch (Exception e) { - throw new RuntimeException("예상치 못한 오류",e); - } - } - - private CongestionDto xmlToDto(String xml) throws Exception { - // JAXB를 사용해서 XML을 DTO로 변환 - JAXBContext context = JAXBContext.newInstance(CongestionDto.class); - Unmarshaller unmarshaller = context.createUnmarshaller(); - return (CongestionDto) unmarshaller.unmarshal(new StringReader(xml)); - } -} From 049d4a0a827f6d60fe180811895a5a0bb2e0036a Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 23 Apr 2025 10:39:49 +0900 Subject: [PATCH 067/361] =?UTF-8?q?feat:=20Attraction=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=EC=97=90=20seoulAttractionId=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20API=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- congestion-service/build.gradle | 3 + .../service/RoadService.java | 1 - .../controller/AttractionController.java | 4 +- .../controller/CongestionController.java | 21 --- .../dto/attraction/AttractionInfoDto.java | 4 +- .../dto/attraction/AttractionListDto.java | 4 +- .../dto/congestion/CongestionDto.java | 146 ------------------ .../placeservice/entity/Attraction.java | 13 +- .../repository/AreaRepository.java | 5 + .../repository/AttractionRepository.java | 5 +- .../service/AttractionService.java | 28 ++-- .../service/CongestionService.java | 53 ------- 13 files changed, 42 insertions(+), 249 deletions(-) delete mode 100644 place-service/src/main/java/com/example/placeservice/controller/CongestionController.java delete mode 100644 place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java delete mode 100644 place-service/src/main/java/com/example/placeservice/service/CongestionService.java diff --git a/README.md b/README.md index 69a8d28..c7a05ff 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. 3. Spring Boot 애플리케이션 설정 - 이제, Spring Boot 애플리케이션에서 PostgreSQL과 연결 설정. application.properties에 PostgreSQL 데이터베이스 설정 추가 ``` - spring.datasource.url=jdbc:postgresql://localhost:5432/star - spring.datasource.username=admin + spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db + spring.datasource.username=root spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect diff --git a/congestion-service/build.gradle b/congestion-service/build.gradle index 899f102..61be6f8 100644 --- a/congestion-service/build.gradle +++ b/congestion-service/build.gradle @@ -24,6 +24,9 @@ repositories { } dependencies { + // 비동기 처리를 위한 webClient 추가 + implementation 'org.springframework.boot:spring-boot-starter-webflux' + // JAXB (XML 파싱) implementation 'javax.xml.bind:jaxb-api:2.3.1' // JAXB Runtime (XML 파싱을 위해 필요) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java index 131b5fd..2415816 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java @@ -1,7 +1,6 @@ package com.example.externalinfoservice.service; import com.example.externalinfoservice.dto.RoadDto; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.Unmarshaller; diff --git a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java index 67d6e02..8205118 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AttractionController.java @@ -26,7 +26,7 @@ public List getAttraction() { // 관광지 정보 조회(place-code) @GetMapping("info/attraction/{place-code}") - public AttractionInfoDto getAttractionInfo(@PathVariable("place-code") long placeCode) { - return attractionService.getAttractionInfoData(placeCode); + public AttractionInfoDto getAttractionInfo(@PathVariable("place-code") long attractionId) { + return attractionService.getAttractionInfoData(attractionId); } } diff --git a/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java b/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java deleted file mode 100644 index 971be0b..0000000 --- a/place-service/src/main/java/com/example/placeservice/controller/CongestionController.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.placeservice.controller; - -import com.example.placeservice.service.CongestionService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/admin/congestion") -@RequiredArgsConstructor -public class CongestionController { - private final CongestionService congestionService; - - @GetMapping("/{area-id}") - public String getCongestionData(@PathVariable("area-id") String areaName) { - // 서울 실시간 도시데이터 OpenAPI 데이터 중 인구 혼잡도 데이터 이용 - return congestionService.getCongestion(areaName); - } -} diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java index f5ff08e..29357e9 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java @@ -7,6 +7,7 @@ @Data public class AttractionInfoDto { private Long attraction_id; + private String seoul_attraction_id; private String name; private String address; private BigDecimal lat; @@ -16,8 +17,9 @@ public class AttractionInfoDto { private String close_day; private String use_time; - public AttractionInfoDto(Long attractionId, String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time) { + public AttractionInfoDto(Long attractionId, String seoul_attraction_id,String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time) { this.attraction_id = attractionId; + this.seoul_attraction_id = seoul_attraction_id; this.name = name; this.address = address; this.lat = lat; diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java index 79e169f..1b5b2b9 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java @@ -7,14 +7,16 @@ @Data public class AttractionListDto { private Long attraction_id; + private String seoul_attraction_id; private String name; private String address; private BigDecimal lat; private BigDecimal lon; - public AttractionListDto(Long attraction_id, String name, String address, BigDecimal lat, BigDecimal lon) { + public AttractionListDto(Long attraction_id,String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon) { this.attraction_id = attraction_id; + this.seoul_attraction_id = seoul_attraction_id; this.name = name; this.address = address; this.lat = lat; diff --git a/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java b/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java deleted file mode 100644 index 903e55b..0000000 --- a/place-service/src/main/java/com/example/placeservice/dto/congestion/CongestionDto.java +++ /dev/null @@ -1,146 +0,0 @@ -package com.example.placeservice.dto.congestion; - -import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.xml.bind.annotation.XmlAccessType; -import jakarta.xml.bind.annotation.XmlAccessorType; -import jakarta.xml.bind.annotation.XmlElement; -import jakarta.xml.bind.annotation.XmlRootElement; -import lombok.Data; - -import java.util.List; - -@Data -@XmlRootElement(name = "Map") -@XmlAccessorType(XmlAccessType.FIELD) -public class CongestionDto { - - @XmlElement(name="SeoulRtd.citydata_ppltn") - @JsonProperty("ppltn_data") - private PpltnData ppltnData; - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class PpltnData { - - @XmlElement(name = "AREA_NM") - @JsonProperty("area_nm") - private String areaName; - - @XmlElement(name = "AREA_CD") - @JsonProperty("area_cd") - private String areaCode; - - @XmlElement(name = "AREA_CONGEST_LVL") - @JsonProperty("area_congest_lvl") - private String areaCongestLvl; - - @XmlElement(name = "AREA_CONGEST_MSG") - @JsonProperty("area_congest_msg") - private String areaCongestionMsg; - - @XmlElement(name = "AREA_PPLTN_MIN") - @JsonProperty("area_ppltn_min") - private int areaPpltnMin; - - @XmlElement(name = "AREA_PPLTN_MAX") - @JsonProperty("area_ppltn_max") - private int areaPpltnMax; - - @XmlElement(name = "MALE_PPLTN_RATE") - @JsonProperty("male_ppltn_rate") - private double malePpltnRate; - - @XmlElement(name = "FEMALE_PPLTN_RATE") - @JsonProperty("female_ppltn_rate") - private double femalePpltnRate; - - @XmlElement(name = "PPLTN_RATE_0") - @JsonProperty("ppltn_rate_0") - private double PpltnRate0; - - @XmlElement(name = "PPLTN_RATE_10") - @JsonProperty("ppltn_rate_10") - private double PpltnRate10; - - @XmlElement(name = "PPLTN_RATE_20") - @JsonProperty("ppltn_rate_20") - private double PpltnRate20; - - @XmlElement(name = "PPLTN_RATE_30") - @JsonProperty("ppltn_rate_30") - private double PpltnRate30; - - @XmlElement(name = "PPLTN_RATE_40") - @JsonProperty("ppltn_rate_40") - private double PpltnRate40; - - @XmlElement(name = "PPLTN_RATE_50") - @JsonProperty("ppltn_rate_50") - private double PpltnRate50; - - @XmlElement(name = "PPLTN_RATE_60") - @JsonProperty("ppltn_rate_60") - private double PpltnRate60; - - @XmlElement(name = "PPLTN_RATE_70") - @JsonProperty("ppltn_rate_70") - private double PpltnRate70; - - @XmlElement(name = "RESNT_PPLTN_RATE") - @JsonProperty("resnt_ppltn_rate") - private double ResntPpltnRate; - - @XmlElement(name = "NON_RESNT_PPLTN_RATE") - @JsonProperty("non_resnt_ppltn_rate") - private double NonResntPpltnRate; - - @XmlElement(name = "REPLACE_YN") - @JsonProperty("replace_yn") - private String replaceYn; - - @XmlElement(name = "PPLTN_TIME") - @JsonProperty("ppltn_time") - private String ppltnTime; - - @XmlElement(name = "FCST_YN") - @JsonProperty("fcst_yn") - private String fcstYn; - - @XmlElement(name = "FCST_PPLTN") - @JsonProperty("fcst_ppltn_wrapper") - private FcstPpltnWrapper FcstPpltnWrapper; - } - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class FcstPpltnWrapper { - @XmlElement(name = "FCST_PPLTN") - @JsonProperty("fcst_ppltn") - private List FcstPpltn; - } - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class FcstPpltn { - @XmlElement(name = "FCST_TIME") - @JsonProperty("fcst_time") - private String fcstTime; - - @XmlElement(name = "FCST_CONGEST_LVL") - @JsonProperty("fcst_congest_lvl") - private String fcstCongestLvl; - - @XmlElement(name = "FCST_PPLTN_MIN") - @JsonProperty("fcst_ppltn_min") - private int fcstPpltnMin; - - @XmlElement(name = "FCST_PPLTN_MAX") - @JsonProperty("fcst_ppltn_max") - private int fcstPpltnMax; - - } - - - - -} diff --git a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java index e8a22a1..9b7b8b9 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java @@ -1,6 +1,5 @@ package com.example.placeservice.entity; -import com.fasterxml.jackson.annotation.JsonBackReference; import jakarta.persistence.*; import lombok.Data; @@ -12,10 +11,10 @@ public class Attraction { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "attraction_id") - private Long attraction_id; + private Long attractionId; - @Column(name = "visit_id", length = 10, nullable = false, unique = true) - private String visitId; + @Column(name = "seoul_attraction_id", length = 10, nullable = false, unique = true) + private String seoulAttractionId; @Column(name="name", length = 200, nullable = false) private String name; @@ -34,13 +33,13 @@ public class Attraction { private String phone; @Column(name="homepage_url", columnDefinition = "TEXT") - private String homepage_url; + private String homepageUrl; @Column(name="close_day", columnDefinition = "TEXT") - private String close_day; + private String closeDay; @Column(name="use_time", columnDefinition = "TEXT") - private String use_time; + private String useTime; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "area_id", nullable = false) diff --git a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java index 13d7a2b..2ac5555 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java @@ -2,6 +2,11 @@ import com.example.placeservice.entity.Area; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; public interface AreaRepository extends JpaRepository { + @Query("SELECT a.name FROM Area a") + List findAllAreaNames(); } diff --git a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java index 2374e53..f43538e 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java @@ -5,7 +5,8 @@ import java.util.Optional; + public interface AttractionRepository extends JpaRepository { - boolean existsByVisitId(String visitId); - Optional findById(Long id); + boolean existsBySeoulAttractionId(String seoul_attraction_id); + Optional findByAttractionId(Long attraction_id); } diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index 87e694c..f4fa117 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -52,23 +52,23 @@ public List fetchDataFromVisitSeoul() { // 3. DTO → Entity로 변환 (매핑) List entities = dto.getAttractions().stream() .map(table -> { - // visit_id 중복 체크 - if (attractionRepository.existsByVisitId(table.getId())) return null; + // seoul_attraction_id 중복 체크 + if (attractionRepository.existsBySeoulAttractionId(table.getId())) return null; // 👇: 조건에 따라 Area 객체를 지정 Area area = findAreaByCondition(table, areaList); // 예: 주소나 지역코드 등으로 판단 if (area == null) return null; Attraction attraction = new Attraction(); - attraction.setVisitId(table.getId()); + attraction.setSeoulAttractionId(table.getId()); attraction.setName(table.getTitle()); attraction.setAddress(table.getNewAddress()); attraction.setLat(new BigDecimal(table.getMapX())); attraction.setLon(new BigDecimal(table.getMapY())); attraction.setPhone(table.getTel()); - attraction.setHomepage_url(table.getHomepage()); - attraction.setClose_day(table.getCloseDay()); - attraction.setUse_time(table.getUseTime()); + attraction.setHomepageUrl(table.getHomepage()); + attraction.setCloseDay(table.getCloseDay()); + attraction.setUseTime(table.getUseTime()); attraction.setArea(area); @@ -113,7 +113,8 @@ public List getAttractionData() { .map(area -> { List attractions = area.getAttractions().stream() .map(attraction -> new AttractionListDto( - attraction.getAttraction_id(), + attraction.getAttractionId(), + attraction.getSeoulAttractionId(), attraction.getName(), attraction.getAddress(), attraction.getLat(), @@ -132,21 +133,22 @@ public List getAttractionData() { } } - public AttractionInfoDto getAttractionInfoData(long placeCode) { + public AttractionInfoDto getAttractionInfoData(long attractionId) { try{ - Attraction attraction = attractionRepository.findById(placeCode) + Attraction attraction = attractionRepository.findByAttractionId(attractionId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "해당 관광지 정보를 찾을 수 없습니다.")); return new AttractionInfoDto( - attraction.getAttraction_id(), + attraction.getAttractionId(), + attraction.getSeoulAttractionId(), attraction.getName(), attraction.getAddress(), attraction.getLat(), attraction.getLon(), attraction.getPhone(), - attraction.getHomepage_url(), - attraction.getClose_day(), - attraction.getUse_time() + attraction.getHomepageUrl(), + attraction.getCloseDay(), + attraction.getUseTime() ); } catch (RuntimeException e) { diff --git a/place-service/src/main/java/com/example/placeservice/service/CongestionService.java b/place-service/src/main/java/com/example/placeservice/service/CongestionService.java deleted file mode 100644 index 6f31875..0000000 --- a/place-service/src/main/java/com/example/placeservice/service/CongestionService.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.placeservice.service; - -import com.example.placeservice.dto.congestion.CongestionDto; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.Unmarshaller; -import org.apache.kafka.common.errors.InvalidRequestException; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.io.IOException; -import java.io.StringReader; - -@Service -public class CongestionService { - - @Value("http://openapi.seoul.go.kr:8088/sample/xml/citydata_ppltn/1/5/") - private String openApiUrl; - - private final RestTemplate restTemplate; - - public CongestionService(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } - - public String getCongestion(String areaName) { - try{ - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String xmlResponse = restTemplate.getForObject(openApiUrl + areaName, String.class); - - // 2. XML 데이터를 DTO로 변환 -> JSON - ObjectMapper objectMapper = new ObjectMapper(); - String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); - - return json; - - }catch (InvalidRequestException e) { - throw new InvalidRequestException(e.getMessage(), e); - } catch(IOException e){ - throw new RuntimeException("외부 API 오류",e); - } catch (Exception e) { - throw new RuntimeException("예상치 못한 오류",e); - } - } - - private CongestionDto xmlToDto(String xml) throws Exception { - // JAXB를 사용해서 XML을 DTO로 변환 - JAXBContext context = JAXBContext.newInstance(CongestionDto.class); - Unmarshaller unmarshaller = context.createUnmarshaller(); - return (CongestionDto) unmarshaller.unmarshal(new StringReader(xml)); - } -} From 12aa8254320da23b161de4f0ebab78bb211ff9ea Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 23 Apr 2025 10:47:33 +0900 Subject: [PATCH 068/361] =?UTF-8?q?Refactor:kakaoid=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/RestaurantDetailResponse.java | 4 ++++ .../example/placeservice/entity/Restaurant.java | 15 +++++++++++++++ .../placeservice/service/KakaoMapClient.java | 6 +++--- .../placeservice/service/RestaurantService.java | 2 ++ .../src/main/resources/application.properties | 2 +- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java b/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java index e9ce8d5..a7effcc 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java +++ b/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java @@ -24,6 +24,9 @@ public class RestaurantDetailResponse { private String categoryGroupName; // 추가 private String categoryName; // 추가 + private String kakao_id; + + public static RestaurantDetailResponse fromEntity(Restaurant restaurant) { return RestaurantDetailResponse.builder() .address_name(restaurant.getAddress()) @@ -32,6 +35,7 @@ public static RestaurantDetailResponse fromEntity(Restaurant restaurant) { .category_name(restaurant.getCategoryName()) // 고정X (DB) .distance("") // 그대로 비워두기 .id(String.valueOf(restaurant.getRestaurantId())) + .kakao_id(restaurant.getKakao_id()) .phone(restaurant.getPhone()) .place_name(restaurant.getName()) .place_url(restaurant.getKakaomap_url()) diff --git a/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java b/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java index 36ae9a3..552bff8 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java @@ -42,6 +42,8 @@ public class Restaurant { @Column(length = 20) private String category_code; // 카테고리 코드 (FD6: 음식점) +// @Column(length = 30) +// private String kakao_id; // 카카오 API가 주는 id 저장 (String) //추가 @Column(length = 100) @@ -50,4 +52,17 @@ public class Restaurant { @Column(length = 200) private String categoryName; // (예시) 음식점 > 한식 > 육류,고기 + @Column(name = "kakao_id", length = 30) + private String kakao_id; + + public String getKakao_id() { + return kakao_id; + } + + public void setKakao_id(String kakao_id) { + this.kakao_id = kakao_id; + } + } + + diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java index 82be371..0fde1be 100644 --- a/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java @@ -26,9 +26,9 @@ public JsonNode searchRestaurantsByCategory(BigDecimal lon, BigDecimal lat, int // Kakao API 호출 URL 세팅 UriComponentsBuilder builder = UriComponentsBuilder .fromHttpUrl("https://dapi.kakao.com/v2/local/search/category.json") - .queryParam("category_group_code", "FD6") // FD6: 음식점 카테고리 - .queryParam("x", lon) // x: 경도(lon) - .queryParam("y", lat) // y: 위도(lat) + .queryParam("category_group_code", "FD6") // 음식점 카테고리 + .queryParam("x", lon) // x: 경도 + .queryParam("y", lat) // y: 위도 .queryParam("sort", "accuracy") // 정확도순 정렬 .queryParam("page", page) // 요청 페이지 .queryParam("size", 15); // 한 페이지에 최대 15개 결과 diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java index d40c820..eb3c2fa 100644 --- a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java +++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java @@ -61,6 +61,8 @@ public List saveRestaurantsNearbyArea(Area area) { restaurant.setCategoryGroupName(doc.has("category_group_name") ? doc.get("category_group_name").asText() : null); restaurant.setCategoryName(doc.has("category_name") ? doc.get("category_name").asText() : null); + restaurant.setKakao_id(doc.get("id").asText()); + savedRestaurants.add(restaurant); restaurantRepository.save(restaurant); savedCount++; // 저장한 개수 카운트 diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 439bd92..fe42919 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -9,7 +9,7 @@ spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.defer-datasource-initialization=true -spring.jpa.hibernate.ddl-auto=create +spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.sql.init.mode=always From b155bc4bec1e574db762fa6f082b92e2c4e94909 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 23 Apr 2025 10:47:33 +0900 Subject: [PATCH 069/361] =?UTF-8?q?Refactor:kakaoid=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/RestaurantDetailResponse.java | 4 ++++ .../example/placeservice/entity/Restaurant.java | 15 +++++++++++++++ .../placeservice/service/KakaoMapClient.java | 6 +++--- .../placeservice/service/RestaurantService.java | 2 ++ .../src/main/resources/application.properties | 2 +- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java b/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java index e9ce8d5..a7effcc 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java +++ b/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java @@ -24,6 +24,9 @@ public class RestaurantDetailResponse { private String categoryGroupName; // 추가 private String categoryName; // 추가 + private String kakao_id; + + public static RestaurantDetailResponse fromEntity(Restaurant restaurant) { return RestaurantDetailResponse.builder() .address_name(restaurant.getAddress()) @@ -32,6 +35,7 @@ public static RestaurantDetailResponse fromEntity(Restaurant restaurant) { .category_name(restaurant.getCategoryName()) // 고정X (DB) .distance("") // 그대로 비워두기 .id(String.valueOf(restaurant.getRestaurantId())) + .kakao_id(restaurant.getKakao_id()) .phone(restaurant.getPhone()) .place_name(restaurant.getName()) .place_url(restaurant.getKakaomap_url()) diff --git a/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java b/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java index 36ae9a3..552bff8 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java @@ -42,6 +42,8 @@ public class Restaurant { @Column(length = 20) private String category_code; // 카테고리 코드 (FD6: 음식점) +// @Column(length = 30) +// private String kakao_id; // 카카오 API가 주는 id 저장 (String) //추가 @Column(length = 100) @@ -50,4 +52,17 @@ public class Restaurant { @Column(length = 200) private String categoryName; // (예시) 음식점 > 한식 > 육류,고기 + @Column(name = "kakao_id", length = 30) + private String kakao_id; + + public String getKakao_id() { + return kakao_id; + } + + public void setKakao_id(String kakao_id) { + this.kakao_id = kakao_id; + } + } + + diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java index 82be371..0fde1be 100644 --- a/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java @@ -26,9 +26,9 @@ public JsonNode searchRestaurantsByCategory(BigDecimal lon, BigDecimal lat, int // Kakao API 호출 URL 세팅 UriComponentsBuilder builder = UriComponentsBuilder .fromHttpUrl("https://dapi.kakao.com/v2/local/search/category.json") - .queryParam("category_group_code", "FD6") // FD6: 음식점 카테고리 - .queryParam("x", lon) // x: 경도(lon) - .queryParam("y", lat) // y: 위도(lat) + .queryParam("category_group_code", "FD6") // 음식점 카테고리 + .queryParam("x", lon) // x: 경도 + .queryParam("y", lat) // y: 위도 .queryParam("sort", "accuracy") // 정확도순 정렬 .queryParam("page", page) // 요청 페이지 .queryParam("size", 15); // 한 페이지에 최대 15개 결과 diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java index d40c820..eb3c2fa 100644 --- a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java +++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java @@ -61,6 +61,8 @@ public List saveRestaurantsNearbyArea(Area area) { restaurant.setCategoryGroupName(doc.has("category_group_name") ? doc.get("category_group_name").asText() : null); restaurant.setCategoryName(doc.has("category_name") ? doc.get("category_name").asText() : null); + restaurant.setKakao_id(doc.get("id").asText()); + savedRestaurants.add(restaurant); restaurantRepository.save(restaurant); savedCount++; // 저장한 개수 카운트 diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 439bd92..fe42919 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -9,7 +9,7 @@ spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.defer-datasource-initialization=true -spring.jpa.hibernate.ddl-auto=create +spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.sql.init.mode=always From 4d4f7dbe6b349af17d471bdffbcbc458cf93038a Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 23 Apr 2025 10:49:20 +0900 Subject: [PATCH 070/361] accommodation data store complete --- place-service/build.gradle | 3 +- .../placeservice/config/DataInitializer.java | 63 +++++++ .../controller/AccommodationController.java | 61 +++++++ .../placeservice/dto/AccommodationDto.java | 21 +++ .../dto/AccommodationResponse.java | 81 +++++++++ .../placeservice/entity/Accommodation.java | 28 +++ .../com/example/placeservice/entity/Area.java | 12 +- .../repository/AccommodationRepository.java | 7 + .../service/AccommodationParseService.java | 60 +++++++ .../service/AccommodationSaveService.java | 161 ++++++++++++++++++ .../service/AccommodationService.java | 21 +++ place-service/src/main/resources/data.sql | 2 +- 12 files changed, 512 insertions(+), 8 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/config/DataInitializer.java create mode 100644 place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AccommodationDto.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AccommodationResponse.java create mode 100644 place-service/src/main/java/com/example/placeservice/entity/Accommodation.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/AccommodationParseService.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/AccommodationSaveService.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/AccommodationService.java diff --git a/place-service/build.gradle b/place-service/build.gradle index 201fa02..d4fdc9f 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -29,7 +29,6 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' - implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-redis' @@ -39,6 +38,8 @@ dependencies { implementation 'org.apache.kafka:kafka-streams' implementation 'org.springframework.kafka:spring-kafka' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0' + implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'org.postgresql:postgresql' diff --git a/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java b/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java new file mode 100644 index 0000000..3bdd033 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java @@ -0,0 +1,63 @@ +package com.example.placeservice.config; + +import com.example.placeservice.dto.AccommodationResponse; +import com.example.placeservice.service.AccommodationParseService; +import com.example.placeservice.service.AccommodationSaveService; +import com.example.placeservice.service.AccommodationService; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class DataInitializer implements CommandLineRunner { + private final AccommodationSaveService accommodationSaveService; + private final AccommodationParseService accommodationParseService; + private final String apiUrl = "http://apis.data.go.kr/B551011/KorService1/searchStay1?arrange=A&areaCode=1&ServiceKey=XxaElYAH0Vl95pt1hDBfkfBYcie4dR%2BbrJbtCdzzmvbVwAIeVnebWMpiZOD0OqkjMZgr39aEFnTBGJ8jjSmEtQ%3D%3D&listYN=Y&MobileOS=ETC&MobileApp=AppTest&arrange=A&numOfRows=500&pageNo=1"; + + @Override + public void run(String... args) throws Exception { + // API 호출 및 XML 파싱 + String xmlData = fetchXmlData(apiUrl); + List items = accommodationParseService.parseXmlData(xmlData); + + // 파싱된 데이터 저장 + accommodationSaveService.saveAccommodation(items); + + System.out.println("데이터 초기화가 완료되었습니다."); + } + + private String fetchXmlData(String apiUrl) throws IOException { + URL url = new URL(apiUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + StringBuilder xmlData = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + xmlData.append(line); + } + return xmlData.toString(); + } finally { + connection.disconnect(); // 연결 해제 + } + } +/* + private String fetchDataFromApi() { + // API 호출 코드 + // 예시: RestTemplate 사용 + RestTemplate restTemplate = new RestTemplate(); + return restTemplate.getForObject(apiUrl, String.class); + } + +*/ +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java new file mode 100644 index 0000000..232c617 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java @@ -0,0 +1,61 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.dto.AccommodationResponse; +import com.example.placeservice.service.AccommodationSaveService; +import com.example.placeservice.service.AccommodationService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; + +@RestController +@RequestMapping("/main/accommodation") +public class AccommodationController { + private final String serviceKey = "XxaElYAH0Vl95pt1hDBfkfBYcie4dR%2BbrJbtCdzzmvbVwAIeVnebWMpiZOD0OqkjMZgr39aEFnTBGJ8jjSmEtQ%3D%3D"; + private final String baseApiUrl = "http://apis.data.go.kr/B551011/KorService1/searchStay1"; + private final AccommodationService accommodationService; + private final AccommodationSaveService accommodationSaveService; + + // 생성자 주입 + public AccommodationController(AccommodationService accommodationService, AccommodationSaveService accommodationSaveService) { + this.accommodationService = accommodationService; + this.accommodationSaveService = accommodationSaveService; + } + + @GetMapping("/list") + public List getAccommodations() throws IOException { + // API 요청 URL 구성 + String apiUrl = baseApiUrl + "?arrange=A&areaCode=1&ServiceKey=" + serviceKey + + "&listYN=Y&MobileOS=ETC&MobileApp=AppTest&numOfRows=500&pageNo=1"; + + // HTTP 연결 및 데이터 가져오기 + String xmlData = fetchXmlData(apiUrl); + + // 서비스를 통해 XML 데이터 파싱 + return accommodationService.getAccommodationData(xmlData); + } + + // XML 데이터를 가져오는 메소드 + private String fetchXmlData(String apiUrl) throws IOException { + URL url = new URL(apiUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + StringBuilder xmlData = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + xmlData.append(line); + } + return xmlData.toString(); + } finally { + connection.disconnect(); // 연결 해제 + } + } +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AccommodationDto.java b/place-service/src/main/java/com/example/placeservice/dto/AccommodationDto.java new file mode 100644 index 0000000..6520c1a --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AccommodationDto.java @@ -0,0 +1,21 @@ +package com.example.placeservice.dto; + +import lombok.*; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AccommodationDto { + private Long accommodationId; + private Long areaId; + private String name; + private String address; + private BigDecimal lat; + private BigDecimal lon; + private String phone; + private String sigunguCode; + private String imageUrl; +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AccommodationResponse.java b/place-service/src/main/java/com/example/placeservice/dto/AccommodationResponse.java new file mode 100644 index 0000000..1ba9a62 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AccommodationResponse.java @@ -0,0 +1,81 @@ +package com.example.placeservice.dto; + +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import lombok.Data; + +import java.util.List; + +@Data +@XmlRootElement(name = "response") +@XmlAccessorType(XmlAccessType.FIELD) +public class AccommodationResponse { + @XmlElement(name = "header") + private Header header; + + @XmlElement(name = "body") + private Body body; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class Header { + @XmlElement(name = "resultCode") + private String resultCode; + + @XmlElement(name = "resultMsg") + private String resultMsg; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class Body { + @XmlElement(name = "items") + private Items items; + + @XmlElement(name = "numOfRows") + private int numOfRows; + + @XmlElement(name = "pageNo") + private int pageNo; + + @XmlElement(name = "totalCount") + private int totalCount; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class Items { + @XmlElement(name = "item") + private List item; // 여기를 List으로 변경 + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class Item { + private String addr1; + private String addr2; + private String areacode; + private String benikia; + private String cat1; + private String cat2; + private String cat3; + private String contentid; + private String contenttypeid; + private String createdtime; + private String firstimage; + private String firstimage2; + private String cpyrhtDivCd; + private String goodstay; + private String hanok; + private String mapx; + private String mapy; + private String mlevel; + private String modifiedtime; + private String tel; + private String title; + private String booktour; + private String sigungucode; + } + } + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java new file mode 100644 index 0000000..0f4473e --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java @@ -0,0 +1,28 @@ +package com.example.placeservice.entity; + +import jakarta.persistence.*; + +import java.math.BigDecimal; + +@Entity +public class Accommodation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long accommodation_id; + + @ManyToOne + @JoinColumn(name = "area_id", referencedColumnName = "area_id") + private Area area; + + private Long content_id; + private String category; + private String name; + private String address; + @Column(precision = 10, scale = 6) + private BigDecimal lat; + @Column(precision = 10, scale = 6) + private BigDecimal lon; + private String phone; + private String sigungu_code; + private String image_url; +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index 6c14791..c768b68 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -1,10 +1,12 @@ package com.example.placeservice.entity; import jakarta.persistence.*; +import lombok.Getter; import java.math.BigDecimal; @Entity +@Getter public class Area { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -23,11 +25,9 @@ public class Area { @Column(name = "category", length = 20, nullable = false) private String category; // 구분 - @Column(name = "x", precision = 10, scale = 6, nullable = false) - private BigDecimal x; // 위도 - - @Column(name = "y", precision = 10, scale = 6, nullable = false) - private BigDecimal y; // 경도 - + @Column(name = "lat", precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 + @Column(name = "lon", precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 } diff --git a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java new file mode 100644 index 0000000..3b73765 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java @@ -0,0 +1,7 @@ +package com.example.placeservice.repository; + +import com.example.placeservice.entity.Accommodation; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AccommodationRepository extends JpaRepository { +} diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationParseService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationParseService.java new file mode 100644 index 0000000..ecb19b6 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationParseService.java @@ -0,0 +1,60 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.AccommodationResponse; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Unmarshaller; +import org.springframework.stereotype.Service; + +import jakarta.xml.bind.JAXBContext; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +@Service +public class AccommodationParseService { + public List parseXmlData(String xmlData) { + if (xmlData == null || xmlData.trim().isEmpty()) { + throw new IllegalArgumentException("XML 데이터가 비어있습니다."); + } + + try { + // JAXB 컨텍스트 생성 + JAXBContext jaxbContext = JAXBContext.newInstance(AccommodationResponse.class); + + // Unmarshaller 생성 + Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + + // XML 문자열을 Java 객체로 변환 + StringReader reader = new StringReader(xmlData); + AccommodationResponse accommodationResponse = (AccommodationResponse) unmarshaller.unmarshal(reader); + + // null 체크 추가 + if (accommodationResponse == null || + accommodationResponse.getBody() == null || + accommodationResponse.getBody().getItems() == null || + accommodationResponse.getBody().getItems().getItem() == null) { + return new ArrayList<>(); // 빈 리스트 반환 + } + + // 이제 getItem()은 List을 반환하므로 바로 사용 가능 + return accommodationResponse.getBody().getItems().getItem(); + + } catch (JAXBException e) { + throw new RuntimeException("XML 파싱 중 오류 발생: " + e.getMessage(), e); + } catch (Exception e) { + throw new RuntimeException("예상치 못한 오류 발생: " + e.getMessage(), e); + } + } + + public void SaveXmlData(String xmlData) { + List items = parseXmlData(xmlData); + + // 각 아이템 정보 출력 + for (AccommodationResponse.Body.Items.Item item : items) { + System.out.println("호텔명: " + item.getTitle()); + System.out.println("주소: " + item.getAddr1() + " " + item.getAddr2()); + System.out.println("전화번호: " + item.getTel()); + System.out.println("---------------------------"); + } + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationSaveService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationSaveService.java new file mode 100644 index 0000000..c12540e --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationSaveService.java @@ -0,0 +1,161 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.AccommodationResponse; +import com.example.placeservice.entity.Area; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; + +@Service +public class AccommodationSaveService { + private final JdbcTemplate jdbcTemplate; + private final EntityManager entityManager; + + public AccommodationSaveService(JdbcTemplate jdbcTemplate, EntityManager entityManager) { + this.jdbcTemplate = jdbcTemplate; + this.entityManager = entityManager; + } + + @Transactional + public void saveAccommodation(List items) { + String sql = "INSERT INTO accommodation (area_id, content_id, category, name, address, lat, lon, phone, sigungu_code, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + List allAreas = entityManager.createQuery("SELECT a FROM Area a", Area.class).getResultList(); + + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + AccommodationResponse.Body.Items.Item item = items.get(i); + + Long areaId = findClosestAreaInMemory(new BigDecimal(item.getMapy()), new BigDecimal(item.getMapx()), allAreas); + ps.setObject(1, areaId); + + // content_id + try { + ps.setLong(2, Long.parseLong(item.getContentid())); + } catch (NumberFormatException e) { + ps.setNull(2, Types.BIGINT); + } + + // category + // 소분류 코드 + // 0100 : 관광호텔 / 0500 : 콘도미니엄 / 0600 : 유스호스텔 / 0700 : 펜션 / 0900 : 모텔 / 1100 : 게하 / 1600 : 한옥 + try { + switch (item.getCat3().substring(5)) { + case "0100": + ps.setString(3, "관광호텔"); break; + case "0500": + ps.setString(3, "콘도미니엄"); break; + case "0600": + ps.setString(3, "유스호스텔"); break; + case "0700": + ps.setString(3, "펜션"); break; + case "0900": + ps.setString(3, "모텔"); break; + case "1100": + ps.setString(3, "게스트하우스"); break; + case "1600": + ps.setString(3, "한옥"); break; + default: + ps.setString(3, "구분없음"); break; + } + } catch (NumberFormatException e) { + ps.setNull(3, Types.VARCHAR); + } + + ps.setString(4, item.getTitle()); + ps.setString(5, item.getAddr1()); + + // lat + try { + if (item.getMapy() != null && !item.getMapy().isEmpty()) { + ps.setBigDecimal(6, new BigDecimal(item.getMapy())); + } else { + ps.setNull(6, Types.NUMERIC); + } + } catch (NumberFormatException e) { + ps.setNull(6, Types.NUMERIC); + } + + // lon + try { + if (item.getMapx() != null && !item.getMapx().isEmpty()) { + ps.setBigDecimal(7, new BigDecimal(item.getMapx())); + } else { + ps.setNull(7, Types.NUMERIC); + } + } catch (NumberFormatException e) { + ps.setNull(7, Types.NUMERIC); + } + + ps.setString(8, item.getTel()); + ps.setString(9, item.getSigungucode()); + + if (item.getFirstimage() != null && !item.getFirstimage().isEmpty()) { + ps.setString(10, item.getFirstimage()); + } else { + ps.setNull(10, Types.VARCHAR); + } + } + + @Override + public int getBatchSize() { + return items.size(); + } + }); + } + + private Long findClosestAreaInMemory(BigDecimal lat, BigDecimal lon, List areas) { + double minDistance = 8000.0; + Long closestAreaId = null; + int count = 0; + + for (Area area : areas) { + if (area.getLat() == null || area.getLon() == null) { + System.out.println("지역 좌표 오류 : 지역번호 " + area.getArea_id()); + continue; + } + double distance = calculateHaversineDistance( + lat.doubleValue(), lon.doubleValue(), + area.getLat().doubleValue(), area.getLon().doubleValue() + ); + if (Double.isNaN(distance)) { + System.out.println("거리가 숫자가 아닙니다 : 지역번호 " + area.getArea_id()); + continue; + } + if (distance < minDistance) { + minDistance = distance; + closestAreaId = area.getArea_id(); + } + } + + return closestAreaId; + } + + private double calculateHaversineDistance(double lat1, double lon1, double lat2, double lon2) { + // 지구 반지름 (km) + final double R = 6371.0; + + // 라디안으로 변환 + double latDistance = Math.toRadians(lat2 - lat1); + double lonDistance = Math.toRadians(lon2 - lon1); + + // 하버사인 공식 + double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) + * Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2); + + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + // 거리 계산 (km) + return R * c; + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java new file mode 100644 index 0000000..859181c --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java @@ -0,0 +1,21 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.AccommodationResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class AccommodationService { + private AccommodationParseService accommodationParseService = new AccommodationParseService(); + + @Autowired + public AccommodationService(AccommodationParseService accommodationParseService) { + this.accommodationParseService = accommodationParseService; + } + + public List getAccommodationData(String xmlData) { + return accommodationParseService.parseXmlData(xmlData); + } +} diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index 5ad3979..4eb8df6 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -1,4 +1,4 @@ -INSERT INTO area (seoul_id, name, name_eng, category, x, y) VALUES +INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), From 135bcb55434db2d4ef86b3dffac74b7d504d0cc5 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 23 Apr 2025 10:49:20 +0900 Subject: [PATCH 071/361] accommodation data store complete --- place-service/build.gradle | 3 +- .../placeservice/config/DataInitializer.java | 63 +++++++ .../controller/AccommodationController.java | 61 +++++++ .../placeservice/dto/AccommodationDto.java | 21 +++ .../dto/AccommodationResponse.java | 81 +++++++++ .../placeservice/entity/Accommodation.java | 28 +++ .../com/example/placeservice/entity/Area.java | 12 +- .../repository/AccommodationRepository.java | 7 + .../service/AccommodationParseService.java | 60 +++++++ .../service/AccommodationSaveService.java | 161 ++++++++++++++++++ .../service/AccommodationService.java | 21 +++ place-service/src/main/resources/data.sql | 2 +- 12 files changed, 512 insertions(+), 8 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/config/DataInitializer.java create mode 100644 place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AccommodationDto.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AccommodationResponse.java create mode 100644 place-service/src/main/java/com/example/placeservice/entity/Accommodation.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/AccommodationParseService.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/AccommodationSaveService.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/AccommodationService.java diff --git a/place-service/build.gradle b/place-service/build.gradle index 201fa02..d4fdc9f 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -29,7 +29,6 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' - implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-redis' @@ -39,6 +38,8 @@ dependencies { implementation 'org.apache.kafka:kafka-streams' implementation 'org.springframework.kafka:spring-kafka' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0' + implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'org.postgresql:postgresql' diff --git a/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java b/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java new file mode 100644 index 0000000..3bdd033 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java @@ -0,0 +1,63 @@ +package com.example.placeservice.config; + +import com.example.placeservice.dto.AccommodationResponse; +import com.example.placeservice.service.AccommodationParseService; +import com.example.placeservice.service.AccommodationSaveService; +import com.example.placeservice.service.AccommodationService; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class DataInitializer implements CommandLineRunner { + private final AccommodationSaveService accommodationSaveService; + private final AccommodationParseService accommodationParseService; + private final String apiUrl = "http://apis.data.go.kr/B551011/KorService1/searchStay1?arrange=A&areaCode=1&ServiceKey=XxaElYAH0Vl95pt1hDBfkfBYcie4dR%2BbrJbtCdzzmvbVwAIeVnebWMpiZOD0OqkjMZgr39aEFnTBGJ8jjSmEtQ%3D%3D&listYN=Y&MobileOS=ETC&MobileApp=AppTest&arrange=A&numOfRows=500&pageNo=1"; + + @Override + public void run(String... args) throws Exception { + // API 호출 및 XML 파싱 + String xmlData = fetchXmlData(apiUrl); + List items = accommodationParseService.parseXmlData(xmlData); + + // 파싱된 데이터 저장 + accommodationSaveService.saveAccommodation(items); + + System.out.println("데이터 초기화가 완료되었습니다."); + } + + private String fetchXmlData(String apiUrl) throws IOException { + URL url = new URL(apiUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + StringBuilder xmlData = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + xmlData.append(line); + } + return xmlData.toString(); + } finally { + connection.disconnect(); // 연결 해제 + } + } +/* + private String fetchDataFromApi() { + // API 호출 코드 + // 예시: RestTemplate 사용 + RestTemplate restTemplate = new RestTemplate(); + return restTemplate.getForObject(apiUrl, String.class); + } + +*/ +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java new file mode 100644 index 0000000..232c617 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java @@ -0,0 +1,61 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.dto.AccommodationResponse; +import com.example.placeservice.service.AccommodationSaveService; +import com.example.placeservice.service.AccommodationService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; + +@RestController +@RequestMapping("/main/accommodation") +public class AccommodationController { + private final String serviceKey = "XxaElYAH0Vl95pt1hDBfkfBYcie4dR%2BbrJbtCdzzmvbVwAIeVnebWMpiZOD0OqkjMZgr39aEFnTBGJ8jjSmEtQ%3D%3D"; + private final String baseApiUrl = "http://apis.data.go.kr/B551011/KorService1/searchStay1"; + private final AccommodationService accommodationService; + private final AccommodationSaveService accommodationSaveService; + + // 생성자 주입 + public AccommodationController(AccommodationService accommodationService, AccommodationSaveService accommodationSaveService) { + this.accommodationService = accommodationService; + this.accommodationSaveService = accommodationSaveService; + } + + @GetMapping("/list") + public List getAccommodations() throws IOException { + // API 요청 URL 구성 + String apiUrl = baseApiUrl + "?arrange=A&areaCode=1&ServiceKey=" + serviceKey + + "&listYN=Y&MobileOS=ETC&MobileApp=AppTest&numOfRows=500&pageNo=1"; + + // HTTP 연결 및 데이터 가져오기 + String xmlData = fetchXmlData(apiUrl); + + // 서비스를 통해 XML 데이터 파싱 + return accommodationService.getAccommodationData(xmlData); + } + + // XML 데이터를 가져오는 메소드 + private String fetchXmlData(String apiUrl) throws IOException { + URL url = new URL(apiUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + StringBuilder xmlData = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + xmlData.append(line); + } + return xmlData.toString(); + } finally { + connection.disconnect(); // 연결 해제 + } + } +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AccommodationDto.java b/place-service/src/main/java/com/example/placeservice/dto/AccommodationDto.java new file mode 100644 index 0000000..6520c1a --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AccommodationDto.java @@ -0,0 +1,21 @@ +package com.example.placeservice.dto; + +import lombok.*; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AccommodationDto { + private Long accommodationId; + private Long areaId; + private String name; + private String address; + private BigDecimal lat; + private BigDecimal lon; + private String phone; + private String sigunguCode; + private String imageUrl; +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AccommodationResponse.java b/place-service/src/main/java/com/example/placeservice/dto/AccommodationResponse.java new file mode 100644 index 0000000..1ba9a62 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AccommodationResponse.java @@ -0,0 +1,81 @@ +package com.example.placeservice.dto; + +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import lombok.Data; + +import java.util.List; + +@Data +@XmlRootElement(name = "response") +@XmlAccessorType(XmlAccessType.FIELD) +public class AccommodationResponse { + @XmlElement(name = "header") + private Header header; + + @XmlElement(name = "body") + private Body body; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class Header { + @XmlElement(name = "resultCode") + private String resultCode; + + @XmlElement(name = "resultMsg") + private String resultMsg; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class Body { + @XmlElement(name = "items") + private Items items; + + @XmlElement(name = "numOfRows") + private int numOfRows; + + @XmlElement(name = "pageNo") + private int pageNo; + + @XmlElement(name = "totalCount") + private int totalCount; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class Items { + @XmlElement(name = "item") + private List item; // 여기를 List으로 변경 + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class Item { + private String addr1; + private String addr2; + private String areacode; + private String benikia; + private String cat1; + private String cat2; + private String cat3; + private String contentid; + private String contenttypeid; + private String createdtime; + private String firstimage; + private String firstimage2; + private String cpyrhtDivCd; + private String goodstay; + private String hanok; + private String mapx; + private String mapy; + private String mlevel; + private String modifiedtime; + private String tel; + private String title; + private String booktour; + private String sigungucode; + } + } + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java new file mode 100644 index 0000000..0f4473e --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java @@ -0,0 +1,28 @@ +package com.example.placeservice.entity; + +import jakarta.persistence.*; + +import java.math.BigDecimal; + +@Entity +public class Accommodation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long accommodation_id; + + @ManyToOne + @JoinColumn(name = "area_id", referencedColumnName = "area_id") + private Area area; + + private Long content_id; + private String category; + private String name; + private String address; + @Column(precision = 10, scale = 6) + private BigDecimal lat; + @Column(precision = 10, scale = 6) + private BigDecimal lon; + private String phone; + private String sigungu_code; + private String image_url; +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index 6c14791..c768b68 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -1,10 +1,12 @@ package com.example.placeservice.entity; import jakarta.persistence.*; +import lombok.Getter; import java.math.BigDecimal; @Entity +@Getter public class Area { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -23,11 +25,9 @@ public class Area { @Column(name = "category", length = 20, nullable = false) private String category; // 구분 - @Column(name = "x", precision = 10, scale = 6, nullable = false) - private BigDecimal x; // 위도 - - @Column(name = "y", precision = 10, scale = 6, nullable = false) - private BigDecimal y; // 경도 - + @Column(name = "lat", precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 + @Column(name = "lon", precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 } diff --git a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java new file mode 100644 index 0000000..3b73765 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java @@ -0,0 +1,7 @@ +package com.example.placeservice.repository; + +import com.example.placeservice.entity.Accommodation; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AccommodationRepository extends JpaRepository { +} diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationParseService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationParseService.java new file mode 100644 index 0000000..ecb19b6 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationParseService.java @@ -0,0 +1,60 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.AccommodationResponse; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Unmarshaller; +import org.springframework.stereotype.Service; + +import jakarta.xml.bind.JAXBContext; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +@Service +public class AccommodationParseService { + public List parseXmlData(String xmlData) { + if (xmlData == null || xmlData.trim().isEmpty()) { + throw new IllegalArgumentException("XML 데이터가 비어있습니다."); + } + + try { + // JAXB 컨텍스트 생성 + JAXBContext jaxbContext = JAXBContext.newInstance(AccommodationResponse.class); + + // Unmarshaller 생성 + Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + + // XML 문자열을 Java 객체로 변환 + StringReader reader = new StringReader(xmlData); + AccommodationResponse accommodationResponse = (AccommodationResponse) unmarshaller.unmarshal(reader); + + // null 체크 추가 + if (accommodationResponse == null || + accommodationResponse.getBody() == null || + accommodationResponse.getBody().getItems() == null || + accommodationResponse.getBody().getItems().getItem() == null) { + return new ArrayList<>(); // 빈 리스트 반환 + } + + // 이제 getItem()은 List을 반환하므로 바로 사용 가능 + return accommodationResponse.getBody().getItems().getItem(); + + } catch (JAXBException e) { + throw new RuntimeException("XML 파싱 중 오류 발생: " + e.getMessage(), e); + } catch (Exception e) { + throw new RuntimeException("예상치 못한 오류 발생: " + e.getMessage(), e); + } + } + + public void SaveXmlData(String xmlData) { + List items = parseXmlData(xmlData); + + // 각 아이템 정보 출력 + for (AccommodationResponse.Body.Items.Item item : items) { + System.out.println("호텔명: " + item.getTitle()); + System.out.println("주소: " + item.getAddr1() + " " + item.getAddr2()); + System.out.println("전화번호: " + item.getTel()); + System.out.println("---------------------------"); + } + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationSaveService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationSaveService.java new file mode 100644 index 0000000..c12540e --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationSaveService.java @@ -0,0 +1,161 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.AccommodationResponse; +import com.example.placeservice.entity.Area; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; + +@Service +public class AccommodationSaveService { + private final JdbcTemplate jdbcTemplate; + private final EntityManager entityManager; + + public AccommodationSaveService(JdbcTemplate jdbcTemplate, EntityManager entityManager) { + this.jdbcTemplate = jdbcTemplate; + this.entityManager = entityManager; + } + + @Transactional + public void saveAccommodation(List items) { + String sql = "INSERT INTO accommodation (area_id, content_id, category, name, address, lat, lon, phone, sigungu_code, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + List allAreas = entityManager.createQuery("SELECT a FROM Area a", Area.class).getResultList(); + + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + AccommodationResponse.Body.Items.Item item = items.get(i); + + Long areaId = findClosestAreaInMemory(new BigDecimal(item.getMapy()), new BigDecimal(item.getMapx()), allAreas); + ps.setObject(1, areaId); + + // content_id + try { + ps.setLong(2, Long.parseLong(item.getContentid())); + } catch (NumberFormatException e) { + ps.setNull(2, Types.BIGINT); + } + + // category + // 소분류 코드 + // 0100 : 관광호텔 / 0500 : 콘도미니엄 / 0600 : 유스호스텔 / 0700 : 펜션 / 0900 : 모텔 / 1100 : 게하 / 1600 : 한옥 + try { + switch (item.getCat3().substring(5)) { + case "0100": + ps.setString(3, "관광호텔"); break; + case "0500": + ps.setString(3, "콘도미니엄"); break; + case "0600": + ps.setString(3, "유스호스텔"); break; + case "0700": + ps.setString(3, "펜션"); break; + case "0900": + ps.setString(3, "모텔"); break; + case "1100": + ps.setString(3, "게스트하우스"); break; + case "1600": + ps.setString(3, "한옥"); break; + default: + ps.setString(3, "구분없음"); break; + } + } catch (NumberFormatException e) { + ps.setNull(3, Types.VARCHAR); + } + + ps.setString(4, item.getTitle()); + ps.setString(5, item.getAddr1()); + + // lat + try { + if (item.getMapy() != null && !item.getMapy().isEmpty()) { + ps.setBigDecimal(6, new BigDecimal(item.getMapy())); + } else { + ps.setNull(6, Types.NUMERIC); + } + } catch (NumberFormatException e) { + ps.setNull(6, Types.NUMERIC); + } + + // lon + try { + if (item.getMapx() != null && !item.getMapx().isEmpty()) { + ps.setBigDecimal(7, new BigDecimal(item.getMapx())); + } else { + ps.setNull(7, Types.NUMERIC); + } + } catch (NumberFormatException e) { + ps.setNull(7, Types.NUMERIC); + } + + ps.setString(8, item.getTel()); + ps.setString(9, item.getSigungucode()); + + if (item.getFirstimage() != null && !item.getFirstimage().isEmpty()) { + ps.setString(10, item.getFirstimage()); + } else { + ps.setNull(10, Types.VARCHAR); + } + } + + @Override + public int getBatchSize() { + return items.size(); + } + }); + } + + private Long findClosestAreaInMemory(BigDecimal lat, BigDecimal lon, List areas) { + double minDistance = 8000.0; + Long closestAreaId = null; + int count = 0; + + for (Area area : areas) { + if (area.getLat() == null || area.getLon() == null) { + System.out.println("지역 좌표 오류 : 지역번호 " + area.getArea_id()); + continue; + } + double distance = calculateHaversineDistance( + lat.doubleValue(), lon.doubleValue(), + area.getLat().doubleValue(), area.getLon().doubleValue() + ); + if (Double.isNaN(distance)) { + System.out.println("거리가 숫자가 아닙니다 : 지역번호 " + area.getArea_id()); + continue; + } + if (distance < minDistance) { + minDistance = distance; + closestAreaId = area.getArea_id(); + } + } + + return closestAreaId; + } + + private double calculateHaversineDistance(double lat1, double lon1, double lat2, double lon2) { + // 지구 반지름 (km) + final double R = 6371.0; + + // 라디안으로 변환 + double latDistance = Math.toRadians(lat2 - lat1); + double lonDistance = Math.toRadians(lon2 - lon1); + + // 하버사인 공식 + double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) + * Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2); + + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + // 거리 계산 (km) + return R * c; + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java new file mode 100644 index 0000000..859181c --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java @@ -0,0 +1,21 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.AccommodationResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class AccommodationService { + private AccommodationParseService accommodationParseService = new AccommodationParseService(); + + @Autowired + public AccommodationService(AccommodationParseService accommodationParseService) { + this.accommodationParseService = accommodationParseService; + } + + public List getAccommodationData(String xmlData) { + return accommodationParseService.parseXmlData(xmlData); + } +} diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index 5ad3979..4eb8df6 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -1,4 +1,4 @@ -INSERT INTO area (seoul_id, name, name_eng, category, x, y) VALUES +INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), From 25ffc6942633da668080e9c0b1df078dacb1ec82 Mon Sep 17 00:00:00 2001 From: GLENSIS Date: Wed, 23 Apr 2025 10:52:05 +0900 Subject: [PATCH 072/361] =?UTF-8?q?=ED=96=89=EC=82=AC=20db,=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/RestTemplateConfig.java | 14 ++++ .../controller/CulturalEventController.java | 26 ++++++++ .../placeservice/dto/CulturalEventInfo.java | 16 +++++ .../placeservice/dto/CulturalEventItem.java | 59 +++++++++++++++++ .../dto/CulturalEventResponse.java | 10 +++ .../com/example/placeservice/dto/Result.java | 9 +++ .../placeservice/entity/CulturalEvent.java | 57 ++++++++++++++++ .../example/placeservice/entity/GuArea.java | 21 ++++++ .../external/CulturalEventClient.java | 20 ++++++ .../init/EventDataInitializer.java | 19 ++++++ .../repository/CulturalEventRepository.java | 14 ++++ .../repository/GuAreaRepository.java | 10 +++ .../service/CulturalEventService.java | 50 ++++++++++++++ .../service/EventParserService.java | 66 +++++++++++++++++++ .../placeservice/service/GuAreaService.java | 22 +++++++ .../src/main/resources/application.properties | 2 +- 16 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 place-service/src/main/java/com/example/placeservice/config/RestTemplateConfig.java create mode 100644 place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/CulturalEventInfo.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/CulturalEventItem.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/CulturalEventResponse.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/Result.java create mode 100644 place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java create mode 100644 place-service/src/main/java/com/example/placeservice/entity/GuArea.java create mode 100644 place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java create mode 100644 place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/GuAreaRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/EventParserService.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/GuAreaService.java diff --git a/place-service/src/main/java/com/example/placeservice/config/RestTemplateConfig.java b/place-service/src/main/java/com/example/placeservice/config/RestTemplateConfig.java new file mode 100644 index 0000000..ac48671 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package com.example.placeservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java b/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java new file mode 100644 index 0000000..76fd796 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java @@ -0,0 +1,26 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.entity.CulturalEvent; +import com.example.placeservice.repository.CulturalEventRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/main") +@RequiredArgsConstructor +@Slf4j +public class CulturalEventController { + + private final CulturalEventRepository culturalEventRepository; + + // /main/events: 저장된 모든 이벤트 조회 + @GetMapping("/events") + public List getAllEvents() { + return culturalEventRepository.findAll(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/CulturalEventInfo.java b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventInfo.java new file mode 100644 index 0000000..ad6d466 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventInfo.java @@ -0,0 +1,16 @@ +package com.example.placeservice.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class CulturalEventInfo { + private int list_total_count; + private Result RESULT; + @JsonProperty("row") + private List row; +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/CulturalEventItem.java b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventItem.java new file mode 100644 index 0000000..236accf --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventItem.java @@ -0,0 +1,59 @@ +package com.example.placeservice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +public class CulturalEventItem { + + @JsonProperty("CODENAME") + private String codename; + + @JsonProperty("GUNAME") + private String guname; + + @JsonProperty("TITLE") + private String title; + + @JsonProperty("PLACE") + private String place; + + @JsonProperty("ORG_NAME") + private String orgName; + + @JsonProperty("USE_TRGT") + private String useTrgt; + + @JsonProperty("USE_FEE") + private String useFee; + + @JsonProperty("ORG_LINK") + private String orgLink; + + @JsonProperty("MAIN_IMG") + private String mainImg; + + @JsonProperty("STRTDATE") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.S") // 날짜 포맷 설정 + private LocalDateTime strtdate; + + @JsonProperty("END_DATE") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.S") // 날짜 포맷 설정 + private LocalDateTime endDate; + + @JsonProperty("LOT") + private BigDecimal lot; + + @JsonProperty("LAT") + private BigDecimal lat; + + @JsonProperty("IS_FREE") + private String isFree; + + @JsonProperty("HMPG_ADDR") + private String hmpgAddr; +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/CulturalEventResponse.java b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventResponse.java new file mode 100644 index 0000000..cbe9153 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventResponse.java @@ -0,0 +1,10 @@ +package com.example.placeservice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class CulturalEventResponse { + @JsonProperty("culturalEventInfo") + private CulturalEventInfo culturalEventInfo; +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/Result.java b/place-service/src/main/java/com/example/placeservice/dto/Result.java new file mode 100644 index 0000000..efd696a --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/Result.java @@ -0,0 +1,9 @@ +package com.example.placeservice.dto; + +import lombok.Data; + +@Data +public class Result { + private String CODE; + private String MESSAGE; +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java new file mode 100644 index 0000000..0928754 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java @@ -0,0 +1,57 @@ +package com.example.placeservice.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class CulturalEvent { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "event_id") + private Long eventId; + + @JsonIgnore // GuArea 필드를 직렬화에서 제외 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "area_id") + private GuArea area; + + @Column(name = "category", length = 200) + private String category; + + @Column(name = "title", length = 50) + private String title; + + @Column(name = "address", length = 50) + private String address; // PLACE → address + + @Column(name = "lat", precision = 10, scale = 6) + private BigDecimal lat; + + @Column(name = "lon", precision = 10, scale = 6) + private BigDecimal lon; + + @Column(name = "target", length = 100) + private String target; + + @Column(name = "event_fee", columnDefinition = "TEXT") + private String eventFee; + + @Column(name = "event_img", length = 200) + private String eventImg; + + @Column(name = "start_date") + private LocalDateTime startDate; + + @Column(name = "end_date") + private LocalDateTime endDate; +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/GuArea.java b/place-service/src/main/java/com/example/placeservice/entity/GuArea.java new file mode 100644 index 0000000..7fc1516 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/GuArea.java @@ -0,0 +1,21 @@ +package com.example.placeservice.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class GuArea { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "area_id") + private Long areaId; + + @Column(name = "gu_name", unique = true) + private String guName; +} diff --git a/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java b/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java new file mode 100644 index 0000000..027dc4e --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java @@ -0,0 +1,20 @@ +package com.example.placeservice.external; + +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class CulturalEventClient { + + private final RestTemplate restTemplate; + + public CulturalEventClient(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + // API에서 1000개씩 데이터를 받아오는 메서드 + public String fetchEventData(int startIndex, int endIndex) { + String url = String.format("http://openapi.seoul.go.kr:8088/7669764c417069613736734567476c/json/culturalEventInfo/%d/%d/", startIndex, endIndex); + return restTemplate.getForObject(url, String.class); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java b/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java new file mode 100644 index 0000000..f6dd9d9 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java @@ -0,0 +1,19 @@ +package com.example.placeservice.init; + +import com.example.placeservice.service.CulturalEventService; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class EventDataInitializer implements CommandLineRunner { + + private final CulturalEventService culturalEventService; + + @Override + public void run(String... args) { + // 애플리케이션 시작 시 데이터를 자동으로 받아와 저장 + culturalEventService.fetchAndSaveAllEvents(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java new file mode 100644 index 0000000..f238de4 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java @@ -0,0 +1,14 @@ +// CulturalEventRepository.java +package com.example.placeservice.repository; + +import com.example.placeservice.entity.CulturalEvent; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; + +@Repository +public interface CulturalEventRepository extends JpaRepository { + boolean existsByTitleAndAddressAndStartDate(String title, String address, LocalDateTime startDate); + +} diff --git a/place-service/src/main/java/com/example/placeservice/repository/GuAreaRepository.java b/place-service/src/main/java/com/example/placeservice/repository/GuAreaRepository.java new file mode 100644 index 0000000..32de982 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/GuAreaRepository.java @@ -0,0 +1,10 @@ +package com.example.placeservice.repository; + +import com.example.placeservice.entity.GuArea; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface GuAreaRepository extends JpaRepository { + Optional findByGuName(String guName); +} diff --git a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java new file mode 100644 index 0000000..ce27cc1 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java @@ -0,0 +1,50 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.CulturalEventItem; +import com.example.placeservice.entity.CulturalEvent; +import com.example.placeservice.external.CulturalEventClient; +import com.example.placeservice.repository.CulturalEventRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CulturalEventService { + + private final CulturalEventClient culturalEventClient; + private final EventParserService eventParserService; + private final CulturalEventRepository culturalEventRepository; + + // API에서 데이터를 1000개씩 받아와서 저장하는 메서드 + @Transactional + public void fetchAndSaveAllEvents() { + int[] ranges = {1, 1001, 2001, 3001, 4001, 5001}; // 각 범위의 시작 인덱스 + for (int i = 0; i < ranges.length; i++) { + int startIndex = ranges[i]; + int endIndex = startIndex+999; // endIndex는 1000개씩 데이터를 가져오므로 1000개씩 증가 + + // API에서 데이터 가져오기 + String jsonData = culturalEventClient.fetchEventData(startIndex, endIndex); + List eventItems = eventParserService.parse(jsonData); + + // 데이터가 없다면 더 이상 가져올 데이터가 없다는 의미 + if (!eventItems.isEmpty()) { + // 중복 제거하고 저장 + saveEvents(eventItems); + } + } + } + + private void saveEvents(List eventItems) { + List events = eventParserService.toEntityList(eventItems); + for (CulturalEvent event : events) { + // 중복 처리 로직 + if (!culturalEventRepository.existsByTitleAndAddressAndStartDate(event.getTitle(), event.getAddress(), event.getStartDate())) { + culturalEventRepository.save(event); + } + } + } +} diff --git a/place-service/src/main/java/com/example/placeservice/service/EventParserService.java b/place-service/src/main/java/com/example/placeservice/service/EventParserService.java new file mode 100644 index 0000000..9f91b92 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/EventParserService.java @@ -0,0 +1,66 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.CulturalEventItem; +import com.example.placeservice.dto.CulturalEventResponse; +import com.example.placeservice.entity.CulturalEvent; +import com.example.placeservice.entity.GuArea; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor // final 필드에 대한 생성자 주입 +public class EventParserService { + + private final ObjectMapper objectMapper; + private final GuAreaService guAreaService; // 지역 정보를 가져오는 서비스 + + public List parse(String jsonResponse) { + try { + CulturalEventResponse response = objectMapper.readValue(jsonResponse, CulturalEventResponse.class); + return response.getCulturalEventInfo().getRow(); + } catch (Exception e) { + log.error("파싱 중 오류 발생: {}", e.getMessage()); + return List.of(); // 빈 리스트 반환 + } + } + + public CulturalEvent toEntity(CulturalEventItem item) { + CulturalEvent event = new CulturalEvent(); + + // 1. 지역 매핑 + GuArea area = guAreaService.findOrCreateArea(item.getGuname()); + event.setArea(area); + + // 2. 필드 매핑 (컬럼명에 맞춰 정확히 매핑) + event.setCategory(item.getCodename()); // category + event.setTitle(item.getTitle()); // title + event.setAddress(item.getPlace()); // address + event.setLat(item.getLat()); // lat + event.setLon(item.getLot()); // lon + event.setTarget(item.getUseTrgt()); // target + event.setEventFee(item.getUseFee()); // event_fee + event.setStartDate(item.getStrtdate()); // start_date + event.setEndDate(item.getEndDate()); // end_date + + // 3. 이미지 URL 정리 + String imageUrl = item.getMainImg(); + if (imageUrl != null) { + imageUrl = imageUrl.replace("https://culture.seoul.go.ko?", + "https://culture.seoul.go.kr/cmmn/file/getImage.do?"); + } + event.setEventImg(imageUrl); // event_img + + return event; + } + + + public List toEntityList(List items) { + return items.stream() + .map(this::toEntity) + .toList(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/service/GuAreaService.java b/place-service/src/main/java/com/example/placeservice/service/GuAreaService.java new file mode 100644 index 0000000..adb3642 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/GuAreaService.java @@ -0,0 +1,22 @@ +package com.example.placeservice.service; + +import com.example.placeservice.entity.GuArea; +import com.example.placeservice.repository.GuAreaRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class GuAreaService { + + private final GuAreaRepository guAreaRepository; + + public GuArea findOrCreateArea(String guName) { + return guAreaRepository.findByGuName(guName) + .orElseGet(() -> { + GuArea newArea = new GuArea(); + newArea.setGuName(guName); + return guAreaRepository.save(newArea); + }); + } +} diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 02c04c8..80447d7 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -7,7 +7,7 @@ spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update +spring.jpa.hibernate.ddl-auto=none spring.jpa.defer-datasource-initialization=true # Hibernate SQL ?? ??? From 5adb343a2650813314252f2d71dc4daf13f8def0 Mon Sep 17 00:00:00 2001 From: GLENSIS Date: Wed, 23 Apr 2025 10:52:05 +0900 Subject: [PATCH 073/361] =?UTF-8?q?=ED=96=89=EC=82=AC=20db,=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/RestTemplateConfig.java | 14 ++++ .../controller/CulturalEventController.java | 26 ++++++++ .../placeservice/dto/CulturalEventInfo.java | 16 +++++ .../placeservice/dto/CulturalEventItem.java | 59 +++++++++++++++++ .../dto/CulturalEventResponse.java | 10 +++ .../com/example/placeservice/dto/Result.java | 9 +++ .../placeservice/entity/CulturalEvent.java | 57 ++++++++++++++++ .../example/placeservice/entity/GuArea.java | 21 ++++++ .../external/CulturalEventClient.java | 20 ++++++ .../init/EventDataInitializer.java | 19 ++++++ .../repository/CulturalEventRepository.java | 14 ++++ .../repository/GuAreaRepository.java | 10 +++ .../service/CulturalEventService.java | 50 ++++++++++++++ .../service/EventParserService.java | 66 +++++++++++++++++++ .../placeservice/service/GuAreaService.java | 22 +++++++ .../src/main/resources/application.properties | 2 +- 16 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 place-service/src/main/java/com/example/placeservice/config/RestTemplateConfig.java create mode 100644 place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/CulturalEventInfo.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/CulturalEventItem.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/CulturalEventResponse.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/Result.java create mode 100644 place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java create mode 100644 place-service/src/main/java/com/example/placeservice/entity/GuArea.java create mode 100644 place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java create mode 100644 place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/GuAreaRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/EventParserService.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/GuAreaService.java diff --git a/place-service/src/main/java/com/example/placeservice/config/RestTemplateConfig.java b/place-service/src/main/java/com/example/placeservice/config/RestTemplateConfig.java new file mode 100644 index 0000000..ac48671 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package com.example.placeservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java b/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java new file mode 100644 index 0000000..76fd796 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java @@ -0,0 +1,26 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.entity.CulturalEvent; +import com.example.placeservice.repository.CulturalEventRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/main") +@RequiredArgsConstructor +@Slf4j +public class CulturalEventController { + + private final CulturalEventRepository culturalEventRepository; + + // /main/events: 저장된 모든 이벤트 조회 + @GetMapping("/events") + public List getAllEvents() { + return culturalEventRepository.findAll(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/CulturalEventInfo.java b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventInfo.java new file mode 100644 index 0000000..ad6d466 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventInfo.java @@ -0,0 +1,16 @@ +package com.example.placeservice.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class CulturalEventInfo { + private int list_total_count; + private Result RESULT; + @JsonProperty("row") + private List row; +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/CulturalEventItem.java b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventItem.java new file mode 100644 index 0000000..236accf --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventItem.java @@ -0,0 +1,59 @@ +package com.example.placeservice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +public class CulturalEventItem { + + @JsonProperty("CODENAME") + private String codename; + + @JsonProperty("GUNAME") + private String guname; + + @JsonProperty("TITLE") + private String title; + + @JsonProperty("PLACE") + private String place; + + @JsonProperty("ORG_NAME") + private String orgName; + + @JsonProperty("USE_TRGT") + private String useTrgt; + + @JsonProperty("USE_FEE") + private String useFee; + + @JsonProperty("ORG_LINK") + private String orgLink; + + @JsonProperty("MAIN_IMG") + private String mainImg; + + @JsonProperty("STRTDATE") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.S") // 날짜 포맷 설정 + private LocalDateTime strtdate; + + @JsonProperty("END_DATE") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.S") // 날짜 포맷 설정 + private LocalDateTime endDate; + + @JsonProperty("LOT") + private BigDecimal lot; + + @JsonProperty("LAT") + private BigDecimal lat; + + @JsonProperty("IS_FREE") + private String isFree; + + @JsonProperty("HMPG_ADDR") + private String hmpgAddr; +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/CulturalEventResponse.java b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventResponse.java new file mode 100644 index 0000000..cbe9153 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventResponse.java @@ -0,0 +1,10 @@ +package com.example.placeservice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class CulturalEventResponse { + @JsonProperty("culturalEventInfo") + private CulturalEventInfo culturalEventInfo; +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/Result.java b/place-service/src/main/java/com/example/placeservice/dto/Result.java new file mode 100644 index 0000000..efd696a --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/Result.java @@ -0,0 +1,9 @@ +package com.example.placeservice.dto; + +import lombok.Data; + +@Data +public class Result { + private String CODE; + private String MESSAGE; +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java new file mode 100644 index 0000000..0928754 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java @@ -0,0 +1,57 @@ +package com.example.placeservice.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class CulturalEvent { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "event_id") + private Long eventId; + + @JsonIgnore // GuArea 필드를 직렬화에서 제외 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "area_id") + private GuArea area; + + @Column(name = "category", length = 200) + private String category; + + @Column(name = "title", length = 50) + private String title; + + @Column(name = "address", length = 50) + private String address; // PLACE → address + + @Column(name = "lat", precision = 10, scale = 6) + private BigDecimal lat; + + @Column(name = "lon", precision = 10, scale = 6) + private BigDecimal lon; + + @Column(name = "target", length = 100) + private String target; + + @Column(name = "event_fee", columnDefinition = "TEXT") + private String eventFee; + + @Column(name = "event_img", length = 200) + private String eventImg; + + @Column(name = "start_date") + private LocalDateTime startDate; + + @Column(name = "end_date") + private LocalDateTime endDate; +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/GuArea.java b/place-service/src/main/java/com/example/placeservice/entity/GuArea.java new file mode 100644 index 0000000..7fc1516 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/GuArea.java @@ -0,0 +1,21 @@ +package com.example.placeservice.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class GuArea { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "area_id") + private Long areaId; + + @Column(name = "gu_name", unique = true) + private String guName; +} diff --git a/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java b/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java new file mode 100644 index 0000000..027dc4e --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java @@ -0,0 +1,20 @@ +package com.example.placeservice.external; + +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class CulturalEventClient { + + private final RestTemplate restTemplate; + + public CulturalEventClient(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + // API에서 1000개씩 데이터를 받아오는 메서드 + public String fetchEventData(int startIndex, int endIndex) { + String url = String.format("http://openapi.seoul.go.kr:8088/7669764c417069613736734567476c/json/culturalEventInfo/%d/%d/", startIndex, endIndex); + return restTemplate.getForObject(url, String.class); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java b/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java new file mode 100644 index 0000000..f6dd9d9 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java @@ -0,0 +1,19 @@ +package com.example.placeservice.init; + +import com.example.placeservice.service.CulturalEventService; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class EventDataInitializer implements CommandLineRunner { + + private final CulturalEventService culturalEventService; + + @Override + public void run(String... args) { + // 애플리케이션 시작 시 데이터를 자동으로 받아와 저장 + culturalEventService.fetchAndSaveAllEvents(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java new file mode 100644 index 0000000..f238de4 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java @@ -0,0 +1,14 @@ +// CulturalEventRepository.java +package com.example.placeservice.repository; + +import com.example.placeservice.entity.CulturalEvent; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; + +@Repository +public interface CulturalEventRepository extends JpaRepository { + boolean existsByTitleAndAddressAndStartDate(String title, String address, LocalDateTime startDate); + +} diff --git a/place-service/src/main/java/com/example/placeservice/repository/GuAreaRepository.java b/place-service/src/main/java/com/example/placeservice/repository/GuAreaRepository.java new file mode 100644 index 0000000..32de982 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/GuAreaRepository.java @@ -0,0 +1,10 @@ +package com.example.placeservice.repository; + +import com.example.placeservice.entity.GuArea; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface GuAreaRepository extends JpaRepository { + Optional findByGuName(String guName); +} diff --git a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java new file mode 100644 index 0000000..ce27cc1 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java @@ -0,0 +1,50 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.CulturalEventItem; +import com.example.placeservice.entity.CulturalEvent; +import com.example.placeservice.external.CulturalEventClient; +import com.example.placeservice.repository.CulturalEventRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CulturalEventService { + + private final CulturalEventClient culturalEventClient; + private final EventParserService eventParserService; + private final CulturalEventRepository culturalEventRepository; + + // API에서 데이터를 1000개씩 받아와서 저장하는 메서드 + @Transactional + public void fetchAndSaveAllEvents() { + int[] ranges = {1, 1001, 2001, 3001, 4001, 5001}; // 각 범위의 시작 인덱스 + for (int i = 0; i < ranges.length; i++) { + int startIndex = ranges[i]; + int endIndex = startIndex+999; // endIndex는 1000개씩 데이터를 가져오므로 1000개씩 증가 + + // API에서 데이터 가져오기 + String jsonData = culturalEventClient.fetchEventData(startIndex, endIndex); + List eventItems = eventParserService.parse(jsonData); + + // 데이터가 없다면 더 이상 가져올 데이터가 없다는 의미 + if (!eventItems.isEmpty()) { + // 중복 제거하고 저장 + saveEvents(eventItems); + } + } + } + + private void saveEvents(List eventItems) { + List events = eventParserService.toEntityList(eventItems); + for (CulturalEvent event : events) { + // 중복 처리 로직 + if (!culturalEventRepository.existsByTitleAndAddressAndStartDate(event.getTitle(), event.getAddress(), event.getStartDate())) { + culturalEventRepository.save(event); + } + } + } +} diff --git a/place-service/src/main/java/com/example/placeservice/service/EventParserService.java b/place-service/src/main/java/com/example/placeservice/service/EventParserService.java new file mode 100644 index 0000000..9f91b92 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/EventParserService.java @@ -0,0 +1,66 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.CulturalEventItem; +import com.example.placeservice.dto.CulturalEventResponse; +import com.example.placeservice.entity.CulturalEvent; +import com.example.placeservice.entity.GuArea; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor // final 필드에 대한 생성자 주입 +public class EventParserService { + + private final ObjectMapper objectMapper; + private final GuAreaService guAreaService; // 지역 정보를 가져오는 서비스 + + public List parse(String jsonResponse) { + try { + CulturalEventResponse response = objectMapper.readValue(jsonResponse, CulturalEventResponse.class); + return response.getCulturalEventInfo().getRow(); + } catch (Exception e) { + log.error("파싱 중 오류 발생: {}", e.getMessage()); + return List.of(); // 빈 리스트 반환 + } + } + + public CulturalEvent toEntity(CulturalEventItem item) { + CulturalEvent event = new CulturalEvent(); + + // 1. 지역 매핑 + GuArea area = guAreaService.findOrCreateArea(item.getGuname()); + event.setArea(area); + + // 2. 필드 매핑 (컬럼명에 맞춰 정확히 매핑) + event.setCategory(item.getCodename()); // category + event.setTitle(item.getTitle()); // title + event.setAddress(item.getPlace()); // address + event.setLat(item.getLat()); // lat + event.setLon(item.getLot()); // lon + event.setTarget(item.getUseTrgt()); // target + event.setEventFee(item.getUseFee()); // event_fee + event.setStartDate(item.getStrtdate()); // start_date + event.setEndDate(item.getEndDate()); // end_date + + // 3. 이미지 URL 정리 + String imageUrl = item.getMainImg(); + if (imageUrl != null) { + imageUrl = imageUrl.replace("https://culture.seoul.go.ko?", + "https://culture.seoul.go.kr/cmmn/file/getImage.do?"); + } + event.setEventImg(imageUrl); // event_img + + return event; + } + + + public List toEntityList(List items) { + return items.stream() + .map(this::toEntity) + .toList(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/service/GuAreaService.java b/place-service/src/main/java/com/example/placeservice/service/GuAreaService.java new file mode 100644 index 0000000..adb3642 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/GuAreaService.java @@ -0,0 +1,22 @@ +package com.example.placeservice.service; + +import com.example.placeservice.entity.GuArea; +import com.example.placeservice.repository.GuAreaRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class GuAreaService { + + private final GuAreaRepository guAreaRepository; + + public GuArea findOrCreateArea(String guName) { + return guAreaRepository.findByGuName(guName) + .orElseGet(() -> { + GuArea newArea = new GuArea(); + newArea.setGuName(guName); + return guAreaRepository.save(newArea); + }); + } +} diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 02c04c8..80447d7 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -7,7 +7,7 @@ spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update +spring.jpa.hibernate.ddl-auto=none spring.jpa.defer-datasource-initialization=true # Hibernate SQL ?? ??? From a8ae8adbb7ae550e1ef1f76a61eabb3dbd6fb0fd Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Wed, 23 Apr 2025 11:13:13 +0900 Subject: [PATCH 074/361] =?UTF-8?q?Feat:=20lat=20lon=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/example/placeservice/entity/Area.java | 4 ++-- .../java/com/example/placeservice/service/CafeService.java | 4 ++-- place-service/src/main/resources/data.sql | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index 8292532..24d9a81 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -38,11 +38,11 @@ public Long getAreaId() { return this.area_id; } - public Number getY() { + public Number getX() { return this.lon; } - public Number getX() { + public Number getY() { return this.lat; } } diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java index ef4c974..0091c84 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CafeService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java @@ -88,8 +88,8 @@ public void processSingleArea(Area area) { Cafe cafe = Cafe.builder() .name(cafeDoc.getPlaceName()) .address(cafeDoc.getAddressName()) - .lat(new BigDecimal(cafeDoc.getX())) - .lon(new BigDecimal(cafeDoc.getY())) + .lat(new BigDecimal(cafeDoc.getY())) + .lon(new BigDecimal(cafeDoc.getX())) .phone(cafeDoc.getPhone()) .kakaomapUrl(cafeDoc.getPlaceUrl()) .categoryCode(cafeDoc.getCategoryGroupCode()) diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index 6ca7d5d..4eb8df6 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -1,4 +1,4 @@ -INSERT INTO area (seoul_id, name, name_eng, category, lon, lat) VALUES +INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), From 120cd959a8174b2e689d52dd8fe6aebdf5d2b484 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Wed, 23 Apr 2025 11:13:13 +0900 Subject: [PATCH 075/361] =?UTF-8?q?Feat:=20lat=20lon=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/example/placeservice/entity/Area.java | 4 ++-- .../java/com/example/placeservice/service/CafeService.java | 4 ++-- place-service/src/main/resources/data.sql | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index 8292532..24d9a81 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -38,11 +38,11 @@ public Long getAreaId() { return this.area_id; } - public Number getY() { + public Number getX() { return this.lon; } - public Number getX() { + public Number getY() { return this.lat; } } diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java index ef4c974..0091c84 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CafeService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java @@ -88,8 +88,8 @@ public void processSingleArea(Area area) { Cafe cafe = Cafe.builder() .name(cafeDoc.getPlaceName()) .address(cafeDoc.getAddressName()) - .lat(new BigDecimal(cafeDoc.getX())) - .lon(new BigDecimal(cafeDoc.getY())) + .lat(new BigDecimal(cafeDoc.getY())) + .lon(new BigDecimal(cafeDoc.getX())) .phone(cafeDoc.getPhone()) .kakaomapUrl(cafeDoc.getPlaceUrl()) .categoryCode(cafeDoc.getCategoryGroupCode()) diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index 6ca7d5d..4eb8df6 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -1,4 +1,4 @@ -INSERT INTO area (seoul_id, name, name_eng, category, lon, lat) VALUES +INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), From 54e664dc4569ca44b5daba3f5e2f4725e325eb17 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 23 Apr 2025 12:58:00 +0900 Subject: [PATCH 076/361] =?UTF-8?q?fix:=20lat/lon=20=EB=B0=98=EB=8C=80?= =?UTF-8?q?=EB=A1=9C=20=EB=90=9C=20=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeservice/service/AttractionService.java | 10 +++++----- place-service/src/main/resources/data.sql | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index f4fa117..4fc8617 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -63,8 +63,8 @@ public List fetchDataFromVisitSeoul() { attraction.setSeoulAttractionId(table.getId()); attraction.setName(table.getTitle()); attraction.setAddress(table.getNewAddress()); - attraction.setLat(new BigDecimal(table.getMapX())); - attraction.setLon(new BigDecimal(table.getMapY())); + attraction.setLat(new BigDecimal(table.getMapY())); + attraction.setLon(new BigDecimal(table.getMapX())); attraction.setPhone(table.getTel()); attraction.setHomepageUrl(table.getHomepage()); attraction.setCloseDay(table.getCloseDay()); @@ -88,8 +88,8 @@ public List fetchDataFromVisitSeoul() { // 관광지 2km이내 area 계산 private Area findAreaByCondition(AttractionDto.AttractionTable table, List areaList) { - double lat = Double.parseDouble(table.getMapX()); - double lon = Double.parseDouble(table.getMapY()); + double lat = Double.parseDouble(table.getMapY()); + double lon = Double.parseDouble(table.getMapX()); for (Area area : areaList) { double distance = calculateDistanceKm( lat, lon, @@ -100,7 +100,7 @@ private Area findAreaByCondition(AttractionDto.AttractionTable table, List } } // 2km 이내에 없는 관광지 체크 - // System.out.println(table.getTitle()+"위도 :"+table.getMapX()+"경도 :"+table.getMapY() ); + // System.out.println(table.getTitle()+"경도:"+table.getMapX()+"위도 :"+table.getMapY() ); return null; } diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index 6ca7d5d..4eb8df6 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -1,4 +1,4 @@ -INSERT INTO area (seoul_id, name, name_eng, category, lon, lat) VALUES +INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), From 289fa37f296e699fe7c72d0968109d9104e4afb1 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 23 Apr 2025 12:58:00 +0900 Subject: [PATCH 077/361] =?UTF-8?q?fix:=20lat/lon=20=EB=B0=98=EB=8C=80?= =?UTF-8?q?=EB=A1=9C=20=EB=90=9C=20=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeservice/service/AttractionService.java | 10 +++++----- place-service/src/main/resources/data.sql | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index f4fa117..4fc8617 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -63,8 +63,8 @@ public List fetchDataFromVisitSeoul() { attraction.setSeoulAttractionId(table.getId()); attraction.setName(table.getTitle()); attraction.setAddress(table.getNewAddress()); - attraction.setLat(new BigDecimal(table.getMapX())); - attraction.setLon(new BigDecimal(table.getMapY())); + attraction.setLat(new BigDecimal(table.getMapY())); + attraction.setLon(new BigDecimal(table.getMapX())); attraction.setPhone(table.getTel()); attraction.setHomepageUrl(table.getHomepage()); attraction.setCloseDay(table.getCloseDay()); @@ -88,8 +88,8 @@ public List fetchDataFromVisitSeoul() { // 관광지 2km이내 area 계산 private Area findAreaByCondition(AttractionDto.AttractionTable table, List areaList) { - double lat = Double.parseDouble(table.getMapX()); - double lon = Double.parseDouble(table.getMapY()); + double lat = Double.parseDouble(table.getMapY()); + double lon = Double.parseDouble(table.getMapX()); for (Area area : areaList) { double distance = calculateDistanceKm( lat, lon, @@ -100,7 +100,7 @@ private Area findAreaByCondition(AttractionDto.AttractionTable table, List } } // 2km 이내에 없는 관광지 체크 - // System.out.println(table.getTitle()+"위도 :"+table.getMapX()+"경도 :"+table.getMapY() ); + // System.out.println(table.getTitle()+"경도:"+table.getMapX()+"위도 :"+table.getMapY() ); return null; } diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index 6ca7d5d..4eb8df6 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -1,4 +1,4 @@ -INSERT INTO area (seoul_id, name, name_eng, category, lon, lat) VALUES +INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), From 1035203961cba758926f211bd369c5dcba03ee60 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Wed, 23 Apr 2025 14:20:31 +0900 Subject: [PATCH 078/361] =?UTF-8?q?Feat:=20lat=20lon=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/placeservice/service/KakaoApiService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java index 7f028a2..6723c63 100644 --- a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java @@ -25,7 +25,7 @@ public class KakaoApiService { private final RestTemplate restTemplate; - @Value("${kakao.api.key}") + @Value("카카오API") private String kakaoApiKey; private static final String KAKAO_API_BASE_URL = "https://dapi.kakao.com/v2/local"; From 69bdaf788753647ef47c26128ae25e51d24d9238 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Wed, 23 Apr 2025 14:20:31 +0900 Subject: [PATCH 079/361] =?UTF-8?q?Feat:=20lat=20lon=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/placeservice/service/KakaoApiService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java index 7f028a2..6723c63 100644 --- a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java @@ -25,7 +25,7 @@ public class KakaoApiService { private final RestTemplate restTemplate; - @Value("${kakao.api.key}") + @Value("카카오API") private String kakaoApiKey; private static final String KAKAO_API_BASE_URL = "https://dapi.kakao.com/v2/local"; From ad8ee1ccaf20a4224d385d37b590300af53b053a Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Wed, 23 Apr 2025 14:21:20 +0900 Subject: [PATCH 080/361] =?UTF-8?q?Feat:=20lat=20lon=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/placeservice/service/KakaoApiService.java | 2 +- place-service/src/main/resources/application.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java index 6723c63..7f028a2 100644 --- a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java @@ -25,7 +25,7 @@ public class KakaoApiService { private final RestTemplate restTemplate; - @Value("카카오API") + @Value("${kakao.api.key}") private String kakaoApiKey; private static final String KAKAO_API_BASE_URL = "https://dapi.kakao.com/v2/local"; diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 439bd92..849a10e 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -14,7 +14,7 @@ spring.jpa.show-sql=true spring.sql.init.mode=always # ??? API ? ?? -kakao.api.key=585808366c299a6686e7104a193017bb +kakao.api.key=??? API ? # ?? ?? logging.level.com.example.placeservice=INFO \ No newline at end of file From 28747a0ed4a3e3a6cdcb6d6f66ae5819e7791d5f Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Wed, 23 Apr 2025 14:21:20 +0900 Subject: [PATCH 081/361] =?UTF-8?q?Feat:=20lat=20lon=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/placeservice/service/KakaoApiService.java | 2 +- place-service/src/main/resources/application.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java index 6723c63..7f028a2 100644 --- a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java @@ -25,7 +25,7 @@ public class KakaoApiService { private final RestTemplate restTemplate; - @Value("카카오API") + @Value("${kakao.api.key}") private String kakaoApiKey; private static final String KAKAO_API_BASE_URL = "https://dapi.kakao.com/v2/local"; diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 439bd92..849a10e 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -14,7 +14,7 @@ spring.jpa.show-sql=true spring.sql.init.mode=always # ??? API ? ?? -kakao.api.key=585808366c299a6686e7104a193017bb +kakao.api.key=??? API ? # ?? ?? logging.level.com.example.placeservice=INFO \ No newline at end of file From a053b0b72854ff9a69d4ce0050dc296cdf204280 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Wed, 23 Apr 2025 14:36:43 +0900 Subject: [PATCH 082/361] =?UTF-8?q?Feat:=20Build=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/placeservice/PlaceServiceApplication.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java index 9a066dc..e55d594 100644 --- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java +++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java @@ -1,5 +1,7 @@ package com.example.placeservice; +import com.example.placeservice.service.CafeService; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @@ -18,4 +20,12 @@ public static void main(String[] args) { public RestTemplate restTemplate() { return new RestTemplate(); } + + @Bean + public CommandLineRunner initData(CafeService cafeService) { + return args -> { + // 애플리케이션 시작 시 모든 장소의 주변 카페 데이터 처리 + cafeService.processAllAreas(); + }; + } } \ No newline at end of file From d5821aeeda065df913d91f899931ad3da2a217e2 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Wed, 23 Apr 2025 14:36:43 +0900 Subject: [PATCH 083/361] =?UTF-8?q?Feat:=20Build=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/placeservice/PlaceServiceApplication.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java index 9a066dc..e55d594 100644 --- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java +++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java @@ -1,5 +1,7 @@ package com.example.placeservice; +import com.example.placeservice.service.CafeService; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @@ -18,4 +20,12 @@ public static void main(String[] args) { public RestTemplate restTemplate() { return new RestTemplate(); } + + @Bean + public CommandLineRunner initData(CafeService cafeService) { + return args -> { + // 애플리케이션 시작 시 모든 장소의 주변 카페 데이터 처리 + cafeService.processAllAreas(); + }; + } } \ No newline at end of file From b21024b5b24c34d890950459f8c1faafe462c355 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Wed, 23 Apr 2025 14:38:42 +0900 Subject: [PATCH 084/361] =?UTF-8?q?Feat:=20build=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- place-service/src/main/resources/application.properties | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 849a10e..8eb7421 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -14,7 +14,6 @@ spring.jpa.show-sql=true spring.sql.init.mode=always # ??? API ? ?? -kakao.api.key=??? API ? - +kakao.api.key= kakao api key # ?? ?? logging.level.com.example.placeservice=INFO \ No newline at end of file From 7f649f743e5f9402bf86773baba9aa3f244ebd1f Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Wed, 23 Apr 2025 14:38:42 +0900 Subject: [PATCH 085/361] =?UTF-8?q?Feat:=20build=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- place-service/src/main/resources/application.properties | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 849a10e..8eb7421 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -14,7 +14,6 @@ spring.jpa.show-sql=true spring.sql.init.mode=always # ??? API ? ?? -kakao.api.key=??? API ? - +kakao.api.key= kakao api key # ?? ?? logging.level.com.example.placeservice=INFO \ No newline at end of file From a57291d7187d2cb4d1279b395f0903eb92b752e4 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 23 Apr 2025 14:41:14 +0900 Subject: [PATCH 086/361] =?UTF-8?q?Refactor:lat,lon=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/RestaurantDetailResponse.java | 2 +- .../placeservice/entity/Restaurant.java | 1 - .../repository/RestaurantRepository.java | 2 +- .../service/RestaurantService.java | 37 ++++++++++--------- place-service/src/main/resources/data.sql | 2 +- 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java b/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java index a7effcc..7840858 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java +++ b/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java @@ -46,4 +46,4 @@ public static RestaurantDetailResponse fromEntity(Restaurant restaurant) { .lon(restaurant.getLon().toString()) .build(); } -} +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java b/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java index 552bff8..8207738 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java @@ -65,4 +65,3 @@ public void setKakao_id(String kakao_id) { } - diff --git a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java index ba08e9f..b331b2d 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java @@ -5,4 +5,4 @@ public interface RestaurantRepository extends JpaRepository { // 기본 CRUD 사용 (findAll, findById, save 등) -} +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java index eb3c2fa..032a4f2 100644 --- a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java +++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java @@ -30,11 +30,12 @@ public List saveRestaurantsNearbyArea(Area area) { int savedCount = 0; // 저장한 음식점 수 while (!isEnd) { - JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLat(), area.getLon(), page); + // ✅ Kakao API 호출 시 경도(lon), 위도(lat) 순서로 전달 + JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLon(), area.getLat(), page); if (response.has("documents")) { for (JsonNode doc : response.get("documents")) { - if (savedCount >= 15) { // 15개 저장했으면 바로 종료 + if (savedCount >= 15) { // 15개 저장했으면 종료 isEnd = true; break; } @@ -43,8 +44,10 @@ public List saveRestaurantsNearbyArea(Area area) { double restaurantLat = Double.parseDouble(doc.get("y").asText()); double distance = calculateDistance( - area.getLon().doubleValue(), area.getLat().doubleValue(), - restaurantLat, restaurantLon + area.getLat().doubleValue(), // 위도 + area.getLon().doubleValue(), // 경도 + restaurantLat, // 위도 + restaurantLon // 경도 ); // 1km 이내 음식점만 저장 @@ -53,19 +56,18 @@ public List saveRestaurantsNearbyArea(Area area) { restaurant.setArea(area); restaurant.setName(doc.get("place_name").asText()); restaurant.setAddress(doc.get("road_address_name").asText()); - restaurant.setLat(new BigDecimal(doc.get("y").asText())); // Kakao API에서 y: lat - restaurant.setLon(new BigDecimal(doc.get("x").asText())); // Kakao API에서 x: lon + restaurant.setLat(new BigDecimal(doc.get("y").asText())); // 위도 + restaurant.setLon(new BigDecimal(doc.get("x").asText())); // 경도 restaurant.setPhone(doc.has("phone") ? doc.get("phone").asText() : null); restaurant.setKakaomap_url(doc.get("place_url").asText()); restaurant.setCategory_code(doc.has("category_group_code") ? doc.get("category_group_code").asText() : null); restaurant.setCategoryGroupName(doc.has("category_group_name") ? doc.get("category_group_name").asText() : null); restaurant.setCategoryName(doc.has("category_name") ? doc.get("category_name").asText() : null); - restaurant.setKakao_id(doc.get("id").asText()); savedRestaurants.add(restaurant); restaurantRepository.save(restaurant); - savedCount++; // 저장한 개수 카운트 + savedCount++; } } @@ -80,27 +82,28 @@ public List saveRestaurantsNearbyArea(Area area) { return savedRestaurants; } - // 전체 Area 대상 음식점 저장 (fetch & save) + // 전체 Area 대상 음식점 저장 @Transactional public void fetchAndSaveRestaurants() { - List areas = areaRepository.findAll(); // 모든 지역 조회 + List areas = areaRepository.findAll(); for (Area area : areas) { - saveRestaurantsNearbyArea(area); // 각 지역별 저장 + saveRestaurantsNearbyArea(area); } } - // Haversine 거리 계산 공식 + // Haversine 거리 계산 공식 (meter 단위) private double calculateDistance(double lat1, double lon1, double lat2, double lon2) { - final int EARTH_RADIUS = 6371; // 지구 반지름 (km) + final int EARTH_RADIUS = 6371; // km double dLat = Math.toRadians(lat2 - lat1); double dLon = Math.toRadians(lon2 - lon1); - double a = Math.sin(dLat/2) * Math.sin(dLat/2) + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) - * Math.sin(dLon/2) * Math.sin(dLon/2); - double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + * Math.sin(dLon / 2) * Math.sin(dLon / 2); + + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return EARTH_RADIUS * c * 1000; // meter 단위로 반환 + return EARTH_RADIUS * c * 1000; // meter 반환 } } diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index 6ecb39a..d091d4b 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -1,4 +1,4 @@ -INSERT INTO area (seoul_id, name, name_eng, category, lon, lat) VALUES +INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), From 39bea566c62e23663bed3defa6e8d239301db644 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 23 Apr 2025 14:41:14 +0900 Subject: [PATCH 087/361] =?UTF-8?q?Refactor:lat,lon=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/RestaurantDetailResponse.java | 2 +- .../placeservice/entity/Restaurant.java | 1 - .../repository/RestaurantRepository.java | 2 +- .../service/RestaurantService.java | 37 ++++++++++--------- place-service/src/main/resources/data.sql | 2 +- 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java b/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java index a7effcc..7840858 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java +++ b/place-service/src/main/java/com/example/placeservice/dto/RestaurantDetailResponse.java @@ -46,4 +46,4 @@ public static RestaurantDetailResponse fromEntity(Restaurant restaurant) { .lon(restaurant.getLon().toString()) .build(); } -} +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java b/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java index 552bff8..8207738 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Restaurant.java @@ -65,4 +65,3 @@ public void setKakao_id(String kakao_id) { } - diff --git a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java index ba08e9f..b331b2d 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java @@ -5,4 +5,4 @@ public interface RestaurantRepository extends JpaRepository { // 기본 CRUD 사용 (findAll, findById, save 등) -} +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java index eb3c2fa..032a4f2 100644 --- a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java +++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java @@ -30,11 +30,12 @@ public List saveRestaurantsNearbyArea(Area area) { int savedCount = 0; // 저장한 음식점 수 while (!isEnd) { - JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLat(), area.getLon(), page); + // ✅ Kakao API 호출 시 경도(lon), 위도(lat) 순서로 전달 + JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLon(), area.getLat(), page); if (response.has("documents")) { for (JsonNode doc : response.get("documents")) { - if (savedCount >= 15) { // 15개 저장했으면 바로 종료 + if (savedCount >= 15) { // 15개 저장했으면 종료 isEnd = true; break; } @@ -43,8 +44,10 @@ public List saveRestaurantsNearbyArea(Area area) { double restaurantLat = Double.parseDouble(doc.get("y").asText()); double distance = calculateDistance( - area.getLon().doubleValue(), area.getLat().doubleValue(), - restaurantLat, restaurantLon + area.getLat().doubleValue(), // 위도 + area.getLon().doubleValue(), // 경도 + restaurantLat, // 위도 + restaurantLon // 경도 ); // 1km 이내 음식점만 저장 @@ -53,19 +56,18 @@ public List saveRestaurantsNearbyArea(Area area) { restaurant.setArea(area); restaurant.setName(doc.get("place_name").asText()); restaurant.setAddress(doc.get("road_address_name").asText()); - restaurant.setLat(new BigDecimal(doc.get("y").asText())); // Kakao API에서 y: lat - restaurant.setLon(new BigDecimal(doc.get("x").asText())); // Kakao API에서 x: lon + restaurant.setLat(new BigDecimal(doc.get("y").asText())); // 위도 + restaurant.setLon(new BigDecimal(doc.get("x").asText())); // 경도 restaurant.setPhone(doc.has("phone") ? doc.get("phone").asText() : null); restaurant.setKakaomap_url(doc.get("place_url").asText()); restaurant.setCategory_code(doc.has("category_group_code") ? doc.get("category_group_code").asText() : null); restaurant.setCategoryGroupName(doc.has("category_group_name") ? doc.get("category_group_name").asText() : null); restaurant.setCategoryName(doc.has("category_name") ? doc.get("category_name").asText() : null); - restaurant.setKakao_id(doc.get("id").asText()); savedRestaurants.add(restaurant); restaurantRepository.save(restaurant); - savedCount++; // 저장한 개수 카운트 + savedCount++; } } @@ -80,27 +82,28 @@ public List saveRestaurantsNearbyArea(Area area) { return savedRestaurants; } - // 전체 Area 대상 음식점 저장 (fetch & save) + // 전체 Area 대상 음식점 저장 @Transactional public void fetchAndSaveRestaurants() { - List areas = areaRepository.findAll(); // 모든 지역 조회 + List areas = areaRepository.findAll(); for (Area area : areas) { - saveRestaurantsNearbyArea(area); // 각 지역별 저장 + saveRestaurantsNearbyArea(area); } } - // Haversine 거리 계산 공식 + // Haversine 거리 계산 공식 (meter 단위) private double calculateDistance(double lat1, double lon1, double lat2, double lon2) { - final int EARTH_RADIUS = 6371; // 지구 반지름 (km) + final int EARTH_RADIUS = 6371; // km double dLat = Math.toRadians(lat2 - lat1); double dLon = Math.toRadians(lon2 - lon1); - double a = Math.sin(dLat/2) * Math.sin(dLat/2) + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) - * Math.sin(dLon/2) * Math.sin(dLon/2); - double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + * Math.sin(dLon / 2) * Math.sin(dLon / 2); + + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return EARTH_RADIUS * c * 1000; // meter 단위로 반환 + return EARTH_RADIUS * c * 1000; // meter 반환 } } diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index 6ecb39a..d091d4b 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -1,4 +1,4 @@ -INSERT INTO area (seoul_id, name, name_eng, category, lon, lat) VALUES +INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), From a288e6a4a737d3b3f8b83e6de986f9eedafc03b6 Mon Sep 17 00:00:00 2001 From: GLENSIS Date: Wed, 23 Apr 2025 10:52:05 +0900 Subject: [PATCH 088/361] =?UTF-8?q?GuArea=20=EC=82=AD=EC=A0=9C=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CulturalEventController.java | 26 +++++++ .../placeservice/dto/CulturalEventInfo.java | 16 ++++ .../placeservice/dto/CulturalEventItem.java | 59 ++++++++++++++ .../dto/CulturalEventResponse.java | 10 +++ .../com/example/placeservice/dto/Result.java | 9 +++ .../placeservice/entity/CulturalEvent.java | 58 ++++++++++++++ .../external/CulturalEventClient.java | 20 +++++ .../init/EventDataInitializer.java | 19 +++++ .../repository/CulturalEventRepository.java | 14 ++++ .../service/CulturalEventService.java | 76 +++++++++++++++++++ .../service/EventParserService.java | 61 +++++++++++++++ .../src/main/resources/application.properties | 3 +- 12 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/CulturalEventInfo.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/CulturalEventItem.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/CulturalEventResponse.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/Result.java create mode 100644 place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java create mode 100644 place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java create mode 100644 place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/EventParserService.java diff --git a/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java b/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java new file mode 100644 index 0000000..76fd796 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java @@ -0,0 +1,26 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.entity.CulturalEvent; +import com.example.placeservice.repository.CulturalEventRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/main") +@RequiredArgsConstructor +@Slf4j +public class CulturalEventController { + + private final CulturalEventRepository culturalEventRepository; + + // /main/events: 저장된 모든 이벤트 조회 + @GetMapping("/events") + public List getAllEvents() { + return culturalEventRepository.findAll(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/CulturalEventInfo.java b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventInfo.java new file mode 100644 index 0000000..ad6d466 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventInfo.java @@ -0,0 +1,16 @@ +package com.example.placeservice.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class CulturalEventInfo { + private int list_total_count; + private Result RESULT; + @JsonProperty("row") + private List row; +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/CulturalEventItem.java b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventItem.java new file mode 100644 index 0000000..236accf --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventItem.java @@ -0,0 +1,59 @@ +package com.example.placeservice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +public class CulturalEventItem { + + @JsonProperty("CODENAME") + private String codename; + + @JsonProperty("GUNAME") + private String guname; + + @JsonProperty("TITLE") + private String title; + + @JsonProperty("PLACE") + private String place; + + @JsonProperty("ORG_NAME") + private String orgName; + + @JsonProperty("USE_TRGT") + private String useTrgt; + + @JsonProperty("USE_FEE") + private String useFee; + + @JsonProperty("ORG_LINK") + private String orgLink; + + @JsonProperty("MAIN_IMG") + private String mainImg; + + @JsonProperty("STRTDATE") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.S") // 날짜 포맷 설정 + private LocalDateTime strtdate; + + @JsonProperty("END_DATE") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.S") // 날짜 포맷 설정 + private LocalDateTime endDate; + + @JsonProperty("LOT") + private BigDecimal lot; + + @JsonProperty("LAT") + private BigDecimal lat; + + @JsonProperty("IS_FREE") + private String isFree; + + @JsonProperty("HMPG_ADDR") + private String hmpgAddr; +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/CulturalEventResponse.java b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventResponse.java new file mode 100644 index 0000000..cbe9153 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventResponse.java @@ -0,0 +1,10 @@ +package com.example.placeservice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class CulturalEventResponse { + @JsonProperty("culturalEventInfo") + private CulturalEventInfo culturalEventInfo; +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/Result.java b/place-service/src/main/java/com/example/placeservice/dto/Result.java new file mode 100644 index 0000000..efd696a --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/Result.java @@ -0,0 +1,9 @@ +package com.example.placeservice.dto; + +import lombok.Data; + +@Data +public class Result { + private String CODE; + private String MESSAGE; +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java new file mode 100644 index 0000000..dd9f350 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java @@ -0,0 +1,58 @@ +package com.example.placeservice.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class CulturalEvent { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "event_id") + private Long eventId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "area_id") + @JsonIgnore + private Area area; + + + @Column(name = "category", length = 200) + private String category; + + @Column(name = "title", length = 50) + private String title; + + @Column(name = "address", length = 50) + private String address; // PLACE → address + + @Column(name = "lat", precision = 10, scale = 6) + private BigDecimal lat; + + @Column(name = "lon", precision = 10, scale = 6) + private BigDecimal lon; + + @Column(name = "target", length = 100) + private String target; + + @Column(name = "event_fee", columnDefinition = "TEXT") + private String eventFee; + + @Column(name = "event_img", length = 200) + private String eventImg; + + @Column(name = "start_date") + private LocalDateTime startDate; + + @Column(name = "end_date") + private LocalDateTime endDate; +} diff --git a/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java b/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java new file mode 100644 index 0000000..027dc4e --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java @@ -0,0 +1,20 @@ +package com.example.placeservice.external; + +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class CulturalEventClient { + + private final RestTemplate restTemplate; + + public CulturalEventClient(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + // API에서 1000개씩 데이터를 받아오는 메서드 + public String fetchEventData(int startIndex, int endIndex) { + String url = String.format("http://openapi.seoul.go.kr:8088/7669764c417069613736734567476c/json/culturalEventInfo/%d/%d/", startIndex, endIndex); + return restTemplate.getForObject(url, String.class); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java b/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java new file mode 100644 index 0000000..f6dd9d9 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java @@ -0,0 +1,19 @@ +package com.example.placeservice.init; + +import com.example.placeservice.service.CulturalEventService; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class EventDataInitializer implements CommandLineRunner { + + private final CulturalEventService culturalEventService; + + @Override + public void run(String... args) { + // 애플리케이션 시작 시 데이터를 자동으로 받아와 저장 + culturalEventService.fetchAndSaveAllEvents(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java new file mode 100644 index 0000000..f238de4 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java @@ -0,0 +1,14 @@ +// CulturalEventRepository.java +package com.example.placeservice.repository; + +import com.example.placeservice.entity.CulturalEvent; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; + +@Repository +public interface CulturalEventRepository extends JpaRepository { + boolean existsByTitleAndAddressAndStartDate(String title, String address, LocalDateTime startDate); + +} diff --git a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java new file mode 100644 index 0000000..f047076 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java @@ -0,0 +1,76 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.CulturalEventItem; +import com.example.placeservice.entity.Area; +import com.example.placeservice.entity.CulturalEvent; +import com.example.placeservice.external.CulturalEventClient; +import com.example.placeservice.repository.AreaRepository; +import com.example.placeservice.repository.CulturalEventRepository; +import com.example.placeservice.service.EventParserService; +import com.example.placeservice.util.GeoUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CulturalEventService { + + private final CulturalEventClient culturalEventClient; + private final EventParserService eventParserService; + private final CulturalEventRepository culturalEventRepository; + private final AreaRepository areaRepository; // 🔹 추가 + + @Transactional + public void fetchAndSaveAllEvents() { + int[] ranges = {1, 1001, 2001, 3001, 4001, 5001}; + for (int i = 0; i < ranges.length; i++) { + int startIndex = ranges[i]; + int endIndex = startIndex + 999; + + String jsonData = culturalEventClient.fetchEventData(startIndex, endIndex); + List eventItems = eventParserService.parse(jsonData); + + if (!eventItems.isEmpty()) { + saveEvents(eventItems); + } + } + } + + private void saveEvents(List eventItems) { + List events = eventParserService.toEntityList(eventItems); + List areas = areaRepository.findAll(); // 🔹 모든 지역 좌표 불러오기 + + for (CulturalEvent event : events) { + if (!culturalEventRepository.existsByTitleAndAddressAndStartDate( + event.getTitle(), event.getAddress(), event.getStartDate())) { + + // 🔹 행사 위치 기준 가장 가까운 지역 찾기 + Area nearestArea = findNearestArea(event.getLat().doubleValue(), event.getLon().doubleValue(), areas); + event.setArea(nearestArea); // 🔹 지역 설정 + + culturalEventRepository.save(event); + } + } + } + + private Area findNearestArea(double lat, double lon, List areas) { + double minDistance = Double.MAX_VALUE; + Area nearestArea = null; + + for (Area area : areas) { + double areaLat = area.getLat().doubleValue(); + double areaLon = area.getLon().doubleValue(); + double distance = GeoUtils.calculateDistanceKm(lat, lon, areaLat, areaLon); + + if (distance < minDistance) { + minDistance = distance; + nearestArea = area; + } + } + + return nearestArea; + } +} diff --git a/place-service/src/main/java/com/example/placeservice/service/EventParserService.java b/place-service/src/main/java/com/example/placeservice/service/EventParserService.java new file mode 100644 index 0000000..3d599ed --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/EventParserService.java @@ -0,0 +1,61 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.CulturalEventItem; +import com.example.placeservice.dto.CulturalEventResponse; +import com.example.placeservice.entity.CulturalEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor // final 필드에 대한 생성자 주입 +public class EventParserService { + + private final ObjectMapper objectMapper; + + public List parse(String jsonResponse) { + try { + CulturalEventResponse response = objectMapper.readValue(jsonResponse, CulturalEventResponse.class); + return response.getCulturalEventInfo().getRow(); + } catch (Exception e) { + log.error("파싱 중 오류 발생: {}", e.getMessage()); + return List.of(); // 빈 리스트 반환 + } + } + + public CulturalEvent toEntity(CulturalEventItem item) { + CulturalEvent event = new CulturalEvent(); + + + // 2. 필드 매핑 (컬럼명에 맞춰 정확히 매핑) + event.setCategory(item.getCodename()); // category + event.setTitle(item.getTitle()); // title + event.setAddress(item.getPlace()); // address + event.setLat(item.getLat()); // lat + event.setLon(item.getLot()); // lon + event.setTarget(item.getUseTrgt()); // target + event.setEventFee(item.getUseFee()); // event_fee + event.setStartDate(item.getStrtdate()); // start_date + event.setEndDate(item.getEndDate()); // end_date + + // 3. 이미지 URL 정리 + String imageUrl = item.getMainImg(); + if (imageUrl != null) { + imageUrl = imageUrl.replace("https://culture.seoul.go.ko?", + "https://culture.seoul.go.kr/cmmn/file/getImage.do?"); + } + event.setEventImg(imageUrl); // event_img + + return event; + } + + + public List toEntityList(List items) { + return items.stream() + .map(this::toEntity) + .toList(); + } +} diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 02c04c8..72673c5 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -7,9 +7,10 @@ spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update +spring.jpa.hibernate.ddl-auto=none spring.jpa.defer-datasource-initialization=true # Hibernate SQL ?? ??? spring.jpa.show-sql=true spring.sql.init.mode=always + From 667786c080656d05b073e3e428023cfbc49dab36 Mon Sep 17 00:00:00 2001 From: GLENSIS Date: Wed, 23 Apr 2025 10:52:05 +0900 Subject: [PATCH 089/361] =?UTF-8?q?GuArea=20=EC=82=AD=EC=A0=9C=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CulturalEventController.java | 26 +++++++ .../placeservice/dto/CulturalEventInfo.java | 16 ++++ .../placeservice/dto/CulturalEventItem.java | 59 ++++++++++++++ .../dto/CulturalEventResponse.java | 10 +++ .../com/example/placeservice/dto/Result.java | 9 +++ .../placeservice/entity/CulturalEvent.java | 58 ++++++++++++++ .../external/CulturalEventClient.java | 20 +++++ .../init/EventDataInitializer.java | 19 +++++ .../repository/CulturalEventRepository.java | 14 ++++ .../service/CulturalEventService.java | 76 +++++++++++++++++++ .../service/EventParserService.java | 61 +++++++++++++++ .../src/main/resources/application.properties | 3 +- 12 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/CulturalEventInfo.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/CulturalEventItem.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/CulturalEventResponse.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/Result.java create mode 100644 place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java create mode 100644 place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java create mode 100644 place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/EventParserService.java diff --git a/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java b/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java new file mode 100644 index 0000000..76fd796 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java @@ -0,0 +1,26 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.entity.CulturalEvent; +import com.example.placeservice.repository.CulturalEventRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/main") +@RequiredArgsConstructor +@Slf4j +public class CulturalEventController { + + private final CulturalEventRepository culturalEventRepository; + + // /main/events: 저장된 모든 이벤트 조회 + @GetMapping("/events") + public List getAllEvents() { + return culturalEventRepository.findAll(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/CulturalEventInfo.java b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventInfo.java new file mode 100644 index 0000000..ad6d466 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventInfo.java @@ -0,0 +1,16 @@ +package com.example.placeservice.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class CulturalEventInfo { + private int list_total_count; + private Result RESULT; + @JsonProperty("row") + private List row; +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/CulturalEventItem.java b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventItem.java new file mode 100644 index 0000000..236accf --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventItem.java @@ -0,0 +1,59 @@ +package com.example.placeservice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +public class CulturalEventItem { + + @JsonProperty("CODENAME") + private String codename; + + @JsonProperty("GUNAME") + private String guname; + + @JsonProperty("TITLE") + private String title; + + @JsonProperty("PLACE") + private String place; + + @JsonProperty("ORG_NAME") + private String orgName; + + @JsonProperty("USE_TRGT") + private String useTrgt; + + @JsonProperty("USE_FEE") + private String useFee; + + @JsonProperty("ORG_LINK") + private String orgLink; + + @JsonProperty("MAIN_IMG") + private String mainImg; + + @JsonProperty("STRTDATE") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.S") // 날짜 포맷 설정 + private LocalDateTime strtdate; + + @JsonProperty("END_DATE") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.S") // 날짜 포맷 설정 + private LocalDateTime endDate; + + @JsonProperty("LOT") + private BigDecimal lot; + + @JsonProperty("LAT") + private BigDecimal lat; + + @JsonProperty("IS_FREE") + private String isFree; + + @JsonProperty("HMPG_ADDR") + private String hmpgAddr; +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/CulturalEventResponse.java b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventResponse.java new file mode 100644 index 0000000..cbe9153 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/CulturalEventResponse.java @@ -0,0 +1,10 @@ +package com.example.placeservice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class CulturalEventResponse { + @JsonProperty("culturalEventInfo") + private CulturalEventInfo culturalEventInfo; +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/Result.java b/place-service/src/main/java/com/example/placeservice/dto/Result.java new file mode 100644 index 0000000..efd696a --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/Result.java @@ -0,0 +1,9 @@ +package com.example.placeservice.dto; + +import lombok.Data; + +@Data +public class Result { + private String CODE; + private String MESSAGE; +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java new file mode 100644 index 0000000..dd9f350 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java @@ -0,0 +1,58 @@ +package com.example.placeservice.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class CulturalEvent { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "event_id") + private Long eventId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "area_id") + @JsonIgnore + private Area area; + + + @Column(name = "category", length = 200) + private String category; + + @Column(name = "title", length = 50) + private String title; + + @Column(name = "address", length = 50) + private String address; // PLACE → address + + @Column(name = "lat", precision = 10, scale = 6) + private BigDecimal lat; + + @Column(name = "lon", precision = 10, scale = 6) + private BigDecimal lon; + + @Column(name = "target", length = 100) + private String target; + + @Column(name = "event_fee", columnDefinition = "TEXT") + private String eventFee; + + @Column(name = "event_img", length = 200) + private String eventImg; + + @Column(name = "start_date") + private LocalDateTime startDate; + + @Column(name = "end_date") + private LocalDateTime endDate; +} diff --git a/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java b/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java new file mode 100644 index 0000000..027dc4e --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java @@ -0,0 +1,20 @@ +package com.example.placeservice.external; + +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class CulturalEventClient { + + private final RestTemplate restTemplate; + + public CulturalEventClient(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + // API에서 1000개씩 데이터를 받아오는 메서드 + public String fetchEventData(int startIndex, int endIndex) { + String url = String.format("http://openapi.seoul.go.kr:8088/7669764c417069613736734567476c/json/culturalEventInfo/%d/%d/", startIndex, endIndex); + return restTemplate.getForObject(url, String.class); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java b/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java new file mode 100644 index 0000000..f6dd9d9 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java @@ -0,0 +1,19 @@ +package com.example.placeservice.init; + +import com.example.placeservice.service.CulturalEventService; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class EventDataInitializer implements CommandLineRunner { + + private final CulturalEventService culturalEventService; + + @Override + public void run(String... args) { + // 애플리케이션 시작 시 데이터를 자동으로 받아와 저장 + culturalEventService.fetchAndSaveAllEvents(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java new file mode 100644 index 0000000..f238de4 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java @@ -0,0 +1,14 @@ +// CulturalEventRepository.java +package com.example.placeservice.repository; + +import com.example.placeservice.entity.CulturalEvent; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; + +@Repository +public interface CulturalEventRepository extends JpaRepository { + boolean existsByTitleAndAddressAndStartDate(String title, String address, LocalDateTime startDate); + +} diff --git a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java new file mode 100644 index 0000000..f047076 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java @@ -0,0 +1,76 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.CulturalEventItem; +import com.example.placeservice.entity.Area; +import com.example.placeservice.entity.CulturalEvent; +import com.example.placeservice.external.CulturalEventClient; +import com.example.placeservice.repository.AreaRepository; +import com.example.placeservice.repository.CulturalEventRepository; +import com.example.placeservice.service.EventParserService; +import com.example.placeservice.util.GeoUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CulturalEventService { + + private final CulturalEventClient culturalEventClient; + private final EventParserService eventParserService; + private final CulturalEventRepository culturalEventRepository; + private final AreaRepository areaRepository; // 🔹 추가 + + @Transactional + public void fetchAndSaveAllEvents() { + int[] ranges = {1, 1001, 2001, 3001, 4001, 5001}; + for (int i = 0; i < ranges.length; i++) { + int startIndex = ranges[i]; + int endIndex = startIndex + 999; + + String jsonData = culturalEventClient.fetchEventData(startIndex, endIndex); + List eventItems = eventParserService.parse(jsonData); + + if (!eventItems.isEmpty()) { + saveEvents(eventItems); + } + } + } + + private void saveEvents(List eventItems) { + List events = eventParserService.toEntityList(eventItems); + List areas = areaRepository.findAll(); // 🔹 모든 지역 좌표 불러오기 + + for (CulturalEvent event : events) { + if (!culturalEventRepository.existsByTitleAndAddressAndStartDate( + event.getTitle(), event.getAddress(), event.getStartDate())) { + + // 🔹 행사 위치 기준 가장 가까운 지역 찾기 + Area nearestArea = findNearestArea(event.getLat().doubleValue(), event.getLon().doubleValue(), areas); + event.setArea(nearestArea); // 🔹 지역 설정 + + culturalEventRepository.save(event); + } + } + } + + private Area findNearestArea(double lat, double lon, List areas) { + double minDistance = Double.MAX_VALUE; + Area nearestArea = null; + + for (Area area : areas) { + double areaLat = area.getLat().doubleValue(); + double areaLon = area.getLon().doubleValue(); + double distance = GeoUtils.calculateDistanceKm(lat, lon, areaLat, areaLon); + + if (distance < minDistance) { + minDistance = distance; + nearestArea = area; + } + } + + return nearestArea; + } +} diff --git a/place-service/src/main/java/com/example/placeservice/service/EventParserService.java b/place-service/src/main/java/com/example/placeservice/service/EventParserService.java new file mode 100644 index 0000000..3d599ed --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/EventParserService.java @@ -0,0 +1,61 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.CulturalEventItem; +import com.example.placeservice.dto.CulturalEventResponse; +import com.example.placeservice.entity.CulturalEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor // final 필드에 대한 생성자 주입 +public class EventParserService { + + private final ObjectMapper objectMapper; + + public List parse(String jsonResponse) { + try { + CulturalEventResponse response = objectMapper.readValue(jsonResponse, CulturalEventResponse.class); + return response.getCulturalEventInfo().getRow(); + } catch (Exception e) { + log.error("파싱 중 오류 발생: {}", e.getMessage()); + return List.of(); // 빈 리스트 반환 + } + } + + public CulturalEvent toEntity(CulturalEventItem item) { + CulturalEvent event = new CulturalEvent(); + + + // 2. 필드 매핑 (컬럼명에 맞춰 정확히 매핑) + event.setCategory(item.getCodename()); // category + event.setTitle(item.getTitle()); // title + event.setAddress(item.getPlace()); // address + event.setLat(item.getLat()); // lat + event.setLon(item.getLot()); // lon + event.setTarget(item.getUseTrgt()); // target + event.setEventFee(item.getUseFee()); // event_fee + event.setStartDate(item.getStrtdate()); // start_date + event.setEndDate(item.getEndDate()); // end_date + + // 3. 이미지 URL 정리 + String imageUrl = item.getMainImg(); + if (imageUrl != null) { + imageUrl = imageUrl.replace("https://culture.seoul.go.ko?", + "https://culture.seoul.go.kr/cmmn/file/getImage.do?"); + } + event.setEventImg(imageUrl); // event_img + + return event; + } + + + public List toEntityList(List items) { + return items.stream() + .map(this::toEntity) + .toList(); + } +} diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 02c04c8..72673c5 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -7,9 +7,10 @@ spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update +spring.jpa.hibernate.ddl-auto=none spring.jpa.defer-datasource-initialization=true # Hibernate SQL ?? ??? spring.jpa.show-sql=true spring.sql.init.mode=always + From e8d22c877d9c2487422815fbee69f1493675e5f3 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 23 Apr 2025 16:32:22 +0900 Subject: [PATCH 090/361] =?UTF-8?q?Feat:=20=EB=8F=84=EB=A1=9C=20=EC=86=8C?= =?UTF-8?q?=ED=86=B5=20=ED=98=84=ED=99=A9=20API=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20-=20Elasticsearch=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=B0=9B=EC=95=84=EC=98=A4=EB=8A=94=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ESRoadController.java | 25 ++++ .../controller/RoadController.java | 21 ---- .../externalinfoservice/dto/RoadDto.java | 115 ------------------ .../service/ESRoadService.java | 63 ++++++++++ .../service/RoadService.java | 52 -------- 5 files changed, 88 insertions(+), 188 deletions(-) create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/ESRoadController.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ESRoadController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ESRoadController.java new file mode 100644 index 0000000..ba30aa5 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ESRoadController.java @@ -0,0 +1,25 @@ +package com.example.externalinfoservice.controller; + +import com.example.externalinfoservice.service.ESRoadService; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + + +@RestController +@RequestMapping("/main/traffic") +@RequiredArgsConstructor +public class ESRoadController { + private final ESRoadService roadService; + + @GetMapping("/{area-nm}") + public JsonNode Traffic(@PathVariable("area-nm") String areaNm) { + return ESRoadService.getTrafficData(areaNm); + } + + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java deleted file mode 100644 index 44bfd26..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.externalinfoservice.controller; - -import com.example.externalinfoservice.service.RoadService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/traffic") -@RequiredArgsConstructor -public class RoadController { - private final RoadService roadService; - - @GetMapping("/{area-id}") - public String getRoadData(@PathVariable("area-id") String id) { - // TrafficService 호출하여 OpenAPI 데이터를 JSON 형식으로 반환 - return roadService.getTrafficData(id); - } -} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java deleted file mode 100644 index 8918c96..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.example.externalinfoservice.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.xml.bind.annotation.XmlAccessType; -import jakarta.xml.bind.annotation.XmlAccessorType; -import jakarta.xml.bind.annotation.XmlElement; -import jakarta.xml.bind.annotation.XmlRootElement; -import lombok.Data; - -import java.util.List; - -@Data -@XmlRootElement(name = "SeoulRtd.citydata") -@XmlAccessorType(XmlAccessType.FIELD) -public class RoadDto { - - @XmlElement(name = "CITYDATA") - @JsonProperty("city_data") - private CityData cityData; - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class CityData { - @XmlElement(name = "ROAD_TRAFFIC_STTS") - @JsonProperty("road_traffic_stts") - private RoadTrafficSttsWrapper roadTrafficSttsWrapper; - } - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class RoadTrafficSttsWrapper { - @XmlElement(name = "AVG_ROAD_DATA") - @JsonProperty("avg_road_data") - private AvgRoadData avgRoadData; - - @XmlElement(name = "ROAD_TRAFFIC_STTS") - @JsonProperty("road_traffic_stts") - private List roadTrafficDetailList; - } - - - @XmlAccessorType(XmlAccessType.FIELD) - public static class AvgRoadData { - @XmlElement(name = "ROAD_MSG") - @JsonProperty("road_msg") - private String roadMsg; - - @XmlElement(name = "ROAD_TRAFFIC_IDX") - @JsonProperty("road_traffic_idx") - private String trafficIdx; - - @XmlElement(name = "ROAD_TRAFFIC_SPD") - @JsonProperty("road_traffic_spd") - private int trafficSpeed; - - @XmlElement(name = "ROAD_TRAFFIC_TIME") - @JsonProperty("road_traffic_time") - private String trafficTime; - - // Getters & Setters - } - - @XmlAccessorType(XmlAccessType.FIELD) - public static class RoadTrafficDetail { - @XmlElement(name = "LINK_ID") - @JsonProperty("link_id") - private String linkId; - - @XmlElement(name = "ROAD_NM") - @JsonProperty("road_nm") - private String roadName; - - @XmlElement(name = "START_ND_CD") - @JsonProperty("start_nd_cd") - private String startNodeCode; - - @XmlElement(name = "START_ND_NM") - @JsonProperty("start_nd_nm") - private String startNodeName; - - @XmlElement(name = "START_ND_XY") - @JsonProperty("start_nd_xy") - private String startCoordinates; - - @XmlElement(name = "END_ND_CD") - @JsonProperty("end_nd_cd") - private String endNodeCode; - - @XmlElement(name = "END_ND_NM") - @JsonProperty("end_nd_nm") - private String endNodeName; - - @XmlElement(name = "END_ND_XY") - @JsonProperty("end_nd_xy") - private String endCoordinates; - - @XmlElement(name = "DIST") - @JsonProperty("dist") - private double distance; - - @XmlElement(name = "SPD") - @JsonProperty("spd") - private double speed; - - @XmlElement(name = "IDX") - @JsonProperty("idx") - private String congestionIndex; - - @XmlElement(name = "XYLIST") - @JsonProperty("xylist") - private String xyList; - - // Getters & Setters - } -} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java new file mode 100644 index 0000000..2a0e559 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java @@ -0,0 +1,63 @@ +package com.example.externalinfoservice.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.apache.kafka.common.errors.InvalidRequestException; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +@RequiredArgsConstructor +public class ESRoadService { + + private static final RestTemplate restTemplate = new RestTemplate(); + + public static JsonNode getTrafficData(String areaNm) { + try { + String apiUrl = "http://elasticsearch.seoultravel.life/seoul_citydata_road/_search"; // 외부 API URL + + // JSON Body 생성 + String jsonBody = String.format("{\n" + + " \"size\": 1,\n" + + " \"query\": {\n" + + " \"term\": {\n" + + " \"road_traffic.area_nm.keyword\": \"%s\"\n" + + " }\n" + + " }\n" + + "}", areaNm); + // 헤더 설정 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + // 요청 엔티티 만들기 + HttpEntity requestEntity = new HttpEntity<>(jsonBody, headers); + + // POST 요청 보내기 + ResponseEntity response = restTemplate.postForEntity(apiUrl, requestEntity, String.class); + + String body = response.getBody(); + // road_traffic 데이터만 가져오기 + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(String.valueOf(body)); // response는 JSON 문자열 + + JsonNode roadTraffic = root + .path("hits") + .path("hits") + .get(0) // 첫 번째 결과 + .path("_source") + .path("road_traffic"); + + + return roadTraffic; // 외부 API에서 받은 데이터를 그대로 반환 + }catch (InvalidRequestException e) { + throw new InvalidRequestException(e.getMessage(), e); + }catch (Exception e){ + System.out.println(e); + throw new RuntimeException("예상치 못한 오류",e); + } + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java deleted file mode 100644 index 2415816..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.externalinfoservice.service; - -import com.example.externalinfoservice.dto.RoadDto; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.Unmarshaller; -import org.apache.kafka.common.errors.InvalidRequestException; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.io.IOException; -import java.io.StringReader; - -@Service -public class RoadService { - - @Value("http://openapi.seoul.go.kr:8088/sample/xml/citydata/1/5/") - private String openApiUrl; - - private final RestTemplate restTemplate; - - public RoadService(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } - - public String getTrafficData(String id) { - try { - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String xmlResponse = restTemplate.getForObject(openApiUrl + id, String.class); - - // 2. XML 데이터를 DTO로 변환 -> JSON - ObjectMapper objectMapper = new ObjectMapper(); - String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); - - return json; - } catch (InvalidRequestException e) { - throw new InvalidRequestException(e.getMessage(), e); - } catch(IOException e){ - throw new RuntimeException("외부 API 오류",e); - } catch (Exception e) { - throw new RuntimeException("예상치 못한 오류",e); - } - } - - private RoadDto xmlToDto(String xml) throws Exception { - // JAXB를 사용해서 XML을 DTO로 변환 - JAXBContext context = JAXBContext.newInstance(RoadDto.class); - Unmarshaller unmarshaller = context.createUnmarshaller(); - return (RoadDto) unmarshaller.unmarshal(new StringReader(xml)); - } -} From 91aa52e39e73df998f04048bbc6c5a767678dc0d Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 23 Apr 2025 16:32:22 +0900 Subject: [PATCH 091/361] =?UTF-8?q?Feat:=20=EB=8F=84=EB=A1=9C=20=EC=86=8C?= =?UTF-8?q?=ED=86=B5=20=ED=98=84=ED=99=A9=20API=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20-=20Elasticsearch=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=B0=9B=EC=95=84=EC=98=A4=EB=8A=94=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ESRoadController.java | 25 ++++ .../controller/RoadController.java | 21 ---- .../externalinfoservice/dto/RoadDto.java | 115 ------------------ .../service/ESRoadService.java | 63 ++++++++++ .../service/RoadService.java | 52 -------- 5 files changed, 88 insertions(+), 188 deletions(-) create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/ESRoadController.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ESRoadController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ESRoadController.java new file mode 100644 index 0000000..ba30aa5 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ESRoadController.java @@ -0,0 +1,25 @@ +package com.example.externalinfoservice.controller; + +import com.example.externalinfoservice.service.ESRoadService; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + + +@RestController +@RequestMapping("/main/traffic") +@RequiredArgsConstructor +public class ESRoadController { + private final ESRoadService roadService; + + @GetMapping("/{area-nm}") + public JsonNode Traffic(@PathVariable("area-nm") String areaNm) { + return ESRoadService.getTrafficData(areaNm); + } + + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java deleted file mode 100644 index 44bfd26..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/RoadController.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.externalinfoservice.controller; - -import com.example.externalinfoservice.service.RoadService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/traffic") -@RequiredArgsConstructor -public class RoadController { - private final RoadService roadService; - - @GetMapping("/{area-id}") - public String getRoadData(@PathVariable("area-id") String id) { - // TrafficService 호출하여 OpenAPI 데이터를 JSON 형식으로 반환 - return roadService.getTrafficData(id); - } -} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java deleted file mode 100644 index 8918c96..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.example.externalinfoservice.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.xml.bind.annotation.XmlAccessType; -import jakarta.xml.bind.annotation.XmlAccessorType; -import jakarta.xml.bind.annotation.XmlElement; -import jakarta.xml.bind.annotation.XmlRootElement; -import lombok.Data; - -import java.util.List; - -@Data -@XmlRootElement(name = "SeoulRtd.citydata") -@XmlAccessorType(XmlAccessType.FIELD) -public class RoadDto { - - @XmlElement(name = "CITYDATA") - @JsonProperty("city_data") - private CityData cityData; - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class CityData { - @XmlElement(name = "ROAD_TRAFFIC_STTS") - @JsonProperty("road_traffic_stts") - private RoadTrafficSttsWrapper roadTrafficSttsWrapper; - } - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class RoadTrafficSttsWrapper { - @XmlElement(name = "AVG_ROAD_DATA") - @JsonProperty("avg_road_data") - private AvgRoadData avgRoadData; - - @XmlElement(name = "ROAD_TRAFFIC_STTS") - @JsonProperty("road_traffic_stts") - private List roadTrafficDetailList; - } - - - @XmlAccessorType(XmlAccessType.FIELD) - public static class AvgRoadData { - @XmlElement(name = "ROAD_MSG") - @JsonProperty("road_msg") - private String roadMsg; - - @XmlElement(name = "ROAD_TRAFFIC_IDX") - @JsonProperty("road_traffic_idx") - private String trafficIdx; - - @XmlElement(name = "ROAD_TRAFFIC_SPD") - @JsonProperty("road_traffic_spd") - private int trafficSpeed; - - @XmlElement(name = "ROAD_TRAFFIC_TIME") - @JsonProperty("road_traffic_time") - private String trafficTime; - - // Getters & Setters - } - - @XmlAccessorType(XmlAccessType.FIELD) - public static class RoadTrafficDetail { - @XmlElement(name = "LINK_ID") - @JsonProperty("link_id") - private String linkId; - - @XmlElement(name = "ROAD_NM") - @JsonProperty("road_nm") - private String roadName; - - @XmlElement(name = "START_ND_CD") - @JsonProperty("start_nd_cd") - private String startNodeCode; - - @XmlElement(name = "START_ND_NM") - @JsonProperty("start_nd_nm") - private String startNodeName; - - @XmlElement(name = "START_ND_XY") - @JsonProperty("start_nd_xy") - private String startCoordinates; - - @XmlElement(name = "END_ND_CD") - @JsonProperty("end_nd_cd") - private String endNodeCode; - - @XmlElement(name = "END_ND_NM") - @JsonProperty("end_nd_nm") - private String endNodeName; - - @XmlElement(name = "END_ND_XY") - @JsonProperty("end_nd_xy") - private String endCoordinates; - - @XmlElement(name = "DIST") - @JsonProperty("dist") - private double distance; - - @XmlElement(name = "SPD") - @JsonProperty("spd") - private double speed; - - @XmlElement(name = "IDX") - @JsonProperty("idx") - private String congestionIndex; - - @XmlElement(name = "XYLIST") - @JsonProperty("xylist") - private String xyList; - - // Getters & Setters - } -} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java new file mode 100644 index 0000000..2a0e559 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java @@ -0,0 +1,63 @@ +package com.example.externalinfoservice.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.apache.kafka.common.errors.InvalidRequestException; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +@RequiredArgsConstructor +public class ESRoadService { + + private static final RestTemplate restTemplate = new RestTemplate(); + + public static JsonNode getTrafficData(String areaNm) { + try { + String apiUrl = "http://elasticsearch.seoultravel.life/seoul_citydata_road/_search"; // 외부 API URL + + // JSON Body 생성 + String jsonBody = String.format("{\n" + + " \"size\": 1,\n" + + " \"query\": {\n" + + " \"term\": {\n" + + " \"road_traffic.area_nm.keyword\": \"%s\"\n" + + " }\n" + + " }\n" + + "}", areaNm); + // 헤더 설정 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + // 요청 엔티티 만들기 + HttpEntity requestEntity = new HttpEntity<>(jsonBody, headers); + + // POST 요청 보내기 + ResponseEntity response = restTemplate.postForEntity(apiUrl, requestEntity, String.class); + + String body = response.getBody(); + // road_traffic 데이터만 가져오기 + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(String.valueOf(body)); // response는 JSON 문자열 + + JsonNode roadTraffic = root + .path("hits") + .path("hits") + .get(0) // 첫 번째 결과 + .path("_source") + .path("road_traffic"); + + + return roadTraffic; // 외부 API에서 받은 데이터를 그대로 반환 + }catch (InvalidRequestException e) { + throw new InvalidRequestException(e.getMessage(), e); + }catch (Exception e){ + System.out.println(e); + throw new RuntimeException("예상치 못한 오류",e); + } + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java deleted file mode 100644 index 2415816..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.externalinfoservice.service; - -import com.example.externalinfoservice.dto.RoadDto; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.Unmarshaller; -import org.apache.kafka.common.errors.InvalidRequestException; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.io.IOException; -import java.io.StringReader; - -@Service -public class RoadService { - - @Value("http://openapi.seoul.go.kr:8088/sample/xml/citydata/1/5/") - private String openApiUrl; - - private final RestTemplate restTemplate; - - public RoadService(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } - - public String getTrafficData(String id) { - try { - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String xmlResponse = restTemplate.getForObject(openApiUrl + id, String.class); - - // 2. XML 데이터를 DTO로 변환 -> JSON - ObjectMapper objectMapper = new ObjectMapper(); - String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); - - return json; - } catch (InvalidRequestException e) { - throw new InvalidRequestException(e.getMessage(), e); - } catch(IOException e){ - throw new RuntimeException("외부 API 오류",e); - } catch (Exception e) { - throw new RuntimeException("예상치 못한 오류",e); - } - } - - private RoadDto xmlToDto(String xml) throws Exception { - // JAXB를 사용해서 XML을 DTO로 변환 - JAXBContext context = JAXBContext.newInstance(RoadDto.class); - Unmarshaller unmarshaller = context.createUnmarshaller(); - return (RoadDto) unmarshaller.unmarshal(new StringReader(xml)); - } -} From b0c799929fdf494857b8c8de0479d2a795288dad Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Thu, 24 Apr 2025 09:36:15 +0900 Subject: [PATCH 092/361] =?UTF-8?q?Feat:=EB=82=A0=EC=94=A8=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- external-info-service/build.gradle | 13 ++ .../externalinfoservice/config/AppConfig.java | 1 + .../config/SecurityConfig.java | 1 + .../controller/ParkingController.java | 19 ++ .../controller/WeatherController.java | 17 +- .../externalinfoservice/dto/ParkDto.java | 74 ++++++ .../externalinfoservice/dto/RoadDto.java | 6 +- .../externalinfoservice/entity/Area.java | 36 +++ .../exception/CustomExceptionClass.java | 34 +++ .../exception/ErrorCode.java | 49 ++++ .../exception/ErrorResponse.java | 18 ++ .../exception/GlobalExceptionHandler.java | 87 +++++++ .../repository/AreaRepository.java | 12 + .../service/ParkService.java | 52 +++++ .../service/RoadService.java | 18 +- .../service/WeatherService.java | 213 +++++++++++++++--- 16 files changed, 599 insertions(+), 51 deletions(-) create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/entity/Area.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/repository/AreaRepository.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java diff --git a/external-info-service/build.gradle b/external-info-service/build.gradle index 3b8f4df..3be0dd8 100644 --- a/external-info-service/build.gradle +++ b/external-info-service/build.gradle @@ -34,6 +34,19 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-configuration-processor' + implementation 'org.springframework.boot:spring-boot-starter-json' + implementation 'org.springframework.boot:spring-boot-starter-security' + + implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0' + implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.2' + + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' + + implementation 'org.postgresql:postgresql:42.7.2' implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java b/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java index 8e423a6..8225d9c 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java @@ -10,4 +10,5 @@ public class AppConfig { public RestTemplate restTemplate() { return new RestTemplate(); } + } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java b/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java index f77354b..8563f67 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java @@ -19,4 +19,5 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.build(); } + } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java new file mode 100644 index 0000000..1ec6d57 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java @@ -0,0 +1,19 @@ +package com.example.externalinfoservice.controller; + +import com.example.externalinfoservice.service.ParkService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/parking") +@RequiredArgsConstructor +public class ParkingController { + + private final ParkService parkService; + + @GetMapping(value = "/{areaId}", produces = MediaType.APPLICATION_JSON_VALUE) + public String getParking(@PathVariable String areaId) { + return parkService.getParkingData(areaId); + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java index 5b43eb9..194de63 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java @@ -15,12 +15,19 @@ public class WeatherController { private final WeatherService weatherService; - @GetMapping("/{place-code}") - public String getWeatherData(@PathVariable("place-code") String placeCode) { - return weatherService.getWeatherData(placeCode); - } + /** + * 전체 지역의 간략한 날씨 정보를 조회합니다. + */ + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getAllAreasWeatherSummary() { + String jsonData = weatherService.getAllAreasWeatherSummary(); + return ResponseEntity.ok(jsonData); + } - @GetMapping(value = "/api/{place-code}", produces = MediaType.APPLICATION_JSON_VALUE) + /** + * 지역별 상세 날씨 정보를 조회합니다. + */ + @GetMapping(value = "/{place-code}", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getWeatherDataApi(@PathVariable("place-code") String placeCode) { String jsonData = weatherService.getWeatherData(placeCode); return ResponseEntity.ok(jsonData); diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java new file mode 100644 index 0000000..1deb47b --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java @@ -0,0 +1,74 @@ +package com.example.externalinfoservice.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.xml.bind.annotation.*; +import lombok.Data; + +import java.util.List; + +@Data +@XmlRootElement(name = "SeoulRtd.citydata") +@XmlAccessorType(XmlAccessType.FIELD) +@JsonIgnoreProperties(ignoreUnknown = true) +public class ParkDto { + + @XmlElement(name = "CITYDATA") + private CityData cityData; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class CityData { + + @XmlElement(name = "PRK_STTS") + private PrkSttsWrapper prktts; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class PrkSttsWrapper { + + @XmlElement(name = "PRK_STTS") + private List prkStts; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class ParkingStatus { + + @XmlElement(name = "PRK_NM") + private String prk_nm; + + @XmlElement(name = "PRK_CD") + private String prk_cd; + + @XmlElement(name = "PRK_TYPE") + private String prk_type; + + @XmlElement(name = "CPCTY") + private int cpcty; + + @XmlElement(name = "CUR_PRK_CNT") + private String cur_prk_cnt; + + @XmlElement(name = "CUR_PRK_TIME") + private String cur_prk_time; + + @XmlElement(name = "CUR_PRK_YN") + private String cur_prk_yn; + + @XmlElement(name = "PAY_YN") + private String pay_yn; + + @XmlElement(name = "RATES") + private int rates; + + @XmlElement(name = "TIME_RATES") + private int time_rates; + + @XmlElement(name = "ADD_RATES") + private int add_rates; + + @XmlElement(name = "ADD_TIME_RATES") + private int add_time_rates; + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java index ae24bad..8918c96 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java @@ -23,19 +23,19 @@ public class RoadDto { public static class CityData { @XmlElement(name = "ROAD_TRAFFIC_STTS") @JsonProperty("road_traffic_stts") - private List roadTrafficSttsList; + private RoadTrafficSttsWrapper roadTrafficSttsWrapper; } @Data @XmlAccessorType(XmlAccessType.FIELD) - public static class RoadTrafficStts { + public static class RoadTrafficSttsWrapper { @XmlElement(name = "AVG_ROAD_DATA") @JsonProperty("avg_road_data") private AvgRoadData avgRoadData; @XmlElement(name = "ROAD_TRAFFIC_STTS") @JsonProperty("road_traffic_stts") - private RoadTrafficDetail roadTrafficDetail; + private List roadTrafficDetailList; } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/entity/Area.java b/external-info-service/src/main/java/com/example/externalinfoservice/entity/Area.java new file mode 100644 index 0000000..7cc4bc2 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/entity/Area.java @@ -0,0 +1,36 @@ +package com.example.externalinfoservice.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "area") +@Getter +@Setter +public class Area { + @Id + @Column(name = "area_id") + private Long areaId; + + @Column(name = "seoul_id") + private String seoulId; + + @Column(name = "name") + private String name; + + @Column(name = "name_eng") + private String nameEng; + + @Column(name = "category") + private String category; + + @Column(name = "lat") + private Double latitude; + + @Column(name = "lon") + private Double longitude; +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java new file mode 100644 index 0000000..9a78486 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java @@ -0,0 +1,34 @@ +package com.example.externalinfoservice.exception; + +public class CustomExceptionClass { + public static class DataNotFoundException extends RuntimeException { + public DataNotFoundException(String message) { + super(message); + } + + public DataNotFoundException(String message, Throwable cause) { + super(message, cause); + } + } + + public static class ConflictException extends RuntimeException { + public ConflictException(String message) { + super(message); + } + + public ConflictException(String message, Throwable cause) { + super(message, cause); + } + } + + public static class TooManyRequestsException extends RuntimeException { + public TooManyRequestsException(String message) { + super(message); + } + + public TooManyRequestsException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java new file mode 100644 index 0000000..a84b39c --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java @@ -0,0 +1,49 @@ +package com.example.externalinfoservice.exception; + +import org.springframework.http.HttpStatus; + +public enum ErrorCode { + // 400 - 1000 - Invalid request + INVALID_REQUEST("1000", HttpStatus.BAD_REQUEST), + DATA_NOT_FOUND("1001", HttpStatus.BAD_REQUEST), + + // 401 - 2000 - Unauthorized + UNAUTHORIZED("2000", HttpStatus.UNAUTHORIZED), + + // 403 - 2001 - Forbidden + FORBIDDEN("2001", HttpStatus.FORBIDDEN), + + // 404 - 3000 - Resource not found + RESOURCE_NOT_FOUND("3000", HttpStatus.NOT_FOUND), + + // 409 - 3001 - Conflict + CONFLICT("3001", HttpStatus.CONFLICT), + + // 422 - 3002 - Validation failed + VALIDATION_FAILED("3002", HttpStatus.UNPROCESSABLE_ENTITY), + + // 429 - 3003 - Too Many Requests + TOO_MANY_REQUESTS("3003", HttpStatus.TOO_MANY_REQUESTS), + + // 500 - 9000 - Internal server error + INTERNAL_SERVER_ERROR("9000", HttpStatus.INTERNAL_SERVER_ERROR), + + // 503 - 9001 - Service unavailable + SERVICE_UNAVAILABLE("9001", HttpStatus.SERVICE_UNAVAILABLE); + + private final String code; + private final HttpStatus status; + + ErrorCode(String code, HttpStatus status) { + this.code = code; + this.status = status; + } + + public String getCode() { + return code; + } + + public HttpStatus getStatus() { + return status; + } +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java new file mode 100644 index 0000000..276838f --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java @@ -0,0 +1,18 @@ +package com.example.externalinfoservice.exception; + +import lombok.Data; + +@Data +public class ErrorResponse { + private int status; + private String errorCode; + private String message; + + public ErrorResponse(int status, String errorCode, String message) { + this.status = status; + this.errorCode = errorCode; + this.message = message; + } + + +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..ef70e57 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java @@ -0,0 +1,87 @@ +package com.example.externalinfoservice.exception; + +import org.apache.kafka.common.errors.InvalidRequestException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import javax.naming.ServiceUnavailableException; +import javax.xml.bind.ValidationException; + +@RestControllerAdvice +public class GlobalExceptionHandler { + // Spring에서 제공하는 기본 예외 클래스 + + // 400 - 1000 - InvalidRequestException + @ExceptionHandler(InvalidRequestException.class) + public ResponseEntity handleInvalidRequestException(InvalidRequestException ex) { + return buildErrorResponse( ErrorCode.INVALID_REQUEST, ex.getMessage()); + } + // 401 - 2000 - Unauthorized + @ExceptionHandler(HttpClientErrorException.Unauthorized.class) + public ResponseEntity handleUnauthorizedException(HttpClientErrorException.Unauthorized ex) { + return buildErrorResponse( ErrorCode.UNAUTHORIZED, ex.getMessage()); + } + // 403 - 2001 - Forbidden + @ExceptionHandler(HttpClientErrorException.Forbidden.class) + public ResponseEntity handleForbiddenException(HttpClientErrorException.Forbidden ex) { + return buildErrorResponse(ErrorCode.FORBIDDEN, ex.getMessage()); + } + // 404 - 3000 - Resource not found + @ExceptionHandler(HttpClientErrorException.NotFound.class) + public ResponseEntity handleNotFoundException(HttpClientErrorException.NotFound ex) { + return buildErrorResponse(ErrorCode.RESOURCE_NOT_FOUND, ex.getMessage()); + } + + // 커스텀 예외 클래스 + // 400 - 1001 - DataNotFoundException + @ExceptionHandler(CustomExceptionClass.DataNotFoundException.class) + public ResponseEntity handleDataNotFoundException(CustomExceptionClass.DataNotFoundException ex) { + return buildErrorResponse(ErrorCode.DATA_NOT_FOUND, ex.getMessage()); + } + + // 409 - 3001 - Conflict + @ExceptionHandler(CustomExceptionClass.ConflictException.class) // 커스텀 예외 + public ResponseEntity handleConflictException(CustomExceptionClass.ConflictException ex) { + return buildErrorResponse(ErrorCode.CONFLICT, ex.getMessage()); + } + + // 422 - 3002 - Validation failed + @ExceptionHandler(ValidationException.class) // javax.validation 또는 커스텀 가능 + public ResponseEntity handleValidationException(ValidationException ex) { + return buildErrorResponse(ErrorCode.VALIDATION_FAILED, ex.getMessage()); + } + + // 429 - 3003 - Too Many Requests + @ExceptionHandler(CustomExceptionClass.TooManyRequestsException.class) + public ResponseEntity handleTooManyRequestsException(CustomExceptionClass.TooManyRequestsException ex) { + return buildErrorResponse(ErrorCode.TOO_MANY_REQUESTS, ex.getMessage()); + } + + // 500 - 9000 - Internal server error + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleInternalServerError(Exception ex) { + return buildErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage()); + } + + // 503 - 9001 - Service unavailable + @ExceptionHandler(ServiceUnavailableException.class) // 커스텀 예외 + public ResponseEntity handleServiceUnavailableException(ServiceUnavailableException ex) { + return buildErrorResponse(ErrorCode.SERVICE_UNAVAILABLE, ex.getMessage()); + } + + + // === 공통 응답 생성 메서드 === + private ResponseEntity buildErrorResponse(ErrorCode errorCode, String message) { + return new ResponseEntity<>( + new ErrorResponse(errorCode.getStatus().value(), errorCode.getCode(), message), + errorCode.getStatus() + ); + } + + + + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/repository/AreaRepository.java b/external-info-service/src/main/java/com/example/externalinfoservice/repository/AreaRepository.java new file mode 100644 index 0000000..db3db0c --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/repository/AreaRepository.java @@ -0,0 +1,12 @@ +package com.example.externalinfoservice.repository; + +import com.example.externalinfoservice.entity.Area; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AreaRepository extends JpaRepository { + Optional findBySeoulId(String seoulId); +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java new file mode 100644 index 0000000..8507da0 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java @@ -0,0 +1,52 @@ +package com.example.externalinfoservice.service; + +import com.example.externalinfoservice.dto.ParkDto; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.Unmarshaller; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.io.StringReader; + +@Service +public class ParkService { + + @Value("${seoul.api.key}") + private String apiKey; + + private final RestTemplate restTemplate; + + public ParkService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public String getParkingData(String areaId) { + try { + String url = String.format( + "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", + apiKey, areaId + ); + + // XML 요청 + String xmlResponse = restTemplate.getForObject(url, String.class); + + // XML -> DTO + ParkDto parkDto = xmlToDto(xmlResponse); + + // DTO -> JSON + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(parkDto); + + } catch (Exception e) { + throw new RuntimeException("주차장 데이터 파싱 실패: " + e.getMessage(), e); + } + } + + private ParkDto xmlToDto(String xml) throws Exception { + JAXBContext context = JAXBContext.newInstance(ParkDto.class); + Unmarshaller unmarshaller = context.createUnmarshaller(); + return (ParkDto) unmarshaller.unmarshal(new StringReader(xml)); + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java index 40ea77f..2415816 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java @@ -3,12 +3,13 @@ import com.example.externalinfoservice.dto.RoadDto; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Unmarshaller; +import org.apache.kafka.common.errors.InvalidRequestException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import java.io.IOException; import java.io.StringReader; @Service @@ -24,18 +25,21 @@ public RoadService(RestTemplate restTemplate) { } public String getTrafficData(String id) { - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String xmlResponse = restTemplate.getForObject(openApiUrl + id, String.class); - - // 2. XML 데이터를 DTO로 변환 try { - // DTO -> JSON + // 1. 외부 OpenAPI에서 XML 데이터 받아오기 + String xmlResponse = restTemplate.getForObject(openApiUrl + id, String.class); + + // 2. XML 데이터를 DTO로 변환 -> JSON ObjectMapper objectMapper = new ObjectMapper(); String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); return json; + } catch (InvalidRequestException e) { + throw new InvalidRequestException(e.getMessage(), e); + } catch(IOException e){ + throw new RuntimeException("외부 API 오류",e); } catch (Exception e) { - throw new RuntimeException("XML 파싱 실패", e); + throw new RuntimeException("예상치 못한 오류",e); } } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java index 7686729..bd0d6c0 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java @@ -1,87 +1,109 @@ package com.example.externalinfoservice.service; +import com.example.externalinfoservice.entity.Area; +import com.example.externalinfoservice.repository.AreaRepository; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; + import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.ByteArrayInputStream; -import java.util.Set; -import java.util.HashSet; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; @Service public class WeatherService { - private final String weatherApiUrl; + @Value("${seoul.api.key}") + private String apiKey; + private final RestTemplate restTemplate; private final ObjectMapper objectMapper; + private final AreaRepository areaRepository; - // 날씨 관련 필드 목록 - private static final Set WEATHER_FIELDS = new HashSet<>() {{ - add("TEMP"); // 기온 - add("PRECIPITATION"); // 강수량 - add("PRECPT_TYPE"); // 강수 형태 - add("PM10"); // 미세먼지 - add("FCST24HOURS"); // 24시간 예보 - add("SKY_STTS"); // 하늘 상태 + // 날씨 상태별 아이콘 URL 매핑 + private static final Map WEATHER_ICONS = new HashMap<>() {{ + put("구름 조금", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_halfsun.png"); + put("비", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_rain.png"); + put("눈", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_snow.png"); + put("구름 많음", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_cloudy.png"); + put("맑음", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sunny.png"); }}; - public WeatherService(RestTemplate restTemplate, ObjectMapper objectMapper) { - this.weatherApiUrl = "http://openapi.seoul.go.kr:8088/594b4a6559796f683930466466666d/xml/citydata/1/5/"; + public WeatherService(RestTemplate restTemplate, + ObjectMapper objectMapper, + AreaRepository areaRepository) { this.restTemplate = restTemplate; this.objectMapper = objectMapper; + this.areaRepository = areaRepository; } + /** + * 특정 지역의 상세 날씨 정보를 제공합니다. + */ public String getWeatherData(String placeCode) { - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String xmlResponse = restTemplate.getForObject(weatherApiUrl + placeCode, String.class); + try { + // place-service 데이터베이스에서 지역 정보 조회 + Area area = areaRepository.findBySeoulId(placeCode) + .orElseThrow(() -> new RuntimeException("해당 지역 코드를 찾을 수 없습니다: " + placeCode)); - // 전체 XML 응답 출력 - System.out.println("XML Response: " + xmlResponse); + // 1. 외부 OpenAPI에서 XML 데이터 받아오기 + String apiUrl = String.format( + "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", + apiKey, placeCode + ); + String xmlResponse = restTemplate.getForObject(apiUrl, String.class); - try { // XML 파싱을 위한 DocumentBuilder 생성 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes())); - // 모든 노드 이름 출력 - NodeList allNodes = document.getElementsByTagName("*"); - System.out.println("Total nodes: " + allNodes.getLength()); - for (int i = 0; i < allNodes.getLength(); i++) { - System.out.println("Node " + i + ": " + allNodes.item(i).getNodeName()); - } - // JSON 객체 생성 ObjectNode weatherJson = objectMapper.createObjectNode(); ObjectNode weatherSttsJson = objectMapper.createObjectNode(); + // 지역 추가 정보 + weatherJson.put("areaName", area.getName()); + weatherJson.put("areaNameEng", area.getNameEng()); + weatherJson.put("category", area.getCategory()); + + // 위도, 경도 정보 추가 (선택적) + if (area.getLatitude() != null && area.getLongitude() != null) { + ObjectNode locationJson = objectMapper.createObjectNode(); + locationJson.put("latitude", area.getLatitude()); + locationJson.put("longitude", area.getLongitude()); + weatherJson.set("location", locationJson); + } + // 원하는 필드들 String[] desiredFields = { "TEMP", "PRECIPITATION", "PRECPT_TYPE", "PM10", "FCST24HOURS", "SKY_STTS" }; - // 모든 WEATHER_STTS 하위 노드 탐색 - NodeList weatherSttsNodes = document.getElementsByTagName("WEATHER_STTS"); - System.out.println("WEATHER_STTS nodes count: " + weatherSttsNodes.getLength()); + // SKY_STTS 값을 저장할 변수 + String skyStatus = ""; // 각 필드별로 특별 처리 for (String field : desiredFields) { NodeList fieldNodes = document.getElementsByTagName(field); - System.out.println(field + " nodes count: " + fieldNodes.getLength()); // 각 필드에 대해 값 추출 if (fieldNodes.getLength() > 0) { Node fieldNode = fieldNodes.item(0); String nodeValue = fieldNode.getTextContent().trim(); - System.out.println(field + " value: " + nodeValue); if (field.equals("FCST24HOURS")) { // FCST24HOURS는 배열로 처리 @@ -115,21 +137,27 @@ public String getWeatherData(String placeCode) { } weatherSttsJson.set(convertToCamelCase(field), forecastArray); + } else if (field.equals("SKY_STTS")) { + // SKY_STTS 값 저장 + skyStatus = nodeValue; + weatherSttsJson.put(convertToCamelCase(field), nodeValue); } else if (!nodeValue.isEmpty()) { weatherSttsJson.put(convertToCamelCase(field), nodeValue); } } } + // 날씨 상태(SKY_STTS)에 따라 아이콘 URL 매핑 + String iconUrl = WEATHER_ICONS.getOrDefault(skyStatus, "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); + + // 매핑된 아이콘 URL 추가 + weatherSttsJson.put("weatherIcon", iconUrl); + // weatherStts 필드에 추출된 정보 저장 weatherJson.set("weatherStts", weatherSttsJson); - // JSON을 예쁘게 출력 (들여쓰기 2칸) - String jsonString = objectMapper.writerWithDefaultPrettyPrinter() - .writeValueAsString(weatherJson); - // JSON 문자열에 줄바꿈 문자가 제대로 유지되는지 확인 - System.out.println("Formatted JSON: " + jsonString); - return jsonString; + // JSON 변환 및 반환 + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(weatherJson); } catch (Exception e) { e.printStackTrace(); // 전체 스택 트레이스 출력 @@ -137,6 +165,119 @@ public String getWeatherData(String placeCode) { } } + /** + * 모든 지역의 날씨 요약 정보를 JSON 형태로 반환합니다. + */ + public String getAllAreasWeatherSummary() { + try { + // 병렬 처리를 위한 스레드 풀 생성 + ExecutorService executor = Executors.newFixedThreadPool(5); + + // 결과를 담을 객체 생성 + ObjectNode resultJson = objectMapper.createObjectNode(); + resultJson.put("title", "서울시 주요 지역 날씨 정보"); + + // 전체 지역 날씨 정보를 담을 배열 생성 + ArrayNode areasWeather = objectMapper.createArrayNode(); + + // place-service 데이터베이스에서 모든 지역 정보 조회 + List areas = areaRepository.findAll(); + + // 모든 지역에 대해 CompletableFuture를 생성하여 병렬로 API 호출 + List> futures = areas.stream() + .map(area -> CompletableFuture.supplyAsync( + () -> getAreaWeatherSummary(area.getName(), area.getSeoulId()), + executor + )) + .collect(Collectors.toList()); + + // 모든 CompletableFuture가 완료될 때까지 기다리고 결과 수집 + CompletableFuture allFutures = CompletableFuture.allOf( + futures.toArray(new CompletableFuture[0])); + + // 결과 수집 및 JSON 배열에 추가 + allFutures.thenAccept(v -> + futures.forEach(future -> { + try { + ObjectNode node = future.get(); + areasWeather.add(node); + } catch (Exception e) { + // 개별 결과 처리 중 오류 발생 시 무시하고 계속 진행 + } + }) + ).get(); + + // 스레드 풀 종료 + executor.shutdown(); + + // 결과 JSON에 지역 날씨 정보 배열 추가 + resultJson.set("data", areasWeather); + + // JSON 변환 및 반환 + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(resultJson); + + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("지역별 날씨 정보 조회 실패", e); + } + } + + /** + * 지역의 간략한 날씨 정보를 추출합니다. + */ + private ObjectNode getAreaWeatherSummary(String areaName, String areaCode) { + try { + // 날씨 정보 조회 API 호출 + String apiUrl = String.format( + "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", + apiKey, areaCode + ); + String xmlResponse = restTemplate.getForObject(apiUrl, String.class); + + // XML 파싱 + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes())); + + // 필요한 정보 추출 + String temperature = getNodeValueByTagName(document, "TEMP"); + String skyStatus = getNodeValueByTagName(document, "SKY_STTS"); + String precipitationType = getNodeValueByTagName(document, "PRECPT_TYPE"); + + // 날씨 아이콘 매핑 + String iconUrl = WEATHER_ICONS.getOrDefault(skyStatus, + "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); + + // JSON 객체 생성 (간소화된 형식) + ObjectNode areaWeather = objectMapper.createObjectNode(); + areaWeather.put("name", areaName); + areaWeather.put("TEMP", temperature); + areaWeather.put("PRECPT_TYPE", precipitationType); + areaWeather.put("weatherIcon", iconUrl); + + return areaWeather; + } catch (Exception e) { + // 오류 발생 시 기본 정보 반환 + ObjectNode defaultNode = objectMapper.createObjectNode(); + defaultNode.put("name", areaName); + defaultNode.put("TEMP", "N/A"); + defaultNode.put("PRECPT_TYPE", "정보 없음"); + defaultNode.put("weatherIcon", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); + return defaultNode; + } + } + + /** + * 문서에서 태그명으로 노드 값을 가져옵니다. + */ + private String getNodeValueByTagName(Document document, String tagName) { + NodeList nodes = document.getElementsByTagName(tagName); + if (nodes.getLength() > 0) { + return nodes.item(0).getTextContent().trim(); + } + return ""; + } + // 언더스코어 이름을 camelCase로 변환 private String convertToCamelCase(String input) { if (input == null || input.isEmpty()) return input; From dc71db9e9028200b1e0056fcba182a3dbb26ba6b Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Thu, 24 Apr 2025 09:36:15 +0900 Subject: [PATCH 093/361] =?UTF-8?q?Feat:=EB=82=A0=EC=94=A8=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- external-info-service/build.gradle | 13 ++ .../externalinfoservice/config/AppConfig.java | 1 + .../config/SecurityConfig.java | 1 + .../controller/ParkingController.java | 19 ++ .../controller/WeatherController.java | 17 +- .../externalinfoservice/dto/ParkDto.java | 74 ++++++ .../externalinfoservice/dto/RoadDto.java | 6 +- .../externalinfoservice/entity/Area.java | 36 +++ .../exception/CustomExceptionClass.java | 34 +++ .../exception/ErrorCode.java | 49 ++++ .../exception/ErrorResponse.java | 18 ++ .../exception/GlobalExceptionHandler.java | 87 +++++++ .../repository/AreaRepository.java | 12 + .../service/ParkService.java | 52 +++++ .../service/RoadService.java | 18 +- .../service/WeatherService.java | 213 +++++++++++++++--- .../src/main/resources/application.properties | 8 +- 17 files changed, 604 insertions(+), 54 deletions(-) create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/entity/Area.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/repository/AreaRepository.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java diff --git a/external-info-service/build.gradle b/external-info-service/build.gradle index 3b8f4df..3be0dd8 100644 --- a/external-info-service/build.gradle +++ b/external-info-service/build.gradle @@ -34,6 +34,19 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-configuration-processor' + implementation 'org.springframework.boot:spring-boot-starter-json' + implementation 'org.springframework.boot:spring-boot-starter-security' + + implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0' + implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.2' + + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' + + implementation 'org.postgresql:postgresql:42.7.2' implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java b/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java index 8e423a6..8225d9c 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/config/AppConfig.java @@ -10,4 +10,5 @@ public class AppConfig { public RestTemplate restTemplate() { return new RestTemplate(); } + } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java b/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java index f77354b..8563f67 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/config/SecurityConfig.java @@ -19,4 +19,5 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.build(); } + } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java new file mode 100644 index 0000000..1ec6d57 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingController.java @@ -0,0 +1,19 @@ +package com.example.externalinfoservice.controller; + +import com.example.externalinfoservice.service.ParkService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/parking") +@RequiredArgsConstructor +public class ParkingController { + + private final ParkService parkService; + + @GetMapping(value = "/{areaId}", produces = MediaType.APPLICATION_JSON_VALUE) + public String getParking(@PathVariable String areaId) { + return parkService.getParkingData(areaId); + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java index 5b43eb9..194de63 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java @@ -15,12 +15,19 @@ public class WeatherController { private final WeatherService weatherService; - @GetMapping("/{place-code}") - public String getWeatherData(@PathVariable("place-code") String placeCode) { - return weatherService.getWeatherData(placeCode); - } + /** + * 전체 지역의 간략한 날씨 정보를 조회합니다. + */ + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getAllAreasWeatherSummary() { + String jsonData = weatherService.getAllAreasWeatherSummary(); + return ResponseEntity.ok(jsonData); + } - @GetMapping(value = "/api/{place-code}", produces = MediaType.APPLICATION_JSON_VALUE) + /** + * 지역별 상세 날씨 정보를 조회합니다. + */ + @GetMapping(value = "/{place-code}", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity getWeatherDataApi(@PathVariable("place-code") String placeCode) { String jsonData = weatherService.getWeatherData(placeCode); return ResponseEntity.ok(jsonData); diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java new file mode 100644 index 0000000..1deb47b --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java @@ -0,0 +1,74 @@ +package com.example.externalinfoservice.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.xml.bind.annotation.*; +import lombok.Data; + +import java.util.List; + +@Data +@XmlRootElement(name = "SeoulRtd.citydata") +@XmlAccessorType(XmlAccessType.FIELD) +@JsonIgnoreProperties(ignoreUnknown = true) +public class ParkDto { + + @XmlElement(name = "CITYDATA") + private CityData cityData; + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class CityData { + + @XmlElement(name = "PRK_STTS") + private PrkSttsWrapper prktts; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class PrkSttsWrapper { + + @XmlElement(name = "PRK_STTS") + private List prkStts; + } + + @Data + @XmlAccessorType(XmlAccessType.FIELD) + public static class ParkingStatus { + + @XmlElement(name = "PRK_NM") + private String prk_nm; + + @XmlElement(name = "PRK_CD") + private String prk_cd; + + @XmlElement(name = "PRK_TYPE") + private String prk_type; + + @XmlElement(name = "CPCTY") + private int cpcty; + + @XmlElement(name = "CUR_PRK_CNT") + private String cur_prk_cnt; + + @XmlElement(name = "CUR_PRK_TIME") + private String cur_prk_time; + + @XmlElement(name = "CUR_PRK_YN") + private String cur_prk_yn; + + @XmlElement(name = "PAY_YN") + private String pay_yn; + + @XmlElement(name = "RATES") + private int rates; + + @XmlElement(name = "TIME_RATES") + private int time_rates; + + @XmlElement(name = "ADD_RATES") + private int add_rates; + + @XmlElement(name = "ADD_TIME_RATES") + private int add_time_rates; + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java index ae24bad..8918c96 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/RoadDto.java @@ -23,19 +23,19 @@ public class RoadDto { public static class CityData { @XmlElement(name = "ROAD_TRAFFIC_STTS") @JsonProperty("road_traffic_stts") - private List roadTrafficSttsList; + private RoadTrafficSttsWrapper roadTrafficSttsWrapper; } @Data @XmlAccessorType(XmlAccessType.FIELD) - public static class RoadTrafficStts { + public static class RoadTrafficSttsWrapper { @XmlElement(name = "AVG_ROAD_DATA") @JsonProperty("avg_road_data") private AvgRoadData avgRoadData; @XmlElement(name = "ROAD_TRAFFIC_STTS") @JsonProperty("road_traffic_stts") - private RoadTrafficDetail roadTrafficDetail; + private List roadTrafficDetailList; } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/entity/Area.java b/external-info-service/src/main/java/com/example/externalinfoservice/entity/Area.java new file mode 100644 index 0000000..7cc4bc2 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/entity/Area.java @@ -0,0 +1,36 @@ +package com.example.externalinfoservice.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Table(name = "area") +@Getter +@Setter +public class Area { + @Id + @Column(name = "area_id") + private Long areaId; + + @Column(name = "seoul_id") + private String seoulId; + + @Column(name = "name") + private String name; + + @Column(name = "name_eng") + private String nameEng; + + @Column(name = "category") + private String category; + + @Column(name = "lat") + private Double latitude; + + @Column(name = "lon") + private Double longitude; +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java new file mode 100644 index 0000000..9a78486 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java @@ -0,0 +1,34 @@ +package com.example.externalinfoservice.exception; + +public class CustomExceptionClass { + public static class DataNotFoundException extends RuntimeException { + public DataNotFoundException(String message) { + super(message); + } + + public DataNotFoundException(String message, Throwable cause) { + super(message, cause); + } + } + + public static class ConflictException extends RuntimeException { + public ConflictException(String message) { + super(message); + } + + public ConflictException(String message, Throwable cause) { + super(message, cause); + } + } + + public static class TooManyRequestsException extends RuntimeException { + public TooManyRequestsException(String message) { + super(message); + } + + public TooManyRequestsException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java new file mode 100644 index 0000000..a84b39c --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java @@ -0,0 +1,49 @@ +package com.example.externalinfoservice.exception; + +import org.springframework.http.HttpStatus; + +public enum ErrorCode { + // 400 - 1000 - Invalid request + INVALID_REQUEST("1000", HttpStatus.BAD_REQUEST), + DATA_NOT_FOUND("1001", HttpStatus.BAD_REQUEST), + + // 401 - 2000 - Unauthorized + UNAUTHORIZED("2000", HttpStatus.UNAUTHORIZED), + + // 403 - 2001 - Forbidden + FORBIDDEN("2001", HttpStatus.FORBIDDEN), + + // 404 - 3000 - Resource not found + RESOURCE_NOT_FOUND("3000", HttpStatus.NOT_FOUND), + + // 409 - 3001 - Conflict + CONFLICT("3001", HttpStatus.CONFLICT), + + // 422 - 3002 - Validation failed + VALIDATION_FAILED("3002", HttpStatus.UNPROCESSABLE_ENTITY), + + // 429 - 3003 - Too Many Requests + TOO_MANY_REQUESTS("3003", HttpStatus.TOO_MANY_REQUESTS), + + // 500 - 9000 - Internal server error + INTERNAL_SERVER_ERROR("9000", HttpStatus.INTERNAL_SERVER_ERROR), + + // 503 - 9001 - Service unavailable + SERVICE_UNAVAILABLE("9001", HttpStatus.SERVICE_UNAVAILABLE); + + private final String code; + private final HttpStatus status; + + ErrorCode(String code, HttpStatus status) { + this.code = code; + this.status = status; + } + + public String getCode() { + return code; + } + + public HttpStatus getStatus() { + return status; + } +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java new file mode 100644 index 0000000..276838f --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java @@ -0,0 +1,18 @@ +package com.example.externalinfoservice.exception; + +import lombok.Data; + +@Data +public class ErrorResponse { + private int status; + private String errorCode; + private String message; + + public ErrorResponse(int status, String errorCode, String message) { + this.status = status; + this.errorCode = errorCode; + this.message = message; + } + + +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..ef70e57 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java @@ -0,0 +1,87 @@ +package com.example.externalinfoservice.exception; + +import org.apache.kafka.common.errors.InvalidRequestException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import javax.naming.ServiceUnavailableException; +import javax.xml.bind.ValidationException; + +@RestControllerAdvice +public class GlobalExceptionHandler { + // Spring에서 제공하는 기본 예외 클래스 + + // 400 - 1000 - InvalidRequestException + @ExceptionHandler(InvalidRequestException.class) + public ResponseEntity handleInvalidRequestException(InvalidRequestException ex) { + return buildErrorResponse( ErrorCode.INVALID_REQUEST, ex.getMessage()); + } + // 401 - 2000 - Unauthorized + @ExceptionHandler(HttpClientErrorException.Unauthorized.class) + public ResponseEntity handleUnauthorizedException(HttpClientErrorException.Unauthorized ex) { + return buildErrorResponse( ErrorCode.UNAUTHORIZED, ex.getMessage()); + } + // 403 - 2001 - Forbidden + @ExceptionHandler(HttpClientErrorException.Forbidden.class) + public ResponseEntity handleForbiddenException(HttpClientErrorException.Forbidden ex) { + return buildErrorResponse(ErrorCode.FORBIDDEN, ex.getMessage()); + } + // 404 - 3000 - Resource not found + @ExceptionHandler(HttpClientErrorException.NotFound.class) + public ResponseEntity handleNotFoundException(HttpClientErrorException.NotFound ex) { + return buildErrorResponse(ErrorCode.RESOURCE_NOT_FOUND, ex.getMessage()); + } + + // 커스텀 예외 클래스 + // 400 - 1001 - DataNotFoundException + @ExceptionHandler(CustomExceptionClass.DataNotFoundException.class) + public ResponseEntity handleDataNotFoundException(CustomExceptionClass.DataNotFoundException ex) { + return buildErrorResponse(ErrorCode.DATA_NOT_FOUND, ex.getMessage()); + } + + // 409 - 3001 - Conflict + @ExceptionHandler(CustomExceptionClass.ConflictException.class) // 커스텀 예외 + public ResponseEntity handleConflictException(CustomExceptionClass.ConflictException ex) { + return buildErrorResponse(ErrorCode.CONFLICT, ex.getMessage()); + } + + // 422 - 3002 - Validation failed + @ExceptionHandler(ValidationException.class) // javax.validation 또는 커스텀 가능 + public ResponseEntity handleValidationException(ValidationException ex) { + return buildErrorResponse(ErrorCode.VALIDATION_FAILED, ex.getMessage()); + } + + // 429 - 3003 - Too Many Requests + @ExceptionHandler(CustomExceptionClass.TooManyRequestsException.class) + public ResponseEntity handleTooManyRequestsException(CustomExceptionClass.TooManyRequestsException ex) { + return buildErrorResponse(ErrorCode.TOO_MANY_REQUESTS, ex.getMessage()); + } + + // 500 - 9000 - Internal server error + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleInternalServerError(Exception ex) { + return buildErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage()); + } + + // 503 - 9001 - Service unavailable + @ExceptionHandler(ServiceUnavailableException.class) // 커스텀 예외 + public ResponseEntity handleServiceUnavailableException(ServiceUnavailableException ex) { + return buildErrorResponse(ErrorCode.SERVICE_UNAVAILABLE, ex.getMessage()); + } + + + // === 공통 응답 생성 메서드 === + private ResponseEntity buildErrorResponse(ErrorCode errorCode, String message) { + return new ResponseEntity<>( + new ErrorResponse(errorCode.getStatus().value(), errorCode.getCode(), message), + errorCode.getStatus() + ); + } + + + + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/repository/AreaRepository.java b/external-info-service/src/main/java/com/example/externalinfoservice/repository/AreaRepository.java new file mode 100644 index 0000000..db3db0c --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/repository/AreaRepository.java @@ -0,0 +1,12 @@ +package com.example.externalinfoservice.repository; + +import com.example.externalinfoservice.entity.Area; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AreaRepository extends JpaRepository { + Optional findBySeoulId(String seoulId); +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java new file mode 100644 index 0000000..8507da0 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkService.java @@ -0,0 +1,52 @@ +package com.example.externalinfoservice.service; + +import com.example.externalinfoservice.dto.ParkDto; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.Unmarshaller; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.io.StringReader; + +@Service +public class ParkService { + + @Value("${seoul.api.key}") + private String apiKey; + + private final RestTemplate restTemplate; + + public ParkService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public String getParkingData(String areaId) { + try { + String url = String.format( + "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", + apiKey, areaId + ); + + // XML 요청 + String xmlResponse = restTemplate.getForObject(url, String.class); + + // XML -> DTO + ParkDto parkDto = xmlToDto(xmlResponse); + + // DTO -> JSON + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(parkDto); + + } catch (Exception e) { + throw new RuntimeException("주차장 데이터 파싱 실패: " + e.getMessage(), e); + } + } + + private ParkDto xmlToDto(String xml) throws Exception { + JAXBContext context = JAXBContext.newInstance(ParkDto.class); + Unmarshaller unmarshaller = context.createUnmarshaller(); + return (ParkDto) unmarshaller.unmarshal(new StringReader(xml)); + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java index 40ea77f..2415816 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/RoadService.java @@ -3,12 +3,13 @@ import com.example.externalinfoservice.dto.RoadDto; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Unmarshaller; +import org.apache.kafka.common.errors.InvalidRequestException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import java.io.IOException; import java.io.StringReader; @Service @@ -24,18 +25,21 @@ public RoadService(RestTemplate restTemplate) { } public String getTrafficData(String id) { - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String xmlResponse = restTemplate.getForObject(openApiUrl + id, String.class); - - // 2. XML 데이터를 DTO로 변환 try { - // DTO -> JSON + // 1. 외부 OpenAPI에서 XML 데이터 받아오기 + String xmlResponse = restTemplate.getForObject(openApiUrl + id, String.class); + + // 2. XML 데이터를 DTO로 변환 -> JSON ObjectMapper objectMapper = new ObjectMapper(); String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(xmlToDto(xmlResponse)); return json; + } catch (InvalidRequestException e) { + throw new InvalidRequestException(e.getMessage(), e); + } catch(IOException e){ + throw new RuntimeException("외부 API 오류",e); } catch (Exception e) { - throw new RuntimeException("XML 파싱 실패", e); + throw new RuntimeException("예상치 못한 오류",e); } } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java index 7686729..bd0d6c0 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java @@ -1,87 +1,109 @@ package com.example.externalinfoservice.service; +import com.example.externalinfoservice.entity.Area; +import com.example.externalinfoservice.repository.AreaRepository; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; + import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.ByteArrayInputStream; -import java.util.Set; -import java.util.HashSet; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; @Service public class WeatherService { - private final String weatherApiUrl; + @Value("${seoul.api.key}") + private String apiKey; + private final RestTemplate restTemplate; private final ObjectMapper objectMapper; + private final AreaRepository areaRepository; - // 날씨 관련 필드 목록 - private static final Set WEATHER_FIELDS = new HashSet<>() {{ - add("TEMP"); // 기온 - add("PRECIPITATION"); // 강수량 - add("PRECPT_TYPE"); // 강수 형태 - add("PM10"); // 미세먼지 - add("FCST24HOURS"); // 24시간 예보 - add("SKY_STTS"); // 하늘 상태 + // 날씨 상태별 아이콘 URL 매핑 + private static final Map WEATHER_ICONS = new HashMap<>() {{ + put("구름 조금", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_halfsun.png"); + put("비", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_rain.png"); + put("눈", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_snow.png"); + put("구름 많음", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_cloudy.png"); + put("맑음", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sunny.png"); }}; - public WeatherService(RestTemplate restTemplate, ObjectMapper objectMapper) { - this.weatherApiUrl = "http://openapi.seoul.go.kr:8088/594b4a6559796f683930466466666d/xml/citydata/1/5/"; + public WeatherService(RestTemplate restTemplate, + ObjectMapper objectMapper, + AreaRepository areaRepository) { this.restTemplate = restTemplate; this.objectMapper = objectMapper; + this.areaRepository = areaRepository; } + /** + * 특정 지역의 상세 날씨 정보를 제공합니다. + */ public String getWeatherData(String placeCode) { - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String xmlResponse = restTemplate.getForObject(weatherApiUrl + placeCode, String.class); + try { + // place-service 데이터베이스에서 지역 정보 조회 + Area area = areaRepository.findBySeoulId(placeCode) + .orElseThrow(() -> new RuntimeException("해당 지역 코드를 찾을 수 없습니다: " + placeCode)); - // 전체 XML 응답 출력 - System.out.println("XML Response: " + xmlResponse); + // 1. 외부 OpenAPI에서 XML 데이터 받아오기 + String apiUrl = String.format( + "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", + apiKey, placeCode + ); + String xmlResponse = restTemplate.getForObject(apiUrl, String.class); - try { // XML 파싱을 위한 DocumentBuilder 생성 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes())); - // 모든 노드 이름 출력 - NodeList allNodes = document.getElementsByTagName("*"); - System.out.println("Total nodes: " + allNodes.getLength()); - for (int i = 0; i < allNodes.getLength(); i++) { - System.out.println("Node " + i + ": " + allNodes.item(i).getNodeName()); - } - // JSON 객체 생성 ObjectNode weatherJson = objectMapper.createObjectNode(); ObjectNode weatherSttsJson = objectMapper.createObjectNode(); + // 지역 추가 정보 + weatherJson.put("areaName", area.getName()); + weatherJson.put("areaNameEng", area.getNameEng()); + weatherJson.put("category", area.getCategory()); + + // 위도, 경도 정보 추가 (선택적) + if (area.getLatitude() != null && area.getLongitude() != null) { + ObjectNode locationJson = objectMapper.createObjectNode(); + locationJson.put("latitude", area.getLatitude()); + locationJson.put("longitude", area.getLongitude()); + weatherJson.set("location", locationJson); + } + // 원하는 필드들 String[] desiredFields = { "TEMP", "PRECIPITATION", "PRECPT_TYPE", "PM10", "FCST24HOURS", "SKY_STTS" }; - // 모든 WEATHER_STTS 하위 노드 탐색 - NodeList weatherSttsNodes = document.getElementsByTagName("WEATHER_STTS"); - System.out.println("WEATHER_STTS nodes count: " + weatherSttsNodes.getLength()); + // SKY_STTS 값을 저장할 변수 + String skyStatus = ""; // 각 필드별로 특별 처리 for (String field : desiredFields) { NodeList fieldNodes = document.getElementsByTagName(field); - System.out.println(field + " nodes count: " + fieldNodes.getLength()); // 각 필드에 대해 값 추출 if (fieldNodes.getLength() > 0) { Node fieldNode = fieldNodes.item(0); String nodeValue = fieldNode.getTextContent().trim(); - System.out.println(field + " value: " + nodeValue); if (field.equals("FCST24HOURS")) { // FCST24HOURS는 배열로 처리 @@ -115,21 +137,27 @@ public String getWeatherData(String placeCode) { } weatherSttsJson.set(convertToCamelCase(field), forecastArray); + } else if (field.equals("SKY_STTS")) { + // SKY_STTS 값 저장 + skyStatus = nodeValue; + weatherSttsJson.put(convertToCamelCase(field), nodeValue); } else if (!nodeValue.isEmpty()) { weatherSttsJson.put(convertToCamelCase(field), nodeValue); } } } + // 날씨 상태(SKY_STTS)에 따라 아이콘 URL 매핑 + String iconUrl = WEATHER_ICONS.getOrDefault(skyStatus, "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); + + // 매핑된 아이콘 URL 추가 + weatherSttsJson.put("weatherIcon", iconUrl); + // weatherStts 필드에 추출된 정보 저장 weatherJson.set("weatherStts", weatherSttsJson); - // JSON을 예쁘게 출력 (들여쓰기 2칸) - String jsonString = objectMapper.writerWithDefaultPrettyPrinter() - .writeValueAsString(weatherJson); - // JSON 문자열에 줄바꿈 문자가 제대로 유지되는지 확인 - System.out.println("Formatted JSON: " + jsonString); - return jsonString; + // JSON 변환 및 반환 + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(weatherJson); } catch (Exception e) { e.printStackTrace(); // 전체 스택 트레이스 출력 @@ -137,6 +165,119 @@ public String getWeatherData(String placeCode) { } } + /** + * 모든 지역의 날씨 요약 정보를 JSON 형태로 반환합니다. + */ + public String getAllAreasWeatherSummary() { + try { + // 병렬 처리를 위한 스레드 풀 생성 + ExecutorService executor = Executors.newFixedThreadPool(5); + + // 결과를 담을 객체 생성 + ObjectNode resultJson = objectMapper.createObjectNode(); + resultJson.put("title", "서울시 주요 지역 날씨 정보"); + + // 전체 지역 날씨 정보를 담을 배열 생성 + ArrayNode areasWeather = objectMapper.createArrayNode(); + + // place-service 데이터베이스에서 모든 지역 정보 조회 + List areas = areaRepository.findAll(); + + // 모든 지역에 대해 CompletableFuture를 생성하여 병렬로 API 호출 + List> futures = areas.stream() + .map(area -> CompletableFuture.supplyAsync( + () -> getAreaWeatherSummary(area.getName(), area.getSeoulId()), + executor + )) + .collect(Collectors.toList()); + + // 모든 CompletableFuture가 완료될 때까지 기다리고 결과 수집 + CompletableFuture allFutures = CompletableFuture.allOf( + futures.toArray(new CompletableFuture[0])); + + // 결과 수집 및 JSON 배열에 추가 + allFutures.thenAccept(v -> + futures.forEach(future -> { + try { + ObjectNode node = future.get(); + areasWeather.add(node); + } catch (Exception e) { + // 개별 결과 처리 중 오류 발생 시 무시하고 계속 진행 + } + }) + ).get(); + + // 스레드 풀 종료 + executor.shutdown(); + + // 결과 JSON에 지역 날씨 정보 배열 추가 + resultJson.set("data", areasWeather); + + // JSON 변환 및 반환 + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(resultJson); + + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("지역별 날씨 정보 조회 실패", e); + } + } + + /** + * 지역의 간략한 날씨 정보를 추출합니다. + */ + private ObjectNode getAreaWeatherSummary(String areaName, String areaCode) { + try { + // 날씨 정보 조회 API 호출 + String apiUrl = String.format( + "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", + apiKey, areaCode + ); + String xmlResponse = restTemplate.getForObject(apiUrl, String.class); + + // XML 파싱 + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes())); + + // 필요한 정보 추출 + String temperature = getNodeValueByTagName(document, "TEMP"); + String skyStatus = getNodeValueByTagName(document, "SKY_STTS"); + String precipitationType = getNodeValueByTagName(document, "PRECPT_TYPE"); + + // 날씨 아이콘 매핑 + String iconUrl = WEATHER_ICONS.getOrDefault(skyStatus, + "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); + + // JSON 객체 생성 (간소화된 형식) + ObjectNode areaWeather = objectMapper.createObjectNode(); + areaWeather.put("name", areaName); + areaWeather.put("TEMP", temperature); + areaWeather.put("PRECPT_TYPE", precipitationType); + areaWeather.put("weatherIcon", iconUrl); + + return areaWeather; + } catch (Exception e) { + // 오류 발생 시 기본 정보 반환 + ObjectNode defaultNode = objectMapper.createObjectNode(); + defaultNode.put("name", areaName); + defaultNode.put("TEMP", "N/A"); + defaultNode.put("PRECPT_TYPE", "정보 없음"); + defaultNode.put("weatherIcon", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); + return defaultNode; + } + } + + /** + * 문서에서 태그명으로 노드 값을 가져옵니다. + */ + private String getNodeValueByTagName(Document document, String tagName) { + NodeList nodes = document.getElementsByTagName(tagName); + if (nodes.getLength() > 0) { + return nodes.item(0).getTextContent().trim(); + } + return ""; + } + // 언더스코어 이름을 camelCase로 변환 private String convertToCamelCase(String input) { if (input == null || input.isEmpty()) return input; diff --git a/external-info-service/src/main/resources/application.properties b/external-info-service/src/main/resources/application.properties index 381e81a..776a5e3 100644 --- a/external-info-service/src/main/resources/application.properties +++ b/external-info-service/src/main/resources/application.properties @@ -1,11 +1,13 @@ spring.application.name=external-info-service - +server.port=8081 # postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/star -spring.datasource.username=admin +spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db +spring.datasource.username=root spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update + +seoul.api.key=505a6255746b696d34344b46566174 From 260fdbe9d3f93ae6ec4a9e355a81b747c287a7fd Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Thu, 24 Apr 2025 10:09:56 +0900 Subject: [PATCH 094/361] =?UTF-8?q?Refactor:=20.env=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WeatherController.java | 28 --- .../controller/WeatherControllerOriginal.java | 28 +++ .../controller/WeatherEsController.java | 26 +++ ...eatherDto.java => WeatherDtoOriginal.java} | 2 +- .../dto/WeatherRequest.java | 8 + .../service/WeatherEsService.java | 39 ++++ ...rvice.java => WeatherServiceOriginal.java} | 7 +- place-service/.env | 1 + place-service/.gitignore | 2 + place-service/build.gradle | 3 + .../placeservice/PlaceServiceApplication.java | 29 +++ .../placeservice/config/DotenvConfig.java | 14 ++ .../placeservice/service/KakaoMapClient.java | 2 + .../service/RestaurantService.java | 218 +++++++++--------- .../src/main/resources/application.properties | 5 +- 15 files changed, 269 insertions(+), 143 deletions(-) delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherControllerOriginal.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java rename external-info-service/src/main/java/com/example/externalinfoservice/dto/{WeatherDto.java => WeatherDtoOriginal.java} (97%) create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherRequest.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java rename external-info-service/src/main/java/com/example/externalinfoservice/service/{WeatherService.java => WeatherServiceOriginal.java} (98%) create mode 100644 place-service/.env create mode 100644 place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java deleted file mode 100644 index 5b43eb9..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.externalinfoservice.controller; - -import com.example.externalinfoservice.service.WeatherService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/weather") -@RequiredArgsConstructor -public class WeatherController { - private final WeatherService weatherService; - - @GetMapping("/{place-code}") - public String getWeatherData(@PathVariable("place-code") String placeCode) { - return weatherService.getWeatherData(placeCode); - } - - @GetMapping(value = "/api/{place-code}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getWeatherDataApi(@PathVariable("place-code") String placeCode) { - String jsonData = weatherService.getWeatherData(placeCode); - return ResponseEntity.ok(jsonData); - } -} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherControllerOriginal.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherControllerOriginal.java new file mode 100644 index 0000000..c04b7bc --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherControllerOriginal.java @@ -0,0 +1,28 @@ +//package com.example.externalinfoservice.controller; +// +//import com.example.externalinfoservice.service.WeatherService; +//import lombok.RequiredArgsConstructor; +//import org.springframework.http.MediaType; +//import org.springframework.http.ResponseEntity; +//import org.springframework.web.bind.annotation.GetMapping; +//import org.springframework.web.bind.annotation.PathVariable; +//import org.springframework.web.bind.annotation.RequestMapping; +//import org.springframework.web.bind.annotation.RestController; +// +//@RestController +//@RequestMapping("/weather") +//@RequiredArgsConstructor +//public class WeatherControllerOriginal { +// private final WeatherService weatherService; +// +// @GetMapping("/{place-code}") +// public String getWeatherData(@PathVariable("place-code") String placeCode) { +// return weatherService.getWeatherData(placeCode); +// } +// +// @GetMapping(value = "/api/{place-code}", produces = MediaType.APPLICATION_JSON_VALUE) +// public ResponseEntity getWeatherDataApi(@PathVariable("place-code") String placeCode) { +// String jsonData = weatherService.getWeatherData(placeCode); +// return ResponseEntity.ok(jsonData); +// } +//} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java new file mode 100644 index 0000000..14db029 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java @@ -0,0 +1,26 @@ +package com.example.externalinfoservice.controller; + +import com.example.externalinfoservice.dto.WeatherRequest; +import com.example.externalinfoservice.service.WeatherEsService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/main/info") +public class WeatherEsController { + + private final WeatherEsService weatherEsService; + + @GetMapping("/weather") + public ResponseEntity> getWeatherByArea(@RequestParam String area) { + Map weather = weatherEsService.getWeatherFromES(area); + return weather != null + ? ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(weather) + : ResponseEntity.notFound().build(); + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDtoOriginal.java similarity index 97% rename from external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java rename to external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDtoOriginal.java index 3570ae6..02ac271 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDtoOriginal.java @@ -10,7 +10,7 @@ @Data @XmlRootElement(name = "SeoulRtd.citydata") @XmlAccessorType(XmlAccessType.FIELD) -public class WeatherDto { +public class WeatherDtoOriginal { @XmlElement(name = "WEATHER_STTS") @JsonProperty("weather_status") private WeatherStatus weatherStatus; diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherRequest.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherRequest.java new file mode 100644 index 0000000..f9aab81 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherRequest.java @@ -0,0 +1,8 @@ +package com.example.externalinfoservice.dto; + +import lombok.Data; + +@Data +public class WeatherRequest { + private String area; +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java new file mode 100644 index 0000000..4856ec4 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java @@ -0,0 +1,39 @@ +package com.example.externalinfoservice.service; + +import com.example.externalinfoservice.dto.WeatherRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.*; + +@Service +@RequiredArgsConstructor +public class WeatherEsService { + + private final RestTemplate restTemplate = new RestTemplate(); + + public Map getWeatherFromES(String area) { + String url = "http://elasticsearch.seoultravel.life/seoul_citydata_weather/_search"; + + Map term = Map.of("weather.area_nm.keyword", area); + Map query = Map.of("term", term); + Map body = Map.of("size", 1, "query", query); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> entity = new HttpEntity<>(body, headers); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class); + + List> hits = (List>) + ((Map) response.getBody().get("hits")).get("hits"); + + if (hits.isEmpty()) return null; + + Map source = (Map) hits.get(0).get("_source"); + return (Map) source.get("weather"); + } + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java similarity index 98% rename from external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java rename to external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java index 7686729..cd4c513 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java @@ -9,14 +9,15 @@ import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; + import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.ByteArrayInputStream; -import java.util.Set; import java.util.HashSet; +import java.util.Set; @Service -public class WeatherService { +public class WeatherServiceOriginal { private final String weatherApiUrl; private final RestTemplate restTemplate; @@ -32,7 +33,7 @@ public class WeatherService { add("SKY_STTS"); // 하늘 상태 }}; - public WeatherService(RestTemplate restTemplate, ObjectMapper objectMapper) { + public WeatherServiceOriginal(RestTemplate restTemplate, ObjectMapper objectMapper) { this.weatherApiUrl = "http://openapi.seoul.go.kr:8088/594b4a6559796f683930466466666d/xml/citydata/1/5/"; this.restTemplate = restTemplate; this.objectMapper = objectMapper; diff --git a/place-service/.env b/place-service/.env new file mode 100644 index 0000000..a6e6a9b --- /dev/null +++ b/place-service/.env @@ -0,0 +1 @@ +KAKAO_API_KEY=585808366c299a6686e7104a193017bb diff --git a/place-service/.gitignore b/place-service/.gitignore index c2065bc..a00fec8 100644 --- a/place-service/.gitignore +++ b/place-service/.gitignore @@ -35,3 +35,5 @@ out/ ### VS Code ### .vscode/ + +.env diff --git a/place-service/build.gradle b/place-service/build.gradle index e094fb2..458e8f1 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -37,6 +37,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' + //implementation 'io.github.cdimascio:dotenv-java:3.0.0' // 최신 버전 + implementation 'io.github.cdimascio:dotenv-java:2.2.4' + implementation 'org.apache.kafka:kafka-streams' implementation 'org.springframework.kafka:spring-kafka' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java index 7aaef28..5e0758f 100644 --- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java +++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java @@ -1,13 +1,42 @@ package com.example.placeservice; +import com.example.placeservice.repository.RestaurantRepository; +import com.example.placeservice.service.RestaurantService; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.web.client.RestTemplate; @SpringBootApplication +@EnableAsync public class PlaceServiceApplication { public static void main(String[] args) { + // .env 로드하고 시스템 환경변수로 설정 + io.github.cdimascio.dotenv.Dotenv dotenv = io.github.cdimascio.dotenv.Dotenv.load(); + System.setProperty("KAKAO_API_KEY", dotenv.get("KAKAO_API_KEY")); + SpringApplication.run(PlaceServiceApplication.class, args); } + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + @Bean + public CommandLineRunner initData(RestaurantService restaurantService, RestaurantRepository restaurantRepository) { + return args -> { + if (restaurantRepository.count() > 0) { + System.out.println(""); + } else { + System.out.println("음식점 데이터 저장 시작"); + restaurantService.fetchAndSaveRestaurants(); + System.out.println("음식점 데이터 저장 완료"); + } + }; + } + } diff --git a/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java b/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java new file mode 100644 index 0000000..8547181 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java @@ -0,0 +1,14 @@ +package com.example.placeservice.config; + +import io.github.cdimascio.dotenv.Dotenv; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class DotenvConfig { + + @Bean + public Dotenv dotenv() { + return Dotenv.load(); // .env 파일 자동 로딩 + } +} diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java index 0fde1be..c63429e 100644 --- a/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java @@ -17,6 +17,8 @@ public class KakaoMapClient { @Value("${kakao.api.key}") private String kakaoApiKey; + + public JsonNode searchRestaurantsByCategory(BigDecimal lon, BigDecimal lat, int page) { RestTemplate restTemplate = new RestTemplate(); diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java index 032a4f2..4a71e18 100644 --- a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java +++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java @@ -1,109 +1,109 @@ -package com.example.placeservice.service; - -import com.example.placeservice.entity.Area; -import com.example.placeservice.entity.Restaurant; -import com.example.placeservice.repository.AreaRepository; -import com.example.placeservice.repository.RestaurantRepository; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; - -@Service -@RequiredArgsConstructor -public class RestaurantService { - - private final AreaRepository areaRepository; - private final RestaurantRepository restaurantRepository; - private final KakaoMapClient kakaoMapClient; - - // 특정 Area 기준으로 주변 음식점 저장 - public List saveRestaurantsNearbyArea(Area area) { - List savedRestaurants = new ArrayList<>(); - int page = 1; - boolean isEnd = false; - double searchRadius = 1000.0; // 반경 1km - int savedCount = 0; // 저장한 음식점 수 - - while (!isEnd) { - // ✅ Kakao API 호출 시 경도(lon), 위도(lat) 순서로 전달 - JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLon(), area.getLat(), page); - - if (response.has("documents")) { - for (JsonNode doc : response.get("documents")) { - if (savedCount >= 15) { // 15개 저장했으면 종료 - isEnd = true; - break; - } - - double restaurantLon = Double.parseDouble(doc.get("x").asText()); - double restaurantLat = Double.parseDouble(doc.get("y").asText()); - - double distance = calculateDistance( - area.getLat().doubleValue(), // 위도 - area.getLon().doubleValue(), // 경도 - restaurantLat, // 위도 - restaurantLon // 경도 - ); - - // 1km 이내 음식점만 저장 - if (distance <= searchRadius) { - Restaurant restaurant = new Restaurant(); - restaurant.setArea(area); - restaurant.setName(doc.get("place_name").asText()); - restaurant.setAddress(doc.get("road_address_name").asText()); - restaurant.setLat(new BigDecimal(doc.get("y").asText())); // 위도 - restaurant.setLon(new BigDecimal(doc.get("x").asText())); // 경도 - restaurant.setPhone(doc.has("phone") ? doc.get("phone").asText() : null); - restaurant.setKakaomap_url(doc.get("place_url").asText()); - restaurant.setCategory_code(doc.has("category_group_code") ? doc.get("category_group_code").asText() : null); - restaurant.setCategoryGroupName(doc.has("category_group_name") ? doc.get("category_group_name").asText() : null); - restaurant.setCategoryName(doc.has("category_name") ? doc.get("category_name").asText() : null); - restaurant.setKakao_id(doc.get("id").asText()); - - savedRestaurants.add(restaurant); - restaurantRepository.save(restaurant); - savedCount++; - } - } - - if (!isEnd) { - isEnd = response.get("meta").get("is_end").asBoolean(); - page++; - } - } else { - isEnd = true; - } - } - return savedRestaurants; - } - - // 전체 Area 대상 음식점 저장 - @Transactional - public void fetchAndSaveRestaurants() { - List areas = areaRepository.findAll(); - for (Area area : areas) { - saveRestaurantsNearbyArea(area); - } - } - - // Haversine 거리 계산 공식 (meter 단위) - private double calculateDistance(double lat1, double lon1, double lat2, double lon2) { - final int EARTH_RADIUS = 6371; // km - - double dLat = Math.toRadians(lat2 - lat1); - double dLon = Math.toRadians(lon2 - lon1); - - double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) - + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) - * Math.sin(dLon / 2) * Math.sin(dLon / 2); - - double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - return EARTH_RADIUS * c * 1000; // meter 반환 - } -} +package com.example.placeservice.service; + +import com.example.placeservice.entity.Area; +import com.example.placeservice.entity.Restaurant; +import com.example.placeservice.repository.AreaRepository; +import com.example.placeservice.repository.RestaurantRepository; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class RestaurantService { + + private final AreaRepository areaRepository; + private final RestaurantRepository restaurantRepository; + private final KakaoMapClient kakaoMapClient; + + // 특정 Area 기준으로 주변 음식점 저장 + public List saveRestaurantsNearbyArea(Area area) { + List savedRestaurants = new ArrayList<>(); + int page = 1; + boolean isEnd = false; + double searchRadius = 1000.0; // 반경 1km + int savedCount = 0; // 저장한 음식점 수 + + while (!isEnd) { + // ✅ Kakao API 호출 시 경도(lon), 위도(lat) 순서로 전달 + JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLon(), area.getLat(), page); + + if (response.has("documents")) { + for (JsonNode doc : response.get("documents")) { + if (savedCount >= 15) { // 15개 저장했으면 종료 + isEnd = true; + break; + } + + double restaurantLon = Double.parseDouble(doc.get("x").asText()); + double restaurantLat = Double.parseDouble(doc.get("y").asText()); + + double distance = calculateDistance( + area.getLat().doubleValue(), // 위도 + area.getLon().doubleValue(), // 경도 + restaurantLat, // 위도 + restaurantLon // 경도 + ); + + // 1km 이내 음식점만 저장 + if (distance <= searchRadius) { + Restaurant restaurant = new Restaurant(); + restaurant.setArea(area); + restaurant.setName(doc.get("place_name").asText()); + restaurant.setAddress(doc.get("road_address_name").asText()); + restaurant.setLat(new BigDecimal(doc.get("y").asText())); // 위도 + restaurant.setLon(new BigDecimal(doc.get("x").asText())); // 경도 + restaurant.setPhone(doc.has("phone") ? doc.get("phone").asText() : null); + restaurant.setKakaomap_url(doc.get("place_url").asText()); + restaurant.setCategory_code(doc.has("category_group_code") ? doc.get("category_group_code").asText() : null); + restaurant.setCategoryGroupName(doc.has("category_group_name") ? doc.get("category_group_name").asText() : null); + restaurant.setCategoryName(doc.has("category_name") ? doc.get("category_name").asText() : null); + restaurant.setKakao_id(doc.get("id").asText()); + + savedRestaurants.add(restaurant); + restaurantRepository.save(restaurant); + savedCount++; + } + } + + if (!isEnd) { + isEnd = response.get("meta").get("is_end").asBoolean(); + page++; + } + } else { + isEnd = true; + } + } + return savedRestaurants; + } + + // 전체 Area 대상 음식점 저장 + @Transactional + public void fetchAndSaveRestaurants() { + List areas = areaRepository.findAll(); + for (Area area : areas) { + saveRestaurantsNearbyArea(area); + } + } + + // Haversine 거리 계산 공식 (meter 단위) + private double calculateDistance(double lat1, double lon1, double lat2, double lon2) { + final int EARTH_RADIUS = 6371; // km + + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) + * Math.sin(dLon / 2) * Math.sin(dLon / 2); + + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return EARTH_RADIUS * c * 1000; // meter 반환 + } +} diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index fe42919..d70a7ac 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -13,8 +13,9 @@ spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.sql.init.mode=always -# ??? API ? ?? -kakao.api.key=585808366c299a6686e7104a193017bb +#API +#kakao.api.key=585808366c299a6686e7104a193017bb +kakao.api.key=${KAKAO_API_KEY} # ?? ?? logging.level.com.example.placeservice=INFO \ No newline at end of file From f91229f8c0017778c24dca70ec8d5702c9b2421b Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Thu, 24 Apr 2025 10:09:56 +0900 Subject: [PATCH 095/361] =?UTF-8?q?Refactor:=20.env=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WeatherController.java | 28 --- .../controller/WeatherControllerOriginal.java | 28 +++ .../controller/WeatherEsController.java | 26 +++ ...eatherDto.java => WeatherDtoOriginal.java} | 2 +- .../dto/WeatherRequest.java | 8 + .../service/WeatherEsService.java | 39 ++++ ...rvice.java => WeatherServiceOriginal.java} | 7 +- .../src/main/resources/application.properties | 6 + place-service/.env | 1 + place-service/.gitignore | 2 + place-service/build.gradle | 3 + .../placeservice/PlaceServiceApplication.java | 29 +++ .../placeservice/config/DotenvConfig.java | 14 ++ .../placeservice/service/KakaoMapClient.java | 2 + .../service/RestaurantService.java | 218 +++++++++--------- .../src/main/resources/application.properties | 5 +- 16 files changed, 275 insertions(+), 143 deletions(-) delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherControllerOriginal.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java rename external-info-service/src/main/java/com/example/externalinfoservice/dto/{WeatherDto.java => WeatherDtoOriginal.java} (97%) create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherRequest.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java rename external-info-service/src/main/java/com/example/externalinfoservice/service/{WeatherService.java => WeatherServiceOriginal.java} (98%) create mode 100644 place-service/.env create mode 100644 place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java deleted file mode 100644 index 5b43eb9..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherController.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.externalinfoservice.controller; - -import com.example.externalinfoservice.service.WeatherService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/weather") -@RequiredArgsConstructor -public class WeatherController { - private final WeatherService weatherService; - - @GetMapping("/{place-code}") - public String getWeatherData(@PathVariable("place-code") String placeCode) { - return weatherService.getWeatherData(placeCode); - } - - @GetMapping(value = "/api/{place-code}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getWeatherDataApi(@PathVariable("place-code") String placeCode) { - String jsonData = weatherService.getWeatherData(placeCode); - return ResponseEntity.ok(jsonData); - } -} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherControllerOriginal.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherControllerOriginal.java new file mode 100644 index 0000000..c04b7bc --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherControllerOriginal.java @@ -0,0 +1,28 @@ +//package com.example.externalinfoservice.controller; +// +//import com.example.externalinfoservice.service.WeatherService; +//import lombok.RequiredArgsConstructor; +//import org.springframework.http.MediaType; +//import org.springframework.http.ResponseEntity; +//import org.springframework.web.bind.annotation.GetMapping; +//import org.springframework.web.bind.annotation.PathVariable; +//import org.springframework.web.bind.annotation.RequestMapping; +//import org.springframework.web.bind.annotation.RestController; +// +//@RestController +//@RequestMapping("/weather") +//@RequiredArgsConstructor +//public class WeatherControllerOriginal { +// private final WeatherService weatherService; +// +// @GetMapping("/{place-code}") +// public String getWeatherData(@PathVariable("place-code") String placeCode) { +// return weatherService.getWeatherData(placeCode); +// } +// +// @GetMapping(value = "/api/{place-code}", produces = MediaType.APPLICATION_JSON_VALUE) +// public ResponseEntity getWeatherDataApi(@PathVariable("place-code") String placeCode) { +// String jsonData = weatherService.getWeatherData(placeCode); +// return ResponseEntity.ok(jsonData); +// } +//} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java new file mode 100644 index 0000000..14db029 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java @@ -0,0 +1,26 @@ +package com.example.externalinfoservice.controller; + +import com.example.externalinfoservice.dto.WeatherRequest; +import com.example.externalinfoservice.service.WeatherEsService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/main/info") +public class WeatherEsController { + + private final WeatherEsService weatherEsService; + + @GetMapping("/weather") + public ResponseEntity> getWeatherByArea(@RequestParam String area) { + Map weather = weatherEsService.getWeatherFromES(area); + return weather != null + ? ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(weather) + : ResponseEntity.notFound().build(); + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDtoOriginal.java similarity index 97% rename from external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java rename to external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDtoOriginal.java index 3570ae6..02ac271 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDto.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDtoOriginal.java @@ -10,7 +10,7 @@ @Data @XmlRootElement(name = "SeoulRtd.citydata") @XmlAccessorType(XmlAccessType.FIELD) -public class WeatherDto { +public class WeatherDtoOriginal { @XmlElement(name = "WEATHER_STTS") @JsonProperty("weather_status") private WeatherStatus weatherStatus; diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherRequest.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherRequest.java new file mode 100644 index 0000000..f9aab81 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherRequest.java @@ -0,0 +1,8 @@ +package com.example.externalinfoservice.dto; + +import lombok.Data; + +@Data +public class WeatherRequest { + private String area; +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java new file mode 100644 index 0000000..4856ec4 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java @@ -0,0 +1,39 @@ +package com.example.externalinfoservice.service; + +import com.example.externalinfoservice.dto.WeatherRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.*; + +@Service +@RequiredArgsConstructor +public class WeatherEsService { + + private final RestTemplate restTemplate = new RestTemplate(); + + public Map getWeatherFromES(String area) { + String url = "http://elasticsearch.seoultravel.life/seoul_citydata_weather/_search"; + + Map term = Map.of("weather.area_nm.keyword", area); + Map query = Map.of("term", term); + Map body = Map.of("size", 1, "query", query); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> entity = new HttpEntity<>(body, headers); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class); + + List> hits = (List>) + ((Map) response.getBody().get("hits")).get("hits"); + + if (hits.isEmpty()) return null; + + Map source = (Map) hits.get(0).get("_source"); + return (Map) source.get("weather"); + } + +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java similarity index 98% rename from external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java rename to external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java index 7686729..cd4c513 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java @@ -9,14 +9,15 @@ import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; + import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.ByteArrayInputStream; -import java.util.Set; import java.util.HashSet; +import java.util.Set; @Service -public class WeatherService { +public class WeatherServiceOriginal { private final String weatherApiUrl; private final RestTemplate restTemplate; @@ -32,7 +33,7 @@ public class WeatherService { add("SKY_STTS"); // 하늘 상태 }}; - public WeatherService(RestTemplate restTemplate, ObjectMapper objectMapper) { + public WeatherServiceOriginal(RestTemplate restTemplate, ObjectMapper objectMapper) { this.weatherApiUrl = "http://openapi.seoul.go.kr:8088/594b4a6559796f683930466466666d/xml/citydata/1/5/"; this.restTemplate = restTemplate; this.objectMapper = objectMapper; diff --git a/external-info-service/src/main/resources/application.properties b/external-info-service/src/main/resources/application.properties index ee75722..0d3f60e 100644 --- a/external-info-service/src/main/resources/application.properties +++ b/external-info-service/src/main/resources/application.properties @@ -9,5 +9,11 @@ spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=update +spring.elasticsearch.uris= http://elasticsearch.seoultravel.life:9200 + +spring.mvc.contentnegotiation.favor-path-extension=false +spring.mvc.contentnegotiation.media-types.json=application/json +spring.mvc.default-response-type=application/json + seoul.api.key=505a6255746b696d34344b46566174 diff --git a/place-service/.env b/place-service/.env new file mode 100644 index 0000000..a6e6a9b --- /dev/null +++ b/place-service/.env @@ -0,0 +1 @@ +KAKAO_API_KEY=585808366c299a6686e7104a193017bb diff --git a/place-service/.gitignore b/place-service/.gitignore index c2065bc..a00fec8 100644 --- a/place-service/.gitignore +++ b/place-service/.gitignore @@ -35,3 +35,5 @@ out/ ### VS Code ### .vscode/ + +.env diff --git a/place-service/build.gradle b/place-service/build.gradle index e094fb2..458e8f1 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -37,6 +37,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' + //implementation 'io.github.cdimascio:dotenv-java:3.0.0' // 최신 버전 + implementation 'io.github.cdimascio:dotenv-java:2.2.4' + implementation 'org.apache.kafka:kafka-streams' implementation 'org.springframework.kafka:spring-kafka' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java index 7aaef28..5e0758f 100644 --- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java +++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java @@ -1,13 +1,42 @@ package com.example.placeservice; +import com.example.placeservice.repository.RestaurantRepository; +import com.example.placeservice.service.RestaurantService; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.web.client.RestTemplate; @SpringBootApplication +@EnableAsync public class PlaceServiceApplication { public static void main(String[] args) { + // .env 로드하고 시스템 환경변수로 설정 + io.github.cdimascio.dotenv.Dotenv dotenv = io.github.cdimascio.dotenv.Dotenv.load(); + System.setProperty("KAKAO_API_KEY", dotenv.get("KAKAO_API_KEY")); + SpringApplication.run(PlaceServiceApplication.class, args); } + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + @Bean + public CommandLineRunner initData(RestaurantService restaurantService, RestaurantRepository restaurantRepository) { + return args -> { + if (restaurantRepository.count() > 0) { + System.out.println(""); + } else { + System.out.println("음식점 데이터 저장 시작"); + restaurantService.fetchAndSaveRestaurants(); + System.out.println("음식점 데이터 저장 완료"); + } + }; + } + } diff --git a/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java b/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java new file mode 100644 index 0000000..8547181 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java @@ -0,0 +1,14 @@ +package com.example.placeservice.config; + +import io.github.cdimascio.dotenv.Dotenv; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class DotenvConfig { + + @Bean + public Dotenv dotenv() { + return Dotenv.load(); // .env 파일 자동 로딩 + } +} diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java index 0fde1be..c63429e 100644 --- a/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoMapClient.java @@ -17,6 +17,8 @@ public class KakaoMapClient { @Value("${kakao.api.key}") private String kakaoApiKey; + + public JsonNode searchRestaurantsByCategory(BigDecimal lon, BigDecimal lat, int page) { RestTemplate restTemplate = new RestTemplate(); diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java index 032a4f2..4a71e18 100644 --- a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java +++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java @@ -1,109 +1,109 @@ -package com.example.placeservice.service; - -import com.example.placeservice.entity.Area; -import com.example.placeservice.entity.Restaurant; -import com.example.placeservice.repository.AreaRepository; -import com.example.placeservice.repository.RestaurantRepository; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; - -@Service -@RequiredArgsConstructor -public class RestaurantService { - - private final AreaRepository areaRepository; - private final RestaurantRepository restaurantRepository; - private final KakaoMapClient kakaoMapClient; - - // 특정 Area 기준으로 주변 음식점 저장 - public List saveRestaurantsNearbyArea(Area area) { - List savedRestaurants = new ArrayList<>(); - int page = 1; - boolean isEnd = false; - double searchRadius = 1000.0; // 반경 1km - int savedCount = 0; // 저장한 음식점 수 - - while (!isEnd) { - // ✅ Kakao API 호출 시 경도(lon), 위도(lat) 순서로 전달 - JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLon(), area.getLat(), page); - - if (response.has("documents")) { - for (JsonNode doc : response.get("documents")) { - if (savedCount >= 15) { // 15개 저장했으면 종료 - isEnd = true; - break; - } - - double restaurantLon = Double.parseDouble(doc.get("x").asText()); - double restaurantLat = Double.parseDouble(doc.get("y").asText()); - - double distance = calculateDistance( - area.getLat().doubleValue(), // 위도 - area.getLon().doubleValue(), // 경도 - restaurantLat, // 위도 - restaurantLon // 경도 - ); - - // 1km 이내 음식점만 저장 - if (distance <= searchRadius) { - Restaurant restaurant = new Restaurant(); - restaurant.setArea(area); - restaurant.setName(doc.get("place_name").asText()); - restaurant.setAddress(doc.get("road_address_name").asText()); - restaurant.setLat(new BigDecimal(doc.get("y").asText())); // 위도 - restaurant.setLon(new BigDecimal(doc.get("x").asText())); // 경도 - restaurant.setPhone(doc.has("phone") ? doc.get("phone").asText() : null); - restaurant.setKakaomap_url(doc.get("place_url").asText()); - restaurant.setCategory_code(doc.has("category_group_code") ? doc.get("category_group_code").asText() : null); - restaurant.setCategoryGroupName(doc.has("category_group_name") ? doc.get("category_group_name").asText() : null); - restaurant.setCategoryName(doc.has("category_name") ? doc.get("category_name").asText() : null); - restaurant.setKakao_id(doc.get("id").asText()); - - savedRestaurants.add(restaurant); - restaurantRepository.save(restaurant); - savedCount++; - } - } - - if (!isEnd) { - isEnd = response.get("meta").get("is_end").asBoolean(); - page++; - } - } else { - isEnd = true; - } - } - return savedRestaurants; - } - - // 전체 Area 대상 음식점 저장 - @Transactional - public void fetchAndSaveRestaurants() { - List areas = areaRepository.findAll(); - for (Area area : areas) { - saveRestaurantsNearbyArea(area); - } - } - - // Haversine 거리 계산 공식 (meter 단위) - private double calculateDistance(double lat1, double lon1, double lat2, double lon2) { - final int EARTH_RADIUS = 6371; // km - - double dLat = Math.toRadians(lat2 - lat1); - double dLon = Math.toRadians(lon2 - lon1); - - double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) - + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) - * Math.sin(dLon / 2) * Math.sin(dLon / 2); - - double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - return EARTH_RADIUS * c * 1000; // meter 반환 - } -} +package com.example.placeservice.service; + +import com.example.placeservice.entity.Area; +import com.example.placeservice.entity.Restaurant; +import com.example.placeservice.repository.AreaRepository; +import com.example.placeservice.repository.RestaurantRepository; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class RestaurantService { + + private final AreaRepository areaRepository; + private final RestaurantRepository restaurantRepository; + private final KakaoMapClient kakaoMapClient; + + // 특정 Area 기준으로 주변 음식점 저장 + public List saveRestaurantsNearbyArea(Area area) { + List savedRestaurants = new ArrayList<>(); + int page = 1; + boolean isEnd = false; + double searchRadius = 1000.0; // 반경 1km + int savedCount = 0; // 저장한 음식점 수 + + while (!isEnd) { + // ✅ Kakao API 호출 시 경도(lon), 위도(lat) 순서로 전달 + JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLon(), area.getLat(), page); + + if (response.has("documents")) { + for (JsonNode doc : response.get("documents")) { + if (savedCount >= 15) { // 15개 저장했으면 종료 + isEnd = true; + break; + } + + double restaurantLon = Double.parseDouble(doc.get("x").asText()); + double restaurantLat = Double.parseDouble(doc.get("y").asText()); + + double distance = calculateDistance( + area.getLat().doubleValue(), // 위도 + area.getLon().doubleValue(), // 경도 + restaurantLat, // 위도 + restaurantLon // 경도 + ); + + // 1km 이내 음식점만 저장 + if (distance <= searchRadius) { + Restaurant restaurant = new Restaurant(); + restaurant.setArea(area); + restaurant.setName(doc.get("place_name").asText()); + restaurant.setAddress(doc.get("road_address_name").asText()); + restaurant.setLat(new BigDecimal(doc.get("y").asText())); // 위도 + restaurant.setLon(new BigDecimal(doc.get("x").asText())); // 경도 + restaurant.setPhone(doc.has("phone") ? doc.get("phone").asText() : null); + restaurant.setKakaomap_url(doc.get("place_url").asText()); + restaurant.setCategory_code(doc.has("category_group_code") ? doc.get("category_group_code").asText() : null); + restaurant.setCategoryGroupName(doc.has("category_group_name") ? doc.get("category_group_name").asText() : null); + restaurant.setCategoryName(doc.has("category_name") ? doc.get("category_name").asText() : null); + restaurant.setKakao_id(doc.get("id").asText()); + + savedRestaurants.add(restaurant); + restaurantRepository.save(restaurant); + savedCount++; + } + } + + if (!isEnd) { + isEnd = response.get("meta").get("is_end").asBoolean(); + page++; + } + } else { + isEnd = true; + } + } + return savedRestaurants; + } + + // 전체 Area 대상 음식점 저장 + @Transactional + public void fetchAndSaveRestaurants() { + List areas = areaRepository.findAll(); + for (Area area : areas) { + saveRestaurantsNearbyArea(area); + } + } + + // Haversine 거리 계산 공식 (meter 단위) + private double calculateDistance(double lat1, double lon1, double lat2, double lon2) { + final int EARTH_RADIUS = 6371; // km + + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) + * Math.sin(dLon / 2) * Math.sin(dLon / 2); + + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return EARTH_RADIUS * c * 1000; // meter 반환 + } +} diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index fe42919..d70a7ac 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -13,8 +13,9 @@ spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.sql.init.mode=always -# ??? API ? ?? -kakao.api.key=585808366c299a6686e7104a193017bb +#API +#kakao.api.key=585808366c299a6686e7104a193017bb +kakao.api.key=${KAKAO_API_KEY} # ?? ?? logging.level.com.example.placeservice=INFO \ No newline at end of file From 86006c02cf555ddb3091ebad24ae139cfe950632 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Thu, 24 Apr 2025 10:18:49 +0900 Subject: [PATCH 096/361] =?UTF-8?q?Refactor:=20.env=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- place-service/.env | 2 +- place-service/.gitignore | 2 +- place-service/build.gradle | 1 - .../java/com/example/placeservice/PlaceServiceApplication.java | 1 + .../main/java/com/example/placeservice/config/DotenvConfig.java | 1 + place-service/src/main/resources/application.properties | 1 - 6 files changed, 4 insertions(+), 4 deletions(-) diff --git a/place-service/.env b/place-service/.env index a6e6a9b..e9d679d 100644 --- a/place-service/.env +++ b/place-service/.env @@ -1 +1 @@ -KAKAO_API_KEY=585808366c299a6686e7104a193017bb +KAKAO_API_KEY=585808366c299a6686e7104a193017bb \ No newline at end of file diff --git a/place-service/.gitignore b/place-service/.gitignore index a00fec8..4c75e42 100644 --- a/place-service/.gitignore +++ b/place-service/.gitignore @@ -36,4 +36,4 @@ out/ ### VS Code ### .vscode/ -.env +.env \ No newline at end of file diff --git a/place-service/build.gradle b/place-service/build.gradle index 458e8f1..0fd0ebb 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -39,7 +39,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' //implementation 'io.github.cdimascio:dotenv-java:3.0.0' // 최신 버전 implementation 'io.github.cdimascio:dotenv-java:2.2.4' - implementation 'org.apache.kafka:kafka-streams' implementation 'org.springframework.kafka:spring-kafka' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java index 5e0758f..5fec400 100644 --- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java +++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java @@ -21,6 +21,7 @@ public static void main(String[] args) { SpringApplication.run(PlaceServiceApplication.class, args); } + @Bean public RestTemplate restTemplate() { return new RestTemplate(); diff --git a/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java b/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java index 8547181..0eafb25 100644 --- a/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java +++ b/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java @@ -11,4 +11,5 @@ public class DotenvConfig { public Dotenv dotenv() { return Dotenv.load(); // .env 파일 자동 로딩 } + } diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index d70a7ac..ad08a70 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -14,7 +14,6 @@ spring.jpa.show-sql=true spring.sql.init.mode=always #API -#kakao.api.key=585808366c299a6686e7104a193017bb kakao.api.key=${KAKAO_API_KEY} # ?? ?? From 4bfad1742ad4f68b28e61a73a22445cfded7e6b7 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Thu, 24 Apr 2025 10:18:49 +0900 Subject: [PATCH 097/361] =?UTF-8?q?Refactor:=20.env=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- place-service/.env | 2 +- place-service/.gitignore | 2 +- place-service/build.gradle | 1 - .../java/com/example/placeservice/PlaceServiceApplication.java | 1 + .../main/java/com/example/placeservice/config/DotenvConfig.java | 1 + place-service/src/main/resources/application.properties | 1 - 6 files changed, 4 insertions(+), 4 deletions(-) diff --git a/place-service/.env b/place-service/.env index a6e6a9b..e9d679d 100644 --- a/place-service/.env +++ b/place-service/.env @@ -1 +1 @@ -KAKAO_API_KEY=585808366c299a6686e7104a193017bb +KAKAO_API_KEY=585808366c299a6686e7104a193017bb \ No newline at end of file diff --git a/place-service/.gitignore b/place-service/.gitignore index a00fec8..4c75e42 100644 --- a/place-service/.gitignore +++ b/place-service/.gitignore @@ -36,4 +36,4 @@ out/ ### VS Code ### .vscode/ -.env +.env \ No newline at end of file diff --git a/place-service/build.gradle b/place-service/build.gradle index 458e8f1..0fd0ebb 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -39,7 +39,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' //implementation 'io.github.cdimascio:dotenv-java:3.0.0' // 최신 버전 implementation 'io.github.cdimascio:dotenv-java:2.2.4' - implementation 'org.apache.kafka:kafka-streams' implementation 'org.springframework.kafka:spring-kafka' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java index 5e0758f..5fec400 100644 --- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java +++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java @@ -21,6 +21,7 @@ public static void main(String[] args) { SpringApplication.run(PlaceServiceApplication.class, args); } + @Bean public RestTemplate restTemplate() { return new RestTemplate(); diff --git a/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java b/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java index 8547181..0eafb25 100644 --- a/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java +++ b/place-service/src/main/java/com/example/placeservice/config/DotenvConfig.java @@ -11,4 +11,5 @@ public class DotenvConfig { public Dotenv dotenv() { return Dotenv.load(); // .env 파일 자동 로딩 } + } diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index d70a7ac..ad08a70 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -14,7 +14,6 @@ spring.jpa.show-sql=true spring.sql.init.mode=always #API -#kakao.api.key=585808366c299a6686e7104a193017bb kakao.api.key=${KAKAO_API_KEY} # ?? ?? From f9d8bd02898fb85a9378eeac18e8cbd61625597c Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Thu, 24 Apr 2025 10:21:54 +0900 Subject: [PATCH 098/361] Delete place-service/.env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .env 삭제 --- place-service/.env | 1 - 1 file changed, 1 deletion(-) delete mode 100644 place-service/.env diff --git a/place-service/.env b/place-service/.env deleted file mode 100644 index e9d679d..0000000 --- a/place-service/.env +++ /dev/null @@ -1 +0,0 @@ -KAKAO_API_KEY=585808366c299a6686e7104a193017bb \ No newline at end of file From e10cea9e3e63c8151f059cbdc0bae1fb4569ba2b Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Thu, 24 Apr 2025 10:21:54 +0900 Subject: [PATCH 099/361] Delete place-service/.env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .env 삭제 --- place-service/.env | 1 - 1 file changed, 1 deletion(-) delete mode 100644 place-service/.env diff --git a/place-service/.env b/place-service/.env deleted file mode 100644 index e9d679d..0000000 --- a/place-service/.env +++ /dev/null @@ -1 +0,0 @@ -KAKAO_API_KEY=585808366c299a6686e7104a193017bb \ No newline at end of file From 9c493032a6d6d00ac07e738cc2ecb71150e77033 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Thu, 24 Apr 2025 13:13:55 +0900 Subject: [PATCH 100/361] parsing code modified --- .../com/example/placeservice/config/DataInitializer.java | 1 - place-service/src/main/resources/application.properties | 9 +-------- place-service/src/main/resources/data.sql | 4 ---- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java b/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java index 23379c1..8a52374 100644 --- a/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java +++ b/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java @@ -19,7 +19,6 @@ public class DataInitializer implements CommandLineRunner { public void run(String... args) throws Exception { // API 호출 및 XML 파싱 List items = accommodationParseService.parseXmlData(); - // 파싱된 데이터 저장 accommodationSaveService.saveAccommodation(items); diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 9eccc2b..107a79d 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -1,21 +1,14 @@ spring.application.name=place-service -<<<<<<< HEAD -======= spring.config.import=optional:file:.env[.properties] ->>>>>>> a2b194bec03d45b8e8e92e326efdf5344d2d36da # postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db +spring.datasource.url=jdbc:postgresql://db:5432/stars_db spring.datasource.username=root spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -<<<<<<< HEAD -spring.jpa.hibernate.ddl-auto=update -======= spring.jpa.hibernate.ddl-auto=create ->>>>>>> a2b194bec03d45b8e8e92e326efdf5344d2d36da spring.jpa.defer-datasource-initialization=true # Hibernate SQL ?? ??? diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index e8dc3f8..4eb8df6 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -1,8 +1,4 @@ -<<<<<<< HEAD -INSERT INTO area (seoul_id, name, name_eng, category, lon, lat) VALUES -======= INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES ->>>>>>> a2b194bec03d45b8e8e92e326efdf5344d2d36da ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), From a938ad504604d65b8de597bfb6ba1b7e5e1eed1b Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Thu, 24 Apr 2025 13:13:55 +0900 Subject: [PATCH 101/361] parsing code modified --- .../com/example/placeservice/config/DataInitializer.java | 1 - place-service/src/main/resources/application.properties | 9 +-------- place-service/src/main/resources/data.sql | 4 ---- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java b/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java index 23379c1..8a52374 100644 --- a/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java +++ b/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java @@ -19,7 +19,6 @@ public class DataInitializer implements CommandLineRunner { public void run(String... args) throws Exception { // API 호출 및 XML 파싱 List items = accommodationParseService.parseXmlData(); - // 파싱된 데이터 저장 accommodationSaveService.saveAccommodation(items); diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 9eccc2b..107a79d 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -1,21 +1,14 @@ spring.application.name=place-service -<<<<<<< HEAD -======= spring.config.import=optional:file:.env[.properties] ->>>>>>> a2b194bec03d45b8e8e92e326efdf5344d2d36da # postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db +spring.datasource.url=jdbc:postgresql://db:5432/stars_db spring.datasource.username=root spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -<<<<<<< HEAD -spring.jpa.hibernate.ddl-auto=update -======= spring.jpa.hibernate.ddl-auto=create ->>>>>>> a2b194bec03d45b8e8e92e326efdf5344d2d36da spring.jpa.defer-datasource-initialization=true # Hibernate SQL ?? ??? diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index e8dc3f8..4eb8df6 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -1,8 +1,4 @@ -<<<<<<< HEAD -INSERT INTO area (seoul_id, name, name_eng, category, lon, lat) VALUES -======= INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES ->>>>>>> a2b194bec03d45b8e8e92e326efdf5344d2d36da ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), From 7a718fc7cc5aed80ca1ec0d294dfa26ec8102dbe Mon Sep 17 00:00:00 2001 From: GLENSIS Date: Thu, 24 Apr 2025 14:44:02 +0900 Subject: [PATCH 102/361] =?UTF-8?q?area=5Fid=EB=A5=BC=20=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/placeservice/entity/CulturalEvent.java | 6 +++--- .../example/placeservice/service/EventParserService.java | 4 ++-- place-service/src/main/resources/application.properties | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java index 80199ab..4af7f5c 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java +++ b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java @@ -28,10 +28,10 @@ public class CulturalEvent { @Column(name = "category", length = 200) private String category; - @Column(name = "title", length = 50) + @Column(name = "title", length = 200) private String title; - @Column(name = "address", length = 50) + @Column(name = "address", length = 200) private String address; // PLACE → address @Column(name = "lat", precision = 10, scale = 6) @@ -40,7 +40,7 @@ public class CulturalEvent { @Column(name = "lon", precision = 10, scale = 6) private BigDecimal lon; - @Column(name = "target", length = 100) + @Column(name = "target", length = 200) private String target; @Column(name = "event_fee", columnDefinition = "TEXT") diff --git a/place-service/src/main/java/com/example/placeservice/service/EventParserService.java b/place-service/src/main/java/com/example/placeservice/service/EventParserService.java index 3d599ed..f12cbde 100644 --- a/place-service/src/main/java/com/example/placeservice/service/EventParserService.java +++ b/place-service/src/main/java/com/example/placeservice/service/EventParserService.java @@ -34,8 +34,8 @@ public CulturalEvent toEntity(CulturalEventItem item) { event.setCategory(item.getCodename()); // category event.setTitle(item.getTitle()); // title event.setAddress(item.getPlace()); // address - event.setLat(item.getLat()); // lat - event.setLon(item.getLot()); // lon + event.setLat(item.getLot()); // lat + event.setLon(item.getLat()); // lon event.setTarget(item.getUseTrgt()); // target event.setEventFee(item.getUseFee()); // event_fee event.setStartDate(item.getStrtdate()); // start_date diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 72673c5..d8e83a5 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -7,7 +7,7 @@ spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=create spring.jpa.defer-datasource-initialization=true # Hibernate SQL ?? ??? From d6cf1d0bb55a98f0dcf8856f7d4df82e06fd4ae1 Mon Sep 17 00:00:00 2001 From: GLENSIS Date: Thu, 24 Apr 2025 14:44:02 +0900 Subject: [PATCH 103/361] =?UTF-8?q?area=5Fid=EB=A5=BC=20=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/placeservice/entity/CulturalEvent.java | 6 +++--- .../example/placeservice/service/EventParserService.java | 4 ++-- place-service/src/main/resources/application.properties | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java index 80199ab..4af7f5c 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java +++ b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java @@ -28,10 +28,10 @@ public class CulturalEvent { @Column(name = "category", length = 200) private String category; - @Column(name = "title", length = 50) + @Column(name = "title", length = 200) private String title; - @Column(name = "address", length = 50) + @Column(name = "address", length = 200) private String address; // PLACE → address @Column(name = "lat", precision = 10, scale = 6) @@ -40,7 +40,7 @@ public class CulturalEvent { @Column(name = "lon", precision = 10, scale = 6) private BigDecimal lon; - @Column(name = "target", length = 100) + @Column(name = "target", length = 200) private String target; @Column(name = "event_fee", columnDefinition = "TEXT") diff --git a/place-service/src/main/java/com/example/placeservice/service/EventParserService.java b/place-service/src/main/java/com/example/placeservice/service/EventParserService.java index 3d599ed..f12cbde 100644 --- a/place-service/src/main/java/com/example/placeservice/service/EventParserService.java +++ b/place-service/src/main/java/com/example/placeservice/service/EventParserService.java @@ -34,8 +34,8 @@ public CulturalEvent toEntity(CulturalEventItem item) { event.setCategory(item.getCodename()); // category event.setTitle(item.getTitle()); // title event.setAddress(item.getPlace()); // address - event.setLat(item.getLat()); // lat - event.setLon(item.getLot()); // lon + event.setLat(item.getLot()); // lat + event.setLon(item.getLat()); // lon event.setTarget(item.getUseTrgt()); // target event.setEventFee(item.getUseFee()); // event_fee event.setStartDate(item.getStrtdate()); // start_date diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 72673c5..d8e83a5 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -7,7 +7,7 @@ spring.datasource.password=admin spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=none +spring.jpa.hibernate.ddl-auto=create spring.jpa.defer-datasource-initialization=true # Hibernate SQL ?? ??? From 97d756ee7a6694de47e378a4e121baf225921c3f Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Thu, 24 Apr 2025 15:20:11 +0900 Subject: [PATCH 104/361] =?UTF-8?q?Refactor:=20=EC=8B=A4=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExternalInfoServiceApplication.java | 3 ++ .../externalinfoservice/config/WebConfig.java | 16 ++++++ .../controller/SseWeatherController.java | 45 ++++++++++++++++ .../controller/WeatherEsController.java | 19 ++++--- .../scheduler/WeatherPushScheduler.java | 22 ++++++++ .../service/WeatherEsService.java | 52 +++++++++++++++++-- .../resources/static/weather-stream-test.html | 22 ++++++++ 7 files changed, 168 insertions(+), 11 deletions(-) create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/config/WebConfig.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java create mode 100644 external-info-service/src/main/resources/static/weather-stream-test.html diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java index 8ad5cab..cd08ab0 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java @@ -2,11 +2,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling public class ExternalInfoServiceApplication { public static void main(String[] args) { + SpringApplication.run(ExternalInfoServiceApplication.class, args); } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/config/WebConfig.java b/external-info-service/src/main/java/com/example/externalinfoservice/config/WebConfig.java new file mode 100644 index 0000000..873472e --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/config/WebConfig.java @@ -0,0 +1,16 @@ +package com.example.externalinfoservice.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/main/info/weather/stream") + .allowedOrigins("*") + .allowedMethods("GET"); + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java new file mode 100644 index 0000000..d37f517 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java @@ -0,0 +1,45 @@ +package com.example.externalinfoservice.controller; + +import com.example.externalinfoservice.service.WeatherEsService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/main/info/weather") +public class SseWeatherController { + + private final WeatherEsService weatherEsService; + private final List emitters = new CopyOnWriteArrayList<>(); + + @GetMapping("/stream") + public SseEmitter streamWeather() { + SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음 + emitters.add(emitter); + + emitter.onCompletion(() -> emitters.remove(emitter)); + emitter.onTimeout(() -> emitters.remove(emitter)); + emitter.onError((e) -> emitters.remove(emitter)); + + return emitter; + } + + // 주기적으로 클라이언트에게 push + public void sendToClients(List> weatherList) { + for (SseEmitter emitter : emitters) { + try { + emitter.send(SseEmitter.event() + .name("weather-update") + .data(weatherList)); + } catch (IOException e) { + emitter.completeWithError(e); + emitters.remove(emitter); + } + } + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java index 14db029..390a254 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java @@ -1,26 +1,29 @@ package com.example.externalinfoservice.controller; -import com.example.externalinfoservice.dto.WeatherRequest; import com.example.externalinfoservice.service.WeatherEsService; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.List; import java.util.Map; @RestController @RequiredArgsConstructor -@RequestMapping("/main/info") +@RequestMapping("/main/info/weather") public class WeatherEsController { private final WeatherEsService weatherEsService; - @GetMapping("/weather") - public ResponseEntity> getWeatherByArea(@RequestParam String area) { - Map weather = weatherEsService.getWeatherFromES(area); - return weather != null - ? ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(weather) - : ResponseEntity.notFound().build(); + @GetMapping(value = {"/{placeCode}", ""}, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getWeather(@PathVariable(name = "placeCode", required = false) String placeCode) { + if (placeCode == null || placeCode.isBlank()) { + List> allWeather = weatherEsService.getAllWeatherFromES(); + return ResponseEntity.ok(allWeather); + } + + Map weather = weatherEsService.getWeatherFromES(placeCode); + return weather != null ? ResponseEntity.ok(weather) : ResponseEntity.notFound().build(); } } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java new file mode 100644 index 0000000..3f7f58a --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java @@ -0,0 +1,22 @@ +package com.example.externalinfoservice.scheduler; + +import com.example.externalinfoservice.controller.SseWeatherController; +import com.example.externalinfoservice.service.WeatherEsService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class WeatherPushScheduler { + + private final WeatherEsService weatherEsService; + private final SseWeatherController sseWeatherController; + + // 5분마다 날씨 데이터를 SSE 클라이언트에 push + @Scheduled(fixedRate = 10_000) // 5분 = 300초 + public void pushWeatherToClients() { + var weatherList = weatherEsService.getAllWeatherFromES(); + sseWeatherController.sendToClients(weatherList); + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java index 4856ec4..e912d91 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java @@ -1,6 +1,5 @@ package com.example.externalinfoservice.service; -import com.example.externalinfoservice.dto.WeatherRequest; import lombok.RequiredArgsConstructor; import org.springframework.http.*; import org.springframework.stereotype.Service; @@ -14,12 +13,13 @@ public class WeatherEsService { private final RestTemplate restTemplate = new RestTemplate(); + // 특정 지역 날씨 public Map getWeatherFromES(String area) { - String url = "http://elasticsearch.seoultravel.life/seoul_citydata_weather/_search"; + String url = "http://elasticsearch.seoultravel.life/seoul_citydata_weather_20250424/_search"; Map term = Map.of("weather.area_nm.keyword", area); Map query = Map.of("term", term); - Map body = Map.of("size", 1, "query", query); + Map body = Map.of("size", 100, "query", query); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); @@ -36,4 +36,50 @@ public Map getWeatherFromES(String area) { return (Map) source.get("weather"); } + // 전체 지역 날씨 + public List> getAllWeatherFromES() { + String url = "http://elasticsearch.seoultravel.life/seoul_citydata_weather_20250424/_search"; + + Map body = Map.of( + "size", 0, + "aggs", Map.of( + "by_area", Map.of( + "terms", Map.of("field", "weather.area_nm.keyword", "size", 1000), + "aggs", Map.of( + "latest_hit", Map.of( + "top_hits", Map.of( + "size", 1, + "sort", List.of(Map.of("weather.weather_time", Map.of("order", "desc"))) + ) + ) + ) + ) + ) + ); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> entity = new HttpEntity<>(body, headers); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class); + + Map aggregations = (Map) response.getBody().get("aggregations"); + Map byArea = (Map) aggregations.get("by_area"); + List> buckets = (List>) byArea.get("buckets"); + + List> results = new ArrayList<>(); + for (Map bucket : buckets) { + Map latestHitWrapper = (Map) bucket.get("latest_hit"); + Map hitsWrapper = (Map) latestHitWrapper.get("hits"); + List> hits = (List>) hitsWrapper.get("hits"); + if (hits.isEmpty()) continue; + + Map latestHit = hits.get(0); + Map source = (Map) latestHit.get("_source"); + results.add((Map) source.get("weather")); + } + + return results; + } + } diff --git a/external-info-service/src/main/resources/static/weather-stream-test.html b/external-info-service/src/main/resources/static/weather-stream-test.html new file mode 100644 index 0000000..d4a2095 --- /dev/null +++ b/external-info-service/src/main/resources/static/weather-stream-test.html @@ -0,0 +1,22 @@ + + + + + + 날씨 SSE 테스트 + + +

날씨 실시간 수신 중...

+

+
+
+
+

From d5cae02d55292e92cc57de81f21f597190e3fd0f Mon Sep 17 00:00:00 2001
From: suminmmmmmmm 
Date: Thu, 24 Apr 2025 15:20:11 +0900
Subject: [PATCH 105/361] =?UTF-8?q?Refactor:=20=EC=8B=A4=EC=8B=9C=EA=B0=84?=
 =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../ExternalInfoServiceApplication.java       |  3 ++
 .../externalinfoservice/config/WebConfig.java | 16 ++++++
 .../controller/SseWeatherController.java      | 45 ++++++++++++++++
 .../controller/WeatherEsController.java       | 19 ++++---
 .../scheduler/WeatherPushScheduler.java       | 22 ++++++++
 .../service/WeatherEsService.java             | 52 +++++++++++++++++--
 .../resources/static/weather-stream-test.html | 22 ++++++++
 7 files changed, 168 insertions(+), 11 deletions(-)
 create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/config/WebConfig.java
 create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java
 create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java
 create mode 100644 external-info-service/src/main/resources/static/weather-stream-test.html

diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java
index 8ad5cab..cd08ab0 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java
@@ -2,11 +2,14 @@
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
 
 @SpringBootApplication
+@EnableScheduling
 public class ExternalInfoServiceApplication {
 
     public static void main(String[] args) {
+
         SpringApplication.run(ExternalInfoServiceApplication.class, args);
     }
 
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/config/WebConfig.java b/external-info-service/src/main/java/com/example/externalinfoservice/config/WebConfig.java
new file mode 100644
index 0000000..873472e
--- /dev/null
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/config/WebConfig.java
@@ -0,0 +1,16 @@
+package com.example.externalinfoservice.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/main/info/weather/stream")
+                .allowedOrigins("*")
+                .allowedMethods("GET");
+    }
+}
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java
new file mode 100644
index 0000000..d37f517
--- /dev/null
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java
@@ -0,0 +1,45 @@
+package com.example.externalinfoservice.controller;
+
+import com.example.externalinfoservice.service.WeatherEsService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/main/info/weather")
+public class SseWeatherController {
+
+    private final WeatherEsService weatherEsService;
+    private final List emitters = new CopyOnWriteArrayList<>();
+
+    @GetMapping("/stream")
+    public SseEmitter streamWeather() {
+        SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음
+        emitters.add(emitter);
+
+        emitter.onCompletion(() -> emitters.remove(emitter));
+        emitter.onTimeout(() -> emitters.remove(emitter));
+        emitter.onError((e) -> emitters.remove(emitter));
+
+        return emitter;
+    }
+
+    // 주기적으로 클라이언트에게 push
+    public void sendToClients(List> weatherList) {
+        for (SseEmitter emitter : emitters) {
+            try {
+                emitter.send(SseEmitter.event()
+                        .name("weather-update")
+                        .data(weatherList));
+            } catch (IOException e) {
+                emitter.completeWithError(e);
+                emitters.remove(emitter);
+            }
+        }
+    }
+}
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java
index 14db029..390a254 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java
@@ -1,26 +1,29 @@
 package com.example.externalinfoservice.controller;
 
-import com.example.externalinfoservice.dto.WeatherRequest;
 import com.example.externalinfoservice.service.WeatherEsService;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.List;
 import java.util.Map;
 
 @RestController
 @RequiredArgsConstructor
-@RequestMapping("/main/info")
+@RequestMapping("/main/info/weather")
 public class WeatherEsController {
 
     private final WeatherEsService weatherEsService;
 
-    @GetMapping("/weather")
-    public ResponseEntity> getWeatherByArea(@RequestParam String area) {
-        Map weather = weatherEsService.getWeatherFromES(area);
-        return weather != null
-                ? ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(weather)
-                : ResponseEntity.notFound().build();
+    @GetMapping(value = {"/{placeCode}", ""}, produces = MediaType.APPLICATION_JSON_VALUE)
+    public ResponseEntity getWeather(@PathVariable(name = "placeCode", required = false) String placeCode) {
+        if (placeCode == null || placeCode.isBlank()) {
+            List> allWeather = weatherEsService.getAllWeatherFromES();
+            return ResponseEntity.ok(allWeather);
+        }
+
+        Map weather = weatherEsService.getWeatherFromES(placeCode);
+        return weather != null ? ResponseEntity.ok(weather) : ResponseEntity.notFound().build();
     }
 }
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java
new file mode 100644
index 0000000..3f7f58a
--- /dev/null
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java
@@ -0,0 +1,22 @@
+package com.example.externalinfoservice.scheduler;
+
+import com.example.externalinfoservice.controller.SseWeatherController;
+import com.example.externalinfoservice.service.WeatherEsService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class WeatherPushScheduler {
+
+    private final WeatherEsService weatherEsService;
+    private final SseWeatherController sseWeatherController;
+
+    // 5분마다 날씨 데이터를 SSE 클라이언트에 push
+    @Scheduled(fixedRate = 10_000) // 5분 = 300초
+    public void pushWeatherToClients() {
+        var weatherList = weatherEsService.getAllWeatherFromES();
+        sseWeatherController.sendToClients(weatherList);
+    }
+}
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java
index 4856ec4..e912d91 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java
@@ -1,6 +1,5 @@
 package com.example.externalinfoservice.service;
 
-import com.example.externalinfoservice.dto.WeatherRequest;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.*;
 import org.springframework.stereotype.Service;
@@ -14,12 +13,13 @@ public class WeatherEsService {
 
     private final RestTemplate restTemplate = new RestTemplate();
 
+    // 특정 지역 날씨
     public Map getWeatherFromES(String area) {
-        String url = "http://elasticsearch.seoultravel.life/seoul_citydata_weather/_search";
+        String url = "http://elasticsearch.seoultravel.life/seoul_citydata_weather_20250424/_search";
 
         Map term = Map.of("weather.area_nm.keyword", area);
         Map query = Map.of("term", term);
-        Map body = Map.of("size", 1, "query", query);
+        Map body = Map.of("size", 100, "query", query);
 
         HttpHeaders headers = new HttpHeaders();
         headers.setContentType(MediaType.APPLICATION_JSON);
@@ -36,4 +36,50 @@ public Map getWeatherFromES(String area) {
         return (Map) source.get("weather");
     }
 
+    // 전체 지역 날씨
+    public List> getAllWeatherFromES() {
+        String url = "http://elasticsearch.seoultravel.life/seoul_citydata_weather_20250424/_search";
+
+        Map body = Map.of(
+                "size", 0,
+                "aggs", Map.of(
+                        "by_area", Map.of(
+                                "terms", Map.of("field", "weather.area_nm.keyword", "size", 1000),
+                                "aggs", Map.of(
+                                        "latest_hit", Map.of(
+                                                "top_hits", Map.of(
+                                                        "size", 1,
+                                                        "sort", List.of(Map.of("weather.weather_time", Map.of("order", "desc")))
+                                                )
+                                        )
+                                )
+                        )
+                )
+        );
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+
+        HttpEntity> entity = new HttpEntity<>(body, headers);
+        ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);
+
+        Map aggregations = (Map) response.getBody().get("aggregations");
+        Map byArea = (Map) aggregations.get("by_area");
+        List> buckets = (List>) byArea.get("buckets");
+
+        List> results = new ArrayList<>();
+        for (Map bucket : buckets) {
+            Map latestHitWrapper = (Map) bucket.get("latest_hit");
+            Map hitsWrapper = (Map) latestHitWrapper.get("hits");
+            List> hits = (List>) hitsWrapper.get("hits");
+            if (hits.isEmpty()) continue;
+
+            Map latestHit = hits.get(0);
+            Map source = (Map) latestHit.get("_source");
+            results.add((Map) source.get("weather"));
+        }
+
+        return results;
+    }
+
 }
diff --git a/external-info-service/src/main/resources/static/weather-stream-test.html b/external-info-service/src/main/resources/static/weather-stream-test.html
new file mode 100644
index 0000000..d4a2095
--- /dev/null
+++ b/external-info-service/src/main/resources/static/weather-stream-test.html
@@ -0,0 +1,22 @@
+
+
+
+
+    
+    날씨 SSE 테스트
+
+
+

날씨 실시간 수신 중...

+

+
+
+
+

From 1eb56e89d6ae8c480b26ae1ef205f8b84e8815b5 Mon Sep 17 00:00:00 2001
From: suminmmmmmmm 
Date: Thu, 24 Apr 2025 15:23:12 +0900
Subject: [PATCH 106/361] =?UTF-8?q?Refactor:=20=EB=82=A0=EC=94=A8=20?=
 =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?=
 =?UTF-8?q?=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../externalinfoservice/ExternalInfoServiceApplication.java     | 1 +
 .../externalinfoservice/controller/SseWeatherController.java    | 1 +
 .../externalinfoservice/controller/WeatherEsController.java     | 1 +
 .../externalinfoservice/scheduler/WeatherPushScheduler.java     | 1 +
 .../example/externalinfoservice/service/WeatherEsService.java   | 1 +
 .../src/main/resources/static/weather-stream-test.html          | 2 +-
 6 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java
index cd08ab0..d01da14 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java
@@ -10,6 +10,7 @@ public class ExternalInfoServiceApplication {
 
     public static void main(String[] args) {
 
+        
         SpringApplication.run(ExternalInfoServiceApplication.class, args);
     }
 
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java
index d37f517..ecd9589 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java
@@ -9,6 +9,7 @@
 import java.util.*;
 import java.util.concurrent.CopyOnWriteArrayList;
 
+
 @RestController
 @RequiredArgsConstructor
 @RequestMapping("/main/info/weather")
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java
index 390a254..2f146b7 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java
@@ -14,6 +14,7 @@
 @RequestMapping("/main/info/weather")
 public class WeatherEsController {
 
+    
     private final WeatherEsService weatherEsService;
 
     @GetMapping(value = {"/{placeCode}", ""}, produces = MediaType.APPLICATION_JSON_VALUE)
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java
index 3f7f58a..2017ea7 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java
@@ -19,4 +19,5 @@ public void pushWeatherToClients() {
         var weatherList = weatherEsService.getAllWeatherFromES();
         sseWeatherController.sendToClients(weatherList);
     }
+
 }
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java
index e912d91..a323a02 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java
@@ -11,6 +11,7 @@
 @RequiredArgsConstructor
 public class WeatherEsService {
 
+        
     private final RestTemplate restTemplate = new RestTemplate();
 
     // 특정 지역 날씨
diff --git a/external-info-service/src/main/resources/static/weather-stream-test.html b/external-info-service/src/main/resources/static/weather-stream-test.html
index d4a2095..1a4385c 100644
--- a/external-info-service/src/main/resources/static/weather-stream-test.html
+++ b/external-info-service/src/main/resources/static/weather-stream-test.html
@@ -6,7 +6,7 @@
     날씨 SSE 테스트
 
 
-

날씨 실시간 수신 중...

+

날씨 실시간 수신 중


 
 
 
 
diff --git a/place-service/build.gradle b/place-service/build.gradle
index 812c39b..07bd32c 100644
--- a/place-service/build.gradle
+++ b/place-service/build.gradle
@@ -38,7 +38,7 @@ dependencies {
     implementation 'org.postgresql:postgresql:42.7.2'
     implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
     implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
-    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+    //implementation 'org.springframework.boot:spring-boot-starter-data-redis'
     implementation 'org.springframework.boot:spring-boot-starter-security'
     implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
     implementation 'org.springframework.boot:spring-boot-starter-web'
diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
index c497a02..a2e8892 100644
--- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
+++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
@@ -1,6 +1,7 @@
 package com.example.placeservice;
 
 import com.example.placeservice.repository.RestaurantRepository;
+import com.example.placeservice.repository.CafeRepository;
 import com.example.placeservice.service.RestaurantService;
 import com.example.placeservice.service.CafeService;
 import org.springframework.boot.CommandLineRunner;
@@ -22,18 +23,29 @@ public static void main(String[] args) {
         SpringApplication.run(PlaceServiceApplication.class, args);
     }
 
-
     @Bean
-    public CommandLineRunner initData(RestaurantService restaurantService, RestaurantRepository restaurantRepository, CafeService cafeService) {
+    public CommandLineRunner initData(
+            RestaurantService restaurantService,
+            RestaurantRepository restaurantRepository,
+            CafeService cafeService,
+            CafeRepository cafeRepository
+    ) {
         return args -> {
-            cafeService.processAllAreas();
             if (restaurantRepository.count() > 0) {
-                System.out.println("");
+                System.out.println("음식점 데이터가 이미 존재합니다. 건너뜁니다.");
             } else {
                 System.out.println("음식점 데이터 저장 시작");
                 restaurantService.fetchAndSaveRestaurants();
                 System.out.println("음식점 데이터 저장 완료");
             }
+
+            if (cafeRepository.count() == 0) {
+                System.out.println("카페 데이터 저장 시작");
+                cafeService.processAllAreas();
+                System.out.println("카페 데이터 저장 완료");
+            } else {
+                System.out.println("카페 데이터가 이미 존재합니다. 건너뜁니다.");
+            }
         };
     }
 
diff --git a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java
index 7dfcfc4..2b448b0 100644
--- a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java
+++ b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java
@@ -1,54 +1,96 @@
 package com.example.placeservice.controller;
 
+import com.example.placeservice.dto.cafe.CafeDto;
+import com.example.placeservice.entity.Cafe;
 import com.example.placeservice.service.CafeService;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.RequiredArgsConstructor;
+import com.example.placeservice.repository.CafeRepository;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
+import java.util.stream.Collectors;
 
 @Slf4j
 @RestController
-@RequestMapping("/cafes")
+@RequestMapping("/main")
 @RequiredArgsConstructor
 public class CafeController {
 
+    private final CafeRepository cafeRepository;
     private final CafeService cafeService;
 
     /**
-     * 모든 장소의 주변 카페 데이터 처리
+     * 전체 카페 목록 조회
      */
-    @PostMapping("/process-all")
-    public ResponseEntity> processAllAreas() {
-        Map response = new HashMap<>();
+    @GetMapping("/cafe/list")
+    public ResponseEntity>> getCafeList() {
+        log.info("카페 목록 조회");
+        List cafes = cafeRepository.findAll();
 
-        try {
-            cafeService.processAllAreas();
+        // ID를 기준으로 정렬
+        List> cafeList = cafes.stream()
+                .sorted(Comparator.comparing(Cafe::getId))
+                .map(cafe -> {
+                    Map cafeMap = new LinkedHashMap<>();
+                    cafeMap.put("id", cafe.getId().toString());
+                    cafeMap.put("name", cafe.getName());
+                    cafeMap.put("x", cafe.getLon().toString());
+                    cafeMap.put("y", cafe.getLat().toString());
+                    return cafeMap;
+                })
+                .collect(Collectors.toList());
 
-            response.put("success", true);
-            response.put("message", "모든 장소의 주변 카페 정보 처리가 시작되었습니다.");
+        return ResponseEntity.ok(cafeList);
+    }
 
-            return ResponseEntity.ok(response);
-        } catch (Exception e) {
-            log.error("처리 중 오류 발생: {}", e.getMessage(), e);
+    /**
+     * 지역 ID를 기준으로 카페 목록 조회
+     */
+    @GetMapping("/cafe/list/{areaId}")
+    public ResponseEntity> getCafesByAreaId(@PathVariable Long areaId) {
+        log.info("지역 ID {}의 카페 목록 조회", areaId);
+        List cafes = cafeService.getCafesByAreaId(areaId);
 
-            response.put("success", false);
-            response.put("message", "오류 발생: " + e.getMessage());
+        // ID 기준으로 정렬
+        List sortedCafes = cafes.stream()
+                .sorted(Comparator.comparing(CafeDto::getId))
+                .collect(Collectors.toList());
 
-            return ResponseEntity.badRequest().body(response);
-        }
+        return ResponseEntity.ok(sortedCafes);
     }
 
-    /**
-     * 모든 카페 목록 조회
-     */
-    @GetMapping
-    public ResponseEntity>> getAllCafes() {
-        log.info("모든 카페 목록 조회");
-        List> cafes = cafeService.getAllCafesWithLimitedInfo();
-        return ResponseEntity.ok(cafes);
+    @GetMapping("/info/cafe/{cafeId}")
+    public ResponseEntity> getCafeInfo(@PathVariable Long cafeId) {
+        log.info("카페 ID {}의 상세 정보 조회", cafeId);
+
+        // 카페 ID로 상세 정보 조회
+        Optional cafeOptional = cafeRepository.findById(cafeId);
+
+        if (cafeOptional.isPresent()) {
+            Cafe cafe = cafeOptional.get();
+            Map cafeInfo = new LinkedHashMap<>();
+            cafeInfo.put("id", cafe.getId());
+            cafeInfo.put("name", cafe.getName());
+            cafeInfo.put("address", cafe.getAddress());
+            cafeInfo.put("phone", cafe.getPhone());
+            cafeInfo.put("category_code", cafe.getCategoryCode());
+            cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl());
+            cafeInfo.put("lat", cafe.getLat());
+            cafeInfo.put("lon", cafe.getLon());
+
+            Map response = new LinkedHashMap<>();
+            response.put("data", cafeInfo);
+
+            return ResponseEntity.ok(response);
+        }
+
+        return ResponseEntity.notFound().build();
     }
 }
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/controller/MainController.java b/place-service/src/main/java/com/example/placeservice/controller/MainController.java
deleted file mode 100644
index dfb49d6..0000000
--- a/place-service/src/main/java/com/example/placeservice/controller/MainController.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package com.example.placeservice.controller;
-
-import com.example.placeservice.dto.cafe.CafeDto;
-import com.example.placeservice.entity.Cafe;
-import com.example.placeservice.service.CafeService;
-import lombok.RequiredArgsConstructor;
-import com.example.placeservice.repository.CafeRepository;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import java.util.*;
-import java.util.stream.Collectors;
-
-@Slf4j
-@RestController
-@RequestMapping("/main")
-@RequiredArgsConstructor
-public class MainController {
-
-    private final CafeRepository cafeRepository;
-    private final CafeService cafeService;
-
-    /**
-     * 전체 카페 목록 조회
-     */
-    @GetMapping("/cafe/list")
-    public ResponseEntity>> getCafeList() {
-        log.info("카페 목록 조회");
-        List cafes = cafeRepository.findAll();
-
-        // ID를 기준으로 정렬
-        List> cafeList = cafes.stream()
-                .sorted(Comparator.comparing(Cafe::getId))
-                .map(cafe -> {
-                    Map cafeMap = new LinkedHashMap<>();
-                    cafeMap.put("id", cafe.getId().toString());
-                    cafeMap.put("name", cafe.getName());
-                    cafeMap.put("x", cafe.getLon().toString());
-                    cafeMap.put("y", cafe.getLat().toString());
-                    return cafeMap;
-                })
-                .collect(Collectors.toList());
-
-        return ResponseEntity.ok(cafeList);
-    }
-
-    /**
-     * 지역 ID를 기준으로 카페 목록 조회
-     */
-    @GetMapping("/cafe/list/{areaId}")
-    public ResponseEntity> getCafesByAreaId(@PathVariable Long areaId) {
-        log.info("지역 ID {}의 카페 목록 조회", areaId);
-        List cafes = cafeService.getCafesByAreaId(areaId);
-
-        // ID 기준으로 정렬
-        List sortedCafes = cafes.stream()
-                .sorted(Comparator.comparing(CafeDto::getId))
-                .collect(Collectors.toList());
-
-        return ResponseEntity.ok(sortedCafes);
-    }
-
-    @GetMapping("/info/cafe/{cafeId}")
-    public ResponseEntity> getCafeInfo(@PathVariable Long cafeId) {
-        log.info("카페 ID {}의 상세 정보 조회", cafeId);
-
-        // 카페 ID로 상세 정보 조회
-        Optional cafeOptional = cafeRepository.findById(cafeId);
-
-        if (cafeOptional.isPresent()) {
-            Cafe cafe = cafeOptional.get();
-            Map cafeInfo = new LinkedHashMap<>();
-            cafeInfo.put("id", cafe.getId());
-            cafeInfo.put("name", cafe.getName());
-            cafeInfo.put("address", cafe.getAddress());
-            cafeInfo.put("phone", cafe.getPhone());
-            cafeInfo.put("category_code", cafe.getCategoryCode());
-            cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl());
-            cafeInfo.put("lat", cafe.getLat());
-            cafeInfo.put("lon", cafe.getLon());
-
-            Map response = new LinkedHashMap<>();
-            response.put("data", cafeInfo);
-
-            return ResponseEntity.ok(response);
-        }
-
-        return ResponseEntity.notFound().build();
-    }
-}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java b/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java
index ac908a2..7456e8e 100644
--- a/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java
+++ b/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java
@@ -1,5 +1,6 @@
 package com.example.placeservice.controller;
 
+
 import com.example.placeservice.dto.restaurant.RestaurantDetailResponse;
 import com.example.placeservice.dto.restaurant.RestaurantListResponse;
 import com.example.placeservice.service.RestaurantListService;
diff --git a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationResponse.java b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationResponse.java
index 48f29f8..3b375ff 100644
--- a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationResponse.java
+++ b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationResponse.java
@@ -9,7 +9,7 @@
 import java.util.List;
 
 @Data
-@XmlRootElement(name = "OpenAPI_ServiceResponse")
+@XmlRootElement(name = "response")
 @XmlAccessorType(XmlAccessType.FIELD)
 public class AccommodationResponse {
     @XmlElement(name = "header")
diff --git a/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeDto.java b/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeDto.java
index 4da5aa8..60264c9 100644
--- a/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeDto.java
+++ b/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeDto.java
@@ -1,5 +1,6 @@
 package com.example.placeservice.dto.cafe;
 
+
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java
index f107e54..d8f94b3 100644
--- a/place-service/src/main/java/com/example/placeservice/entity/Area.java
+++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java
@@ -38,4 +38,6 @@ public class Area {
     @OneToMany(mappedBy = "area", fetch = FetchType.LAZY)
     @JsonIgnore
     private List attractions;
+
+
 }
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Cafe.java b/place-service/src/main/java/com/example/placeservice/entity/Cafe.java
index 2dffa74..b7e6db9 100644
--- a/place-service/src/main/java/com/example/placeservice/entity/Cafe.java
+++ b/place-service/src/main/java/com/example/placeservice/entity/Cafe.java
@@ -1,5 +1,6 @@
 package com.example.placeservice.entity;
 
+
 import jakarta.persistence.*;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
diff --git a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java
index 2ac5555..698864c 100644
--- a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java
+++ b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java
@@ -7,6 +7,8 @@
 import java.util.List;
 
 public interface AreaRepository extends JpaRepository {
+
+
     @Query("SELECT a.name FROM Area a")
     List findAllAreaNames();
 }
diff --git a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java
index fcacbe1..51c1a6d 100644
--- a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java
+++ b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java
@@ -18,4 +18,4 @@ public interface CafeRepository extends JpaRepository {
     void deleteByAreaId(Long areaId);
 
     Optional findByKakaomapUrl(String placeCode);
-}
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java
index b331b2d..6f04f0a 100644
--- a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java
+++ b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java
@@ -5,4 +5,5 @@
 
 public interface RestaurantRepository extends JpaRepository {
     // 기본 CRUD 사용 (findAll, findById, save 등)
+
 }
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/AreaService.java b/place-service/src/main/java/com/example/placeservice/service/AreaService.java
index 716de96..c3edbaa 100644
--- a/place-service/src/main/java/com/example/placeservice/service/AreaService.java
+++ b/place-service/src/main/java/com/example/placeservice/service/AreaService.java
@@ -5,14 +5,17 @@
 import com.example.placeservice.repository.AreaRepository;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.List;
 
 @Service
 @RequiredArgsConstructor
+@Transactional
 public class AreaService {
     private final AreaRepository areaRepository;
 
+
     public List getAreaData() {
         try{
             List areas = areaRepository.findAll();
diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java
index e1a8529..ffdb843 100644
--- a/place-service/src/main/java/com/example/placeservice/service/CafeService.java
+++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java
@@ -27,30 +27,66 @@ public class CafeService {
     private final CafeRepository cafeRepository;
     private final KakaoApiService kakaoApiService;
 
+    // 모든 지역에 대한 카페 처리가 완료되었는지 확인하는 플래그
+    private boolean processingCompleted = false;
+
     /**
      * 모든 장소에 대해 주변 카페 데이터 처리
      */
     @Async
+    @Transactional
     public void processAllAreas() {
-        List areas = areaRepository.findAll();
-        log.info("총 {}개 장소에 대한 카페 정보를 처리합니다.", areas.size());
+        log.info("processAllAreas() 메서드 시작");
 
-        for (int i = 0; i < areas.size(); i++) {
-            Area area = areas.get(i);
-            log.info("[{}/{}] '{}' (ID: {}) 처리 중...",
-                    i + 1, areas.size(), area.getName(), area.getAreaId());
+        // 이미 처리가 완료되었거나 이미 데이터가 있으면 다시 실행하지 않음
+        if (processingCompleted) {
+            log.info("이미 처리가 완료되었습니다. 중복 실행 방지를 위해 종료합니다.");
+            return;
+        }
 
-            processSingleArea(area);
+        long cafeCount = cafeRepository.count();
+        log.info("현재 DB에 저장된 카페 수: {}", cafeCount);
 
-            // API 호출 간격 조절
-            try {
-                Thread.sleep(300);
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-            }
+        if (cafeCount > 0) {
+            log.info("카페 데이터가 이미 존재합니다. 처리를 건너뜁니다.");
+            processingCompleted = true;
+            return;
         }
 
-        log.info("모든 장소 처리 완료.");
+        try {
+            List areas = areaRepository.findAll();
+            log.info("총 {}개 장소에 대한 카페 정보를 처리합니다.", areas.size());
+
+            // 다른 처리 중 중복 실행 방지
+            processingCompleted = true;
+
+            log.info("지역별 카페 정보 처리 시작");
+            for (int i = 0; i < areas.size(); i++) {
+                Area area = areas.get(i);
+                log.info("[{}/{}] '{}' (ID: {}) 처리 중...",
+                        i + 1, areas.size(), area.getName(), area.getAreaId());
+
+                processSingleArea(area);
+
+                // API 호출 간격 조절
+                try {
+                    Thread.sleep(300);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    log.error("스레드 인터럽트 발생", e);
+                }
+            }
+
+            log.info("모든 장소 처리 완료. 총 처리된 지역: {}", areas.size());
+
+            // 최종 카페 수 확인
+            log.info("최종 저장된 카페 수: {}", cafeRepository.count());
+
+        } catch (Exception e) {
+            processingCompleted = false; // 오류 발생 시 플래그 재설정
+            log.error("카페 데이터 처리 중 오류 발생: {}", e.getMessage(), e);
+            throw e;
+        }
     }
 
     /**
@@ -58,48 +94,103 @@ public void processAllAreas() {
      */
     @Transactional
     public void processSingleArea(Area area) {
+        log.info("processSingleArea() 시작 - 지역: '{}' (ID: {})", area.getName(), area.getAreaId());
+
         try {
             // 해당 지역의 기존 카페 데이터 삭제
+            log.info("지역 ID {}의 기존 카페 데이터 삭제 시도", area.getAreaId());
             cafeRepository.deleteByAreaId(area.getAreaId());
+            log.info("지역 ID {}의 기존 카페 데이터 삭제 완료", area.getAreaId());
 
             // 주변 카페 검색
             double latitude = area.getLat().doubleValue();
             double longitude = area.getLon().doubleValue();
 
             if (latitude == 0 || longitude == 0) {
-                log.warn("장소 ID {}의 위도/경도 정보가 없습니다.", area.getAreaId());
+                log.warn("장소 ID {}의 위도/경도 정보가 없습니다. 위도: {}, 경도: {}",
+                        area.getAreaId(), latitude, longitude);
                 return;
             }
 
+            log.info("카카오 API 호출 - 위도: {}, 경도: {}, 반경: 1000m", latitude, longitude);
             List cafes =
                     kakaoApiService.findCafesNearby(latitude, longitude, 1000);
+            log.info("카카오 API 응답 - 총 {}개의 장소 정보 수신", cafes.size());
 
             // "음식점 > 카페"가 포함된 카페만 필터링하고 "(휴업중)" 카페는 제외
             List filteredCafes = cafes.stream()
-                    .filter(cafe -> cafe.getCategoryName() != null && cafe.getCategoryName().contains("음식점 > 카페"))
-                    .filter(cafe -> cafe.getPlaceName() == null || !cafe.getPlaceName().contains("(휴업중)"))
+                    .filter(cafe -> {
+                        boolean categoryMatch = cafe.getCategoryName() != null &&
+                                cafe.getCategoryName().contains("음식점 > 카페");
+                        if (!categoryMatch) {
+                            log.debug("카테고리 불일치로 제외: {}, 카테고리: {}",
+                                    cafe.getPlaceName(), cafe.getCategoryName());
+                        }
+                        return categoryMatch;
+                    })
+                    .filter(cafe -> {
+                        boolean notClosed = cafe.getPlaceName() == null ||
+                                !cafe.getPlaceName().contains("(휴업중)");
+                        if (!notClosed) {
+                            log.debug("휴업중으로 제외: {}", cafe.getPlaceName());
+                        }
+                        return notClosed;
+                    })
                     .collect(Collectors.toList());
 
             log.info("'{}' 주변 총 {}개의 카페 중 {}개의 '음식점 > 카페' 카테고리(휴업중 제외)를 찾았습니다.",
                     area.getName(), cafes.size(), filteredCafes.size());
 
+            // 최대 15개의 카페만 저장 (음식점과 동일한 로직 적용)
+            int savedCount = 0;
+            int maxCafesToSave = 15;
+
             // 필터링된 카페 정보 저장
             for (KakaoApiService.CafeResponse.Document cafeDoc : filteredCafes) {
-                Cafe cafe = Cafe.builder()
-                        .name(cafeDoc.getPlaceName())
-                        .address(cafeDoc.getAddressName())
-                        .lat(new BigDecimal(cafeDoc.getY()))
-                        .lon(new BigDecimal(cafeDoc.getX()))
-                        .phone(cafeDoc.getPhone())
-                        .kakaomapUrl(cafeDoc.getPlaceUrl())
-                        .categoryCode(cafeDoc.getCategoryGroupCode())
-                        .area(area)
-                        .build();
-
-                cafeRepository.save(cafe);
+                // 최대 카페 저장 개수 제한
+                if (savedCount >= maxCafesToSave) {
+                    log.info("최대 저장 개수({})에 도달하여 저장 중단", maxCafesToSave);
+                    break;
+                }
+
+                log.info("카페 저장 시도: {}, URL: {}", cafeDoc.getPlaceName(), cafeDoc.getPlaceUrl());
+
+                // 이미 동일한 카페가 있는지 확인 (kakaomapUrl로 중복 체크)
+                Optional existingCafe = cafeRepository.findByKakaomapUrl(cafeDoc.getPlaceUrl());
+                if (existingCafe.isPresent()) {
+                    log.info("이미 존재하는 카페이므로 건너뜀: {}, ID: {}",
+                            existingCafe.get().getName(), existingCafe.get().getId());
+                    continue; // 이미 존재하는 카페는 건너뜀
+                }
+
+                try {
+                    Cafe cafe = Cafe.builder()
+                            .name(cafeDoc.getPlaceName())
+                            .address(cafeDoc.getAddressName())
+                            .lat(new BigDecimal(cafeDoc.getY()))
+                            .lon(new BigDecimal(cafeDoc.getX()))
+                            .phone(cafeDoc.getPhone())
+                            .kakaomapUrl(cafeDoc.getPlaceUrl())
+                            .categoryCode(cafeDoc.getCategoryGroupCode())
+                            .area(area)
+                            .build();
+
+                    log.info("카페 객체 생성 완료: {}", cafe.getName());
+                    Cafe savedCafe = cafeRepository.save(cafe);
+                    log.info("카페 저장 완료: {}, 저장된 ID: {}", savedCafe.getName(), savedCafe.getId());
+
+                    savedCount++;
+                } catch (Exception e) {
+                    log.error("카페 저장 중 오류 발생: {}, 오류: {}", cafeDoc.getPlaceName(), e.getMessage(), e);
+                }
             }
+
+            log.info("지역 '{}' (ID: {})에 총 {}개의 카페 저장 완료",
+                    area.getName(), area.getAreaId(), savedCount);
+
         } catch (Exception e) {
             log.error("장소 ID {} 처리 중 오류: {}", area.getAreaId(), e.getMessage(), e);
+            throw e; // 상위 메서드에서 처리할 수 있도록 예외 전파
         }
     }
 
@@ -108,7 +199,10 @@ public void processSingleArea(Area area) {
      */
     @Transactional(readOnly = true)
     public List getCafesByAreaId(Long areaId) {
+        log.info("지역 ID {}의 카페 목록 조회", areaId);
         List cafes = cafeRepository.findByAreaId(areaId);
+        log.info("지역 ID {}에서 총 {}개의 카페 찾음", areaId, cafes.size());
+
         return cafes.stream()
                 .map(this::convertToDto)
                 .collect(Collectors.toList());
@@ -119,7 +213,10 @@ public List getCafesByAreaId(Long areaId) {
      */
     @Transactional(readOnly = true)
     public List getAllCafes() {
+        log.info("모든 카페 목록 조회");
         List cafes = cafeRepository.findAll();
+        log.info("총 {}개의 카페 조회됨", cafes.size());
+
         return cafes.stream()
                 .map(this::convertToDto)
                 .collect(Collectors.toList());
@@ -130,7 +227,10 @@ public List getAllCafes() {
      */
     @Transactional(readOnly = true)
     public List> getAllCafesWithLimitedInfo() {
+        log.info("제한된 정보로 모든 카페 목록 조회");
         List cafes = cafeRepository.findAll();
+        log.info("총 {}개의 카페 조회됨", cafes.size());
+
         return cafes.stream()
                 .map(cafe -> {
                     Map cafeMap = new HashMap<>();
@@ -161,10 +261,13 @@ private CafeDto convertToDto(Cafe cafe) {
 
     @Transactional(readOnly = true)
     public Map getCafeDetailByPlaceCode(String placeCode) {
+        log.info("장소 코드로 카페 상세 정보 조회: {}", placeCode);
         Optional cafeOptional = cafeRepository.findByKakaomapUrl(placeCode);
 
         if (cafeOptional.isPresent()) {
             Cafe cafe = cafeOptional.get();
+            log.info("카페 찾음: {}, ID: {}", cafe.getName(), cafe.getId());
+
             Map cafeInfo = new HashMap<>();
             cafeInfo.put("name", cafe.getName());
             cafeInfo.put("address", cafe.getAddress());
@@ -177,6 +280,7 @@ public Map getCafeDetailByPlaceCode(String placeCode) {
             return cafeInfo;
         }
 
+        log.info("장소 코드 {}에 해당하는 카페를 찾을 수 없음", placeCode);
         return null;
     }
 }
\ No newline at end of file
diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties
index dc1e851..dac0e95 100644
--- a/place-service/src/main/resources/application.properties
+++ b/place-service/src/main/resources/application.properties
@@ -9,12 +9,13 @@ spring.datasource.driver-class-name=org.postgresql.Driver
 
 spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
 spring.jpa.defer-datasource-initialization=true
-spring.jpa.hibernate.ddl-auto=create
+spring.jpa.hibernate.ddl-auto=update
 #spring.jpa.hibernate.ddl-auto=update
 spring.jpa.show-sql=true
 spring.sql.init.mode=always
 
 
+
 #API
 kakao.api.key=${KAKAO_API_KEY}
 

From cc842350c8623531036582c4f6aed9deac42cfd3 Mon Sep 17 00:00:00 2001
From: suminmmmmmmm 
Date: Sat, 26 Apr 2025 10:00:44 +0900
Subject: [PATCH 123/361] =?UTF-8?q?Refactor:=20=EC=9D=8C=EC=8B=9D=EC=A0=90?=
 =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A4=91=EB=B3=B5=20=EC=82=BD?=
 =?UTF-8?q?=EC=9E=85=20=ED=95=B4=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/SseWeatherController.java      |   2 +-
 .../controller/WeatherEsController.java       |   4 +-
 .../service/WeatherEsService.java             |   4 +-
 .../resources/static/weather-stream-test.html |  20 ++-
 place-service/build.gradle                    |   2 +-
 .../placeservice/PlaceServiceApplication.java |  20 ++-
 .../controller/CafeController.java            |  98 ++++++++---
 .../controller/MainController.java            |  94 ----------
 .../controller/RestaurantController.java      |   1 +
 .../accommodation/AccommodationResponse.java  |   2 +-
 .../placeservice/dto/cafe/CafeDto.java        |   1 +
 .../com/example/placeservice/entity/Area.java |   2 +
 .../com/example/placeservice/entity/Cafe.java |   1 +
 .../repository/AreaRepository.java            |   2 +
 .../repository/CafeRepository.java            |   2 +-
 .../repository/RestaurantRepository.java      |   1 +
 .../placeservice/service/AreaService.java     |   3 +
 .../placeservice/service/CafeService.java     | 162 ++++++++++++++----
 .../src/main/resources/application.properties |   3 +-
 19 files changed, 252 insertions(+), 172 deletions(-)
 delete mode 100644 place-service/src/main/java/com/example/placeservice/controller/MainController.java

diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java
index ecd9589..4ed3585 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java
@@ -43,4 +43,4 @@ public void sendToClients(List> weatherList) {
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java
index 2f146b7..4fd1553 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java
@@ -14,7 +14,7 @@
 @RequestMapping("/main/info/weather")
 public class WeatherEsController {
 
-    
+
     private final WeatherEsService weatherEsService;
 
     @GetMapping(value = {"/{placeCode}", ""}, produces = MediaType.APPLICATION_JSON_VALUE)
@@ -27,4 +27,4 @@ public ResponseEntity getWeather(@PathVariable(name = "placeCode", required =
         Map weather = weatherEsService.getWeatherFromES(placeCode);
         return weather != null ? ResponseEntity.ok(weather) : ResponseEntity.notFound().build();
     }
-}
+}
\ No newline at end of file
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java
index a323a02..5faa5c8 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java
@@ -11,7 +11,7 @@
 @RequiredArgsConstructor
 public class WeatherEsService {
 
-        
+
     private final RestTemplate restTemplate = new RestTemplate();
 
     // 특정 지역 날씨
@@ -83,4 +83,4 @@ public List> getAllWeatherFromES() {
         return results;
     }
 
-}
+}
\ No newline at end of file
diff --git a/external-info-service/src/main/resources/static/weather-stream-test.html b/external-info-service/src/main/resources/static/weather-stream-test.html
index 1a4385c..ff848ac 100644
--- a/external-info-service/src/main/resources/static/weather-stream-test.html
+++ b/external-info-service/src/main/resources/static/weather-stream-test.html
@@ -1,22 +1,26 @@
 
 
-
+
 
-    
-    날씨 SSE 테스트
+    SSE Test
 
 
-

날씨 실시간 수신 중

+

실시간 날씨 스트림


 
 
 
 
diff --git a/place-service/build.gradle b/place-service/build.gradle
index 812c39b..07bd32c 100644
--- a/place-service/build.gradle
+++ b/place-service/build.gradle
@@ -38,7 +38,7 @@ dependencies {
     implementation 'org.postgresql:postgresql:42.7.2'
     implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
     implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
-    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+    //implementation 'org.springframework.boot:spring-boot-starter-data-redis'
     implementation 'org.springframework.boot:spring-boot-starter-security'
     implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
     implementation 'org.springframework.boot:spring-boot-starter-web'
diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
index c497a02..a2e8892 100644
--- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
+++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
@@ -1,6 +1,7 @@
 package com.example.placeservice;
 
 import com.example.placeservice.repository.RestaurantRepository;
+import com.example.placeservice.repository.CafeRepository;
 import com.example.placeservice.service.RestaurantService;
 import com.example.placeservice.service.CafeService;
 import org.springframework.boot.CommandLineRunner;
@@ -22,18 +23,29 @@ public static void main(String[] args) {
         SpringApplication.run(PlaceServiceApplication.class, args);
     }
 
-
     @Bean
-    public CommandLineRunner initData(RestaurantService restaurantService, RestaurantRepository restaurantRepository, CafeService cafeService) {
+    public CommandLineRunner initData(
+            RestaurantService restaurantService,
+            RestaurantRepository restaurantRepository,
+            CafeService cafeService,
+            CafeRepository cafeRepository
+    ) {
         return args -> {
-            cafeService.processAllAreas();
             if (restaurantRepository.count() > 0) {
-                System.out.println("");
+                System.out.println("음식점 데이터가 이미 존재합니다. 건너뜁니다.");
             } else {
                 System.out.println("음식점 데이터 저장 시작");
                 restaurantService.fetchAndSaveRestaurants();
                 System.out.println("음식점 데이터 저장 완료");
             }
+
+            if (cafeRepository.count() == 0) {
+                System.out.println("카페 데이터 저장 시작");
+                cafeService.processAllAreas();
+                System.out.println("카페 데이터 저장 완료");
+            } else {
+                System.out.println("카페 데이터가 이미 존재합니다. 건너뜁니다.");
+            }
         };
     }
 
diff --git a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java
index 7dfcfc4..2b448b0 100644
--- a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java
+++ b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java
@@ -1,54 +1,96 @@
 package com.example.placeservice.controller;
 
+import com.example.placeservice.dto.cafe.CafeDto;
+import com.example.placeservice.entity.Cafe;
 import com.example.placeservice.service.CafeService;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.RequiredArgsConstructor;
+import com.example.placeservice.repository.CafeRepository;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
+import java.util.stream.Collectors;
 
 @Slf4j
 @RestController
-@RequestMapping("/cafes")
+@RequestMapping("/main")
 @RequiredArgsConstructor
 public class CafeController {
 
+    private final CafeRepository cafeRepository;
     private final CafeService cafeService;
 
     /**
-     * 모든 장소의 주변 카페 데이터 처리
+     * 전체 카페 목록 조회
      */
-    @PostMapping("/process-all")
-    public ResponseEntity> processAllAreas() {
-        Map response = new HashMap<>();
+    @GetMapping("/cafe/list")
+    public ResponseEntity>> getCafeList() {
+        log.info("카페 목록 조회");
+        List cafes = cafeRepository.findAll();
 
-        try {
-            cafeService.processAllAreas();
+        // ID를 기준으로 정렬
+        List> cafeList = cafes.stream()
+                .sorted(Comparator.comparing(Cafe::getId))
+                .map(cafe -> {
+                    Map cafeMap = new LinkedHashMap<>();
+                    cafeMap.put("id", cafe.getId().toString());
+                    cafeMap.put("name", cafe.getName());
+                    cafeMap.put("x", cafe.getLon().toString());
+                    cafeMap.put("y", cafe.getLat().toString());
+                    return cafeMap;
+                })
+                .collect(Collectors.toList());
 
-            response.put("success", true);
-            response.put("message", "모든 장소의 주변 카페 정보 처리가 시작되었습니다.");
+        return ResponseEntity.ok(cafeList);
+    }
 
-            return ResponseEntity.ok(response);
-        } catch (Exception e) {
-            log.error("처리 중 오류 발생: {}", e.getMessage(), e);
+    /**
+     * 지역 ID를 기준으로 카페 목록 조회
+     */
+    @GetMapping("/cafe/list/{areaId}")
+    public ResponseEntity> getCafesByAreaId(@PathVariable Long areaId) {
+        log.info("지역 ID {}의 카페 목록 조회", areaId);
+        List cafes = cafeService.getCafesByAreaId(areaId);
 
-            response.put("success", false);
-            response.put("message", "오류 발생: " + e.getMessage());
+        // ID 기준으로 정렬
+        List sortedCafes = cafes.stream()
+                .sorted(Comparator.comparing(CafeDto::getId))
+                .collect(Collectors.toList());
 
-            return ResponseEntity.badRequest().body(response);
-        }
+        return ResponseEntity.ok(sortedCafes);
     }
 
-    /**
-     * 모든 카페 목록 조회
-     */
-    @GetMapping
-    public ResponseEntity>> getAllCafes() {
-        log.info("모든 카페 목록 조회");
-        List> cafes = cafeService.getAllCafesWithLimitedInfo();
-        return ResponseEntity.ok(cafes);
+    @GetMapping("/info/cafe/{cafeId}")
+    public ResponseEntity> getCafeInfo(@PathVariable Long cafeId) {
+        log.info("카페 ID {}의 상세 정보 조회", cafeId);
+
+        // 카페 ID로 상세 정보 조회
+        Optional cafeOptional = cafeRepository.findById(cafeId);
+
+        if (cafeOptional.isPresent()) {
+            Cafe cafe = cafeOptional.get();
+            Map cafeInfo = new LinkedHashMap<>();
+            cafeInfo.put("id", cafe.getId());
+            cafeInfo.put("name", cafe.getName());
+            cafeInfo.put("address", cafe.getAddress());
+            cafeInfo.put("phone", cafe.getPhone());
+            cafeInfo.put("category_code", cafe.getCategoryCode());
+            cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl());
+            cafeInfo.put("lat", cafe.getLat());
+            cafeInfo.put("lon", cafe.getLon());
+
+            Map response = new LinkedHashMap<>();
+            response.put("data", cafeInfo);
+
+            return ResponseEntity.ok(response);
+        }
+
+        return ResponseEntity.notFound().build();
     }
 }
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/controller/MainController.java b/place-service/src/main/java/com/example/placeservice/controller/MainController.java
deleted file mode 100644
index dfb49d6..0000000
--- a/place-service/src/main/java/com/example/placeservice/controller/MainController.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package com.example.placeservice.controller;
-
-import com.example.placeservice.dto.cafe.CafeDto;
-import com.example.placeservice.entity.Cafe;
-import com.example.placeservice.service.CafeService;
-import lombok.RequiredArgsConstructor;
-import com.example.placeservice.repository.CafeRepository;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import java.util.*;
-import java.util.stream.Collectors;
-
-@Slf4j
-@RestController
-@RequestMapping("/main")
-@RequiredArgsConstructor
-public class MainController {
-
-    private final CafeRepository cafeRepository;
-    private final CafeService cafeService;
-
-    /**
-     * 전체 카페 목록 조회
-     */
-    @GetMapping("/cafe/list")
-    public ResponseEntity>> getCafeList() {
-        log.info("카페 목록 조회");
-        List cafes = cafeRepository.findAll();
-
-        // ID를 기준으로 정렬
-        List> cafeList = cafes.stream()
-                .sorted(Comparator.comparing(Cafe::getId))
-                .map(cafe -> {
-                    Map cafeMap = new LinkedHashMap<>();
-                    cafeMap.put("id", cafe.getId().toString());
-                    cafeMap.put("name", cafe.getName());
-                    cafeMap.put("x", cafe.getLon().toString());
-                    cafeMap.put("y", cafe.getLat().toString());
-                    return cafeMap;
-                })
-                .collect(Collectors.toList());
-
-        return ResponseEntity.ok(cafeList);
-    }
-
-    /**
-     * 지역 ID를 기준으로 카페 목록 조회
-     */
-    @GetMapping("/cafe/list/{areaId}")
-    public ResponseEntity> getCafesByAreaId(@PathVariable Long areaId) {
-        log.info("지역 ID {}의 카페 목록 조회", areaId);
-        List cafes = cafeService.getCafesByAreaId(areaId);
-
-        // ID 기준으로 정렬
-        List sortedCafes = cafes.stream()
-                .sorted(Comparator.comparing(CafeDto::getId))
-                .collect(Collectors.toList());
-
-        return ResponseEntity.ok(sortedCafes);
-    }
-
-    @GetMapping("/info/cafe/{cafeId}")
-    public ResponseEntity> getCafeInfo(@PathVariable Long cafeId) {
-        log.info("카페 ID {}의 상세 정보 조회", cafeId);
-
-        // 카페 ID로 상세 정보 조회
-        Optional cafeOptional = cafeRepository.findById(cafeId);
-
-        if (cafeOptional.isPresent()) {
-            Cafe cafe = cafeOptional.get();
-            Map cafeInfo = new LinkedHashMap<>();
-            cafeInfo.put("id", cafe.getId());
-            cafeInfo.put("name", cafe.getName());
-            cafeInfo.put("address", cafe.getAddress());
-            cafeInfo.put("phone", cafe.getPhone());
-            cafeInfo.put("category_code", cafe.getCategoryCode());
-            cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl());
-            cafeInfo.put("lat", cafe.getLat());
-            cafeInfo.put("lon", cafe.getLon());
-
-            Map response = new LinkedHashMap<>();
-            response.put("data", cafeInfo);
-
-            return ResponseEntity.ok(response);
-        }
-
-        return ResponseEntity.notFound().build();
-    }
-}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java b/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java
index ac908a2..7456e8e 100644
--- a/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java
+++ b/place-service/src/main/java/com/example/placeservice/controller/RestaurantController.java
@@ -1,5 +1,6 @@
 package com.example.placeservice.controller;
 
+
 import com.example.placeservice.dto.restaurant.RestaurantDetailResponse;
 import com.example.placeservice.dto.restaurant.RestaurantListResponse;
 import com.example.placeservice.service.RestaurantListService;
diff --git a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationResponse.java b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationResponse.java
index 48f29f8..3b375ff 100644
--- a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationResponse.java
+++ b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationResponse.java
@@ -9,7 +9,7 @@
 import java.util.List;
 
 @Data
-@XmlRootElement(name = "OpenAPI_ServiceResponse")
+@XmlRootElement(name = "response")
 @XmlAccessorType(XmlAccessType.FIELD)
 public class AccommodationResponse {
     @XmlElement(name = "header")
diff --git a/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeDto.java b/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeDto.java
index 4da5aa8..60264c9 100644
--- a/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeDto.java
+++ b/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeDto.java
@@ -1,5 +1,6 @@
 package com.example.placeservice.dto.cafe;
 
+
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java
index f107e54..d8f94b3 100644
--- a/place-service/src/main/java/com/example/placeservice/entity/Area.java
+++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java
@@ -38,4 +38,6 @@ public class Area {
     @OneToMany(mappedBy = "area", fetch = FetchType.LAZY)
     @JsonIgnore
     private List attractions;
+
+
 }
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Cafe.java b/place-service/src/main/java/com/example/placeservice/entity/Cafe.java
index 2dffa74..b7e6db9 100644
--- a/place-service/src/main/java/com/example/placeservice/entity/Cafe.java
+++ b/place-service/src/main/java/com/example/placeservice/entity/Cafe.java
@@ -1,5 +1,6 @@
 package com.example.placeservice.entity;
 
+
 import jakarta.persistence.*;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
diff --git a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java
index 2ac5555..698864c 100644
--- a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java
+++ b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java
@@ -7,6 +7,8 @@
 import java.util.List;
 
 public interface AreaRepository extends JpaRepository {
+
+
     @Query("SELECT a.name FROM Area a")
     List findAllAreaNames();
 }
diff --git a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java
index fcacbe1..51c1a6d 100644
--- a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java
+++ b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java
@@ -18,4 +18,4 @@ public interface CafeRepository extends JpaRepository {
     void deleteByAreaId(Long areaId);
 
     Optional findByKakaomapUrl(String placeCode);
-}
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java
index b331b2d..6f04f0a 100644
--- a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java
+++ b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java
@@ -5,4 +5,5 @@
 
 public interface RestaurantRepository extends JpaRepository {
     // 기본 CRUD 사용 (findAll, findById, save 등)
+
 }
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/AreaService.java b/place-service/src/main/java/com/example/placeservice/service/AreaService.java
index 716de96..c3edbaa 100644
--- a/place-service/src/main/java/com/example/placeservice/service/AreaService.java
+++ b/place-service/src/main/java/com/example/placeservice/service/AreaService.java
@@ -5,14 +5,17 @@
 import com.example.placeservice.repository.AreaRepository;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.List;
 
 @Service
 @RequiredArgsConstructor
+@Transactional
 public class AreaService {
     private final AreaRepository areaRepository;
 
+
     public List getAreaData() {
         try{
             List areas = areaRepository.findAll();
diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java
index e1a8529..ffdb843 100644
--- a/place-service/src/main/java/com/example/placeservice/service/CafeService.java
+++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java
@@ -27,30 +27,66 @@ public class CafeService {
     private final CafeRepository cafeRepository;
     private final KakaoApiService kakaoApiService;
 
+    // 모든 지역에 대한 카페 처리가 완료되었는지 확인하는 플래그
+    private boolean processingCompleted = false;
+
     /**
      * 모든 장소에 대해 주변 카페 데이터 처리
      */
     @Async
+    @Transactional
     public void processAllAreas() {
-        List areas = areaRepository.findAll();
-        log.info("총 {}개 장소에 대한 카페 정보를 처리합니다.", areas.size());
+        log.info("processAllAreas() 메서드 시작");
 
-        for (int i = 0; i < areas.size(); i++) {
-            Area area = areas.get(i);
-            log.info("[{}/{}] '{}' (ID: {}) 처리 중...",
-                    i + 1, areas.size(), area.getName(), area.getAreaId());
+        // 이미 처리가 완료되었거나 이미 데이터가 있으면 다시 실행하지 않음
+        if (processingCompleted) {
+            log.info("이미 처리가 완료되었습니다. 중복 실행 방지를 위해 종료합니다.");
+            return;
+        }
 
-            processSingleArea(area);
+        long cafeCount = cafeRepository.count();
+        log.info("현재 DB에 저장된 카페 수: {}", cafeCount);
 
-            // API 호출 간격 조절
-            try {
-                Thread.sleep(300);
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-            }
+        if (cafeCount > 0) {
+            log.info("카페 데이터가 이미 존재합니다. 처리를 건너뜁니다.");
+            processingCompleted = true;
+            return;
         }
 
-        log.info("모든 장소 처리 완료.");
+        try {
+            List areas = areaRepository.findAll();
+            log.info("총 {}개 장소에 대한 카페 정보를 처리합니다.", areas.size());
+
+            // 다른 처리 중 중복 실행 방지
+            processingCompleted = true;
+
+            log.info("지역별 카페 정보 처리 시작");
+            for (int i = 0; i < areas.size(); i++) {
+                Area area = areas.get(i);
+                log.info("[{}/{}] '{}' (ID: {}) 처리 중...",
+                        i + 1, areas.size(), area.getName(), area.getAreaId());
+
+                processSingleArea(area);
+
+                // API 호출 간격 조절
+                try {
+                    Thread.sleep(300);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    log.error("스레드 인터럽트 발생", e);
+                }
+            }
+
+            log.info("모든 장소 처리 완료. 총 처리된 지역: {}", areas.size());
+
+            // 최종 카페 수 확인
+            log.info("최종 저장된 카페 수: {}", cafeRepository.count());
+
+        } catch (Exception e) {
+            processingCompleted = false; // 오류 발생 시 플래그 재설정
+            log.error("카페 데이터 처리 중 오류 발생: {}", e.getMessage(), e);
+            throw e;
+        }
     }
 
     /**
@@ -58,48 +94,103 @@ public void processAllAreas() {
      */
     @Transactional
     public void processSingleArea(Area area) {
+        log.info("processSingleArea() 시작 - 지역: '{}' (ID: {})", area.getName(), area.getAreaId());
+
         try {
             // 해당 지역의 기존 카페 데이터 삭제
+            log.info("지역 ID {}의 기존 카페 데이터 삭제 시도", area.getAreaId());
             cafeRepository.deleteByAreaId(area.getAreaId());
+            log.info("지역 ID {}의 기존 카페 데이터 삭제 완료", area.getAreaId());
 
             // 주변 카페 검색
             double latitude = area.getLat().doubleValue();
             double longitude = area.getLon().doubleValue();
 
             if (latitude == 0 || longitude == 0) {
-                log.warn("장소 ID {}의 위도/경도 정보가 없습니다.", area.getAreaId());
+                log.warn("장소 ID {}의 위도/경도 정보가 없습니다. 위도: {}, 경도: {}",
+                        area.getAreaId(), latitude, longitude);
                 return;
             }
 
+            log.info("카카오 API 호출 - 위도: {}, 경도: {}, 반경: 1000m", latitude, longitude);
             List cafes =
                     kakaoApiService.findCafesNearby(latitude, longitude, 1000);
+            log.info("카카오 API 응답 - 총 {}개의 장소 정보 수신", cafes.size());
 
             // "음식점 > 카페"가 포함된 카페만 필터링하고 "(휴업중)" 카페는 제외
             List filteredCafes = cafes.stream()
-                    .filter(cafe -> cafe.getCategoryName() != null && cafe.getCategoryName().contains("음식점 > 카페"))
-                    .filter(cafe -> cafe.getPlaceName() == null || !cafe.getPlaceName().contains("(휴업중)"))
+                    .filter(cafe -> {
+                        boolean categoryMatch = cafe.getCategoryName() != null &&
+                                cafe.getCategoryName().contains("음식점 > 카페");
+                        if (!categoryMatch) {
+                            log.debug("카테고리 불일치로 제외: {}, 카테고리: {}",
+                                    cafe.getPlaceName(), cafe.getCategoryName());
+                        }
+                        return categoryMatch;
+                    })
+                    .filter(cafe -> {
+                        boolean notClosed = cafe.getPlaceName() == null ||
+                                !cafe.getPlaceName().contains("(휴업중)");
+                        if (!notClosed) {
+                            log.debug("휴업중으로 제외: {}", cafe.getPlaceName());
+                        }
+                        return notClosed;
+                    })
                     .collect(Collectors.toList());
 
             log.info("'{}' 주변 총 {}개의 카페 중 {}개의 '음식점 > 카페' 카테고리(휴업중 제외)를 찾았습니다.",
                     area.getName(), cafes.size(), filteredCafes.size());
 
+            // 최대 15개의 카페만 저장 (음식점과 동일한 로직 적용)
+            int savedCount = 0;
+            int maxCafesToSave = 15;
+
             // 필터링된 카페 정보 저장
             for (KakaoApiService.CafeResponse.Document cafeDoc : filteredCafes) {
-                Cafe cafe = Cafe.builder()
-                        .name(cafeDoc.getPlaceName())
-                        .address(cafeDoc.getAddressName())
-                        .lat(new BigDecimal(cafeDoc.getY()))
-                        .lon(new BigDecimal(cafeDoc.getX()))
-                        .phone(cafeDoc.getPhone())
-                        .kakaomapUrl(cafeDoc.getPlaceUrl())
-                        .categoryCode(cafeDoc.getCategoryGroupCode())
-                        .area(area)
-                        .build();
-
-                cafeRepository.save(cafe);
+                // 최대 카페 저장 개수 제한
+                if (savedCount >= maxCafesToSave) {
+                    log.info("최대 저장 개수({})에 도달하여 저장 중단", maxCafesToSave);
+                    break;
+                }
+
+                log.info("카페 저장 시도: {}, URL: {}", cafeDoc.getPlaceName(), cafeDoc.getPlaceUrl());
+
+                // 이미 동일한 카페가 있는지 확인 (kakaomapUrl로 중복 체크)
+                Optional existingCafe = cafeRepository.findByKakaomapUrl(cafeDoc.getPlaceUrl());
+                if (existingCafe.isPresent()) {
+                    log.info("이미 존재하는 카페이므로 건너뜀: {}, ID: {}",
+                            existingCafe.get().getName(), existingCafe.get().getId());
+                    continue; // 이미 존재하는 카페는 건너뜀
+                }
+
+                try {
+                    Cafe cafe = Cafe.builder()
+                            .name(cafeDoc.getPlaceName())
+                            .address(cafeDoc.getAddressName())
+                            .lat(new BigDecimal(cafeDoc.getY()))
+                            .lon(new BigDecimal(cafeDoc.getX()))
+                            .phone(cafeDoc.getPhone())
+                            .kakaomapUrl(cafeDoc.getPlaceUrl())
+                            .categoryCode(cafeDoc.getCategoryGroupCode())
+                            .area(area)
+                            .build();
+
+                    log.info("카페 객체 생성 완료: {}", cafe.getName());
+                    Cafe savedCafe = cafeRepository.save(cafe);
+                    log.info("카페 저장 완료: {}, 저장된 ID: {}", savedCafe.getName(), savedCafe.getId());
+
+                    savedCount++;
+                } catch (Exception e) {
+                    log.error("카페 저장 중 오류 발생: {}, 오류: {}", cafeDoc.getPlaceName(), e.getMessage(), e);
+                }
             }
+
+            log.info("지역 '{}' (ID: {})에 총 {}개의 카페 저장 완료",
+                    area.getName(), area.getAreaId(), savedCount);
+
         } catch (Exception e) {
             log.error("장소 ID {} 처리 중 오류: {}", area.getAreaId(), e.getMessage(), e);
+            throw e; // 상위 메서드에서 처리할 수 있도록 예외 전파
         }
     }
 
@@ -108,7 +199,10 @@ public void processSingleArea(Area area) {
      */
     @Transactional(readOnly = true)
     public List getCafesByAreaId(Long areaId) {
+        log.info("지역 ID {}의 카페 목록 조회", areaId);
         List cafes = cafeRepository.findByAreaId(areaId);
+        log.info("지역 ID {}에서 총 {}개의 카페 찾음", areaId, cafes.size());
+
         return cafes.stream()
                 .map(this::convertToDto)
                 .collect(Collectors.toList());
@@ -119,7 +213,10 @@ public List getCafesByAreaId(Long areaId) {
      */
     @Transactional(readOnly = true)
     public List getAllCafes() {
+        log.info("모든 카페 목록 조회");
         List cafes = cafeRepository.findAll();
+        log.info("총 {}개의 카페 조회됨", cafes.size());
+
         return cafes.stream()
                 .map(this::convertToDto)
                 .collect(Collectors.toList());
@@ -130,7 +227,10 @@ public List getAllCafes() {
      */
     @Transactional(readOnly = true)
     public List> getAllCafesWithLimitedInfo() {
+        log.info("제한된 정보로 모든 카페 목록 조회");
         List cafes = cafeRepository.findAll();
+        log.info("총 {}개의 카페 조회됨", cafes.size());
+
         return cafes.stream()
                 .map(cafe -> {
                     Map cafeMap = new HashMap<>();
@@ -161,10 +261,13 @@ private CafeDto convertToDto(Cafe cafe) {
 
     @Transactional(readOnly = true)
     public Map getCafeDetailByPlaceCode(String placeCode) {
+        log.info("장소 코드로 카페 상세 정보 조회: {}", placeCode);
         Optional cafeOptional = cafeRepository.findByKakaomapUrl(placeCode);
 
         if (cafeOptional.isPresent()) {
             Cafe cafe = cafeOptional.get();
+            log.info("카페 찾음: {}, ID: {}", cafe.getName(), cafe.getId());
+
             Map cafeInfo = new HashMap<>();
             cafeInfo.put("name", cafe.getName());
             cafeInfo.put("address", cafe.getAddress());
@@ -177,6 +280,7 @@ public Map getCafeDetailByPlaceCode(String placeCode) {
             return cafeInfo;
         }
 
+        log.info("장소 코드 {}에 해당하는 카페를 찾을 수 없음", placeCode);
         return null;
     }
 }
\ No newline at end of file
diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties
index dc1e851..dac0e95 100644
--- a/place-service/src/main/resources/application.properties
+++ b/place-service/src/main/resources/application.properties
@@ -9,12 +9,13 @@ spring.datasource.driver-class-name=org.postgresql.Driver
 
 spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
 spring.jpa.defer-datasource-initialization=true
-spring.jpa.hibernate.ddl-auto=create
+spring.jpa.hibernate.ddl-auto=update
 #spring.jpa.hibernate.ddl-auto=update
 spring.jpa.show-sql=true
 spring.sql.init.mode=always
 
 
+
 #API
 kakao.api.key=${KAKAO_API_KEY}
 

From 53618cf123e7a9259ae8b9b1076e97fc93192f1e Mon Sep 17 00:00:00 2001
From: ha-yun 
Date: Sat, 26 Apr 2025 11:23:52 +0900
Subject: [PATCH 124/361] =?UTF-8?q?Refactor:=20DataInit=20=EC=BD=94?=
 =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=EB=B0=8F=20?=
 =?UTF-8?q?area=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A4=91=EB=B3=B5=20?=
 =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=AC=B8=EC=A0=9C=20=EB=B0=8F=20=EA=B0=80?=
 =?UTF-8?q?=EC=9E=A5=20=EB=A8=BC=EC=A0=80=20=EC=A0=80=EC=9E=A5=EB=90=98?=
 =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../placeservice/PlaceServiceApplication.java | 34 ---------
 .../com/example/placeservice/entity/Area.java |  2 +-
 .../placeservice/init/AttractionDataInit.java | 20 -----
 .../example/placeservice/init/DataInit.java   | 74 +++++++++++++++++++
 .../init/EventDataInitializer.java            | 19 -----
 .../src/main/resources/application.properties |  3 +-
 place-service/src/main/resources/data.sql     |  3 +-
 7 files changed, 78 insertions(+), 77 deletions(-)
 delete mode 100644 place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java
 create mode 100644 place-service/src/main/java/com/example/placeservice/init/DataInit.java
 delete mode 100644 place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java

diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
index beac01e..459d531 100644
--- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
+++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
@@ -1,14 +1,6 @@
 package com.example.placeservice;
-
-import com.example.placeservice.repository.CafeRepository;
-import com.example.placeservice.repository.RestaurantRepository;
-import com.example.placeservice.repository.CafeRepository;
-import com.example.placeservice.service.RestaurantService;
-import com.example.placeservice.service.CafeService;
-import org.springframework.boot.CommandLineRunner;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.context.annotation.Bean;
 import org.springframework.scheduling.annotation.EnableAsync;
 
 @SpringBootApplication
@@ -22,30 +14,4 @@ public static void main(String[] args) {
 
         SpringApplication.run(PlaceServiceApplication.class, args);
     }
-
-    @Bean
-
-    public CommandLineRunner initData(RestaurantService restaurantService, RestaurantRepository restaurantRepository, CafeService cafeService, CafeRepository cafeRepository) {
-        return args -> {
-            // 음식점 데이터 초기화
-            if (restaurantRepository.count() == 0) {
-                System.out.println("음식점 데이터 저장 시작");
-                restaurantService.fetchAndSaveRestaurants();
-                System.out.println("음식점 데이터 저장 완료");
-            } else {
-                System.out.println("음식점 데이터가 이미 존재합니다. 건너뜁니다.");
-            }
-
-            // 카페 데이터 초기화
-            if (cafeRepository.count() == 0) {
-                System.out.println("카페 데이터 저장 시작");
-                cafeService.processAllAreas();
-                System.out.println("카페 데이터 저장 완료");
-            } else {
-                System.out.println("카페 데이터가 이미 존재합니다. 건너뜁니다.");
-            }
-
-            
-        };
-    }
 }
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java
index d8f94b3..652f631 100644
--- a/place-service/src/main/java/com/example/placeservice/entity/Area.java
+++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java
@@ -17,7 +17,7 @@ public class Area {
     @Column(name = "area_id")
     private Long areaId;
 
-    @Column(name = "seoul_id", length = 10, nullable = false)
+    @Column(name = "seoul_id", length = 10, nullable = false, unique = true)
     private String seoulId;
 
     @Column(name = "name", length = 50, nullable = false)
diff --git a/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java b/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java
deleted file mode 100644
index 5912fb0..0000000
--- a/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.example.placeservice.init;
-
-import com.example.placeservice.service.AttractionService;
-import lombok.RequiredArgsConstructor;
-import org.springframework.boot.CommandLineRunner;
-import org.springframework.stereotype.Component;
-
-// SB 실행 시, attraction data 저장(area 데이터가 먼저 저장되어 있어야 함)
-@Component
-@RequiredArgsConstructor
-public class AttractionDataInit implements CommandLineRunner {
-
-    private final AttractionService attractionService;
-
-    @Override
-    public void run(String... args) throws Exception {
-        attractionService.fetchDataFromVisitSeoul(); // 애플리케이션 시작 시 자동 실행
-    }
-
-}
diff --git a/place-service/src/main/java/com/example/placeservice/init/DataInit.java b/place-service/src/main/java/com/example/placeservice/init/DataInit.java
new file mode 100644
index 0000000..fe247da
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/init/DataInit.java
@@ -0,0 +1,74 @@
+package com.example.placeservice.init;
+
+import com.example.placeservice.repository.CafeRepository;
+import com.example.placeservice.repository.RestaurantRepository;
+import com.example.placeservice.service.AttractionService;
+import com.example.placeservice.service.CafeService;
+import com.example.placeservice.service.CulturalEventService;
+import com.example.placeservice.service.RestaurantService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StreamUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+// SB 실행 시, attraction data 저장(area 데이터가 먼저 저장되어 있어야 함)
+@Component
+@RequiredArgsConstructor
+public class DataInit implements CommandLineRunner {
+
+    private final AttractionService attractionService;
+    private final CulturalEventService culturalEventService;
+    private final RestaurantRepository restaurantRepository;
+    private final RestaurantService restaurantService;
+    private final CafeRepository cafeRepository;
+    private final CafeService cafeService;
+
+    private final JdbcTemplate jdbcTemplate;
+
+
+    @Override
+    public void run(String... args) throws Exception {
+        // 1. data.sql 파일 실행(지역 데이터)
+        try {
+            ClassPathResource resource = new ClassPathResource("data.sql");
+
+            // 파일 내용을 문자열로 읽기
+            String sql = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
+
+            // SQL 실행
+            jdbcTemplate.execute(sql);
+            System.out.println("data.sql 실행 완료");
+
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        // 2. 장소 데이터 삽입
+        attractionService.fetchDataFromVisitSeoul();
+        culturalEventService.fetchAndSaveAllEvents();
+
+        if (restaurantRepository.count() == 0) {
+            System.out.println("음식점 데이터 저장 시작");
+            restaurantService.fetchAndSaveRestaurants();
+            System.out.println("음식점 데이터 저장 완료");
+        } else {
+            System.out.println("음식점 데이터가 이미 존재합니다. 건너뜁니다.");
+        }
+
+        // 카페 데이터 초기화
+        if (cafeRepository.count() == 0) {
+            System.out.println("카페 데이터 저장 시작");
+            cafeService.processAllAreas();
+            System.out.println("카페 데이터 저장 완료");
+        } else {
+            System.out.println("카페 데이터가 이미 존재합니다. 건너뜁니다.");
+        }
+
+    }
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java b/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java
deleted file mode 100644
index 32e5ede..0000000
--- a/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.example.placeservice.init;
-
-import com.example.placeservice.service.CulturalEventService;
-import lombok.RequiredArgsConstructor;
-import org.springframework.boot.CommandLineRunner;
-import org.springframework.stereotype.Component;
-
-@Component
-@RequiredArgsConstructor
-public class EventDataInitializer implements CommandLineRunner {
-
-    private final CulturalEventService culturalEventService;
-
-    @Override
-    public void run(String... args) throws Exception {
-        // 애플리케이션 시작 시 데이터를 자동으로 받아와 저장
-        culturalEventService.fetchAndSaveAllEvents();
-    }
-}
diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties
index dac0e95..75e7543 100644
--- a/place-service/src/main/resources/application.properties
+++ b/place-service/src/main/resources/application.properties
@@ -12,9 +12,8 @@ spring.jpa.defer-datasource-initialization=true
 spring.jpa.hibernate.ddl-auto=update
 #spring.jpa.hibernate.ddl-auto=update
 spring.jpa.show-sql=true
-spring.sql.init.mode=always
-
 
+#spring.sql.init.mode=always
 
 #API
 kakao.api.key=${KAKAO_API_KEY}
diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql
index 2113a37..6d15ed5 100644
--- a/place-service/src/main/resources/data.sql
+++ b/place-service/src/main/resources/data.sql
@@ -114,4 +114,5 @@ INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES
 ('POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264),
 ('POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616),
 ('POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821),
-('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286);
+('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286)
+ON CONFLICT (seoul_id) DO NOTHING;
\ No newline at end of file

From a644b13a7ca4583e42aa9dc40836a18f10b10d47 Mon Sep 17 00:00:00 2001
From: ha-yun 
Date: Sat, 26 Apr 2025 11:23:52 +0900
Subject: [PATCH 125/361] =?UTF-8?q?Refactor:=20DataInit=20=EC=BD=94?=
 =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=EB=B0=8F=20?=
 =?UTF-8?q?area=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A4=91=EB=B3=B5=20?=
 =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=AC=B8=EC=A0=9C=20=EB=B0=8F=20=EA=B0=80?=
 =?UTF-8?q?=EC=9E=A5=20=EB=A8=BC=EC=A0=80=20=EC=A0=80=EC=9E=A5=EB=90=98?=
 =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../placeservice/PlaceServiceApplication.java | 34 ---------
 .../com/example/placeservice/entity/Area.java |  2 +-
 .../placeservice/init/AttractionDataInit.java | 20 -----
 .../example/placeservice/init/DataInit.java   | 74 +++++++++++++++++++
 .../init/EventDataInitializer.java            | 19 -----
 .../src/main/resources/application.properties |  3 +-
 place-service/src/main/resources/data.sql     |  3 +-
 7 files changed, 78 insertions(+), 77 deletions(-)
 delete mode 100644 place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java
 create mode 100644 place-service/src/main/java/com/example/placeservice/init/DataInit.java
 delete mode 100644 place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java

diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
index beac01e..459d531 100644
--- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
+++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
@@ -1,14 +1,6 @@
 package com.example.placeservice;
-
-import com.example.placeservice.repository.CafeRepository;
-import com.example.placeservice.repository.RestaurantRepository;
-import com.example.placeservice.repository.CafeRepository;
-import com.example.placeservice.service.RestaurantService;
-import com.example.placeservice.service.CafeService;
-import org.springframework.boot.CommandLineRunner;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.context.annotation.Bean;
 import org.springframework.scheduling.annotation.EnableAsync;
 
 @SpringBootApplication
@@ -22,30 +14,4 @@ public static void main(String[] args) {
 
         SpringApplication.run(PlaceServiceApplication.class, args);
     }
-
-    @Bean
-
-    public CommandLineRunner initData(RestaurantService restaurantService, RestaurantRepository restaurantRepository, CafeService cafeService, CafeRepository cafeRepository) {
-        return args -> {
-            // 음식점 데이터 초기화
-            if (restaurantRepository.count() == 0) {
-                System.out.println("음식점 데이터 저장 시작");
-                restaurantService.fetchAndSaveRestaurants();
-                System.out.println("음식점 데이터 저장 완료");
-            } else {
-                System.out.println("음식점 데이터가 이미 존재합니다. 건너뜁니다.");
-            }
-
-            // 카페 데이터 초기화
-            if (cafeRepository.count() == 0) {
-                System.out.println("카페 데이터 저장 시작");
-                cafeService.processAllAreas();
-                System.out.println("카페 데이터 저장 완료");
-            } else {
-                System.out.println("카페 데이터가 이미 존재합니다. 건너뜁니다.");
-            }
-
-            
-        };
-    }
 }
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java
index d8f94b3..652f631 100644
--- a/place-service/src/main/java/com/example/placeservice/entity/Area.java
+++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java
@@ -17,7 +17,7 @@ public class Area {
     @Column(name = "area_id")
     private Long areaId;
 
-    @Column(name = "seoul_id", length = 10, nullable = false)
+    @Column(name = "seoul_id", length = 10, nullable = false, unique = true)
     private String seoulId;
 
     @Column(name = "name", length = 50, nullable = false)
diff --git a/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java b/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java
deleted file mode 100644
index 5912fb0..0000000
--- a/place-service/src/main/java/com/example/placeservice/init/AttractionDataInit.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.example.placeservice.init;
-
-import com.example.placeservice.service.AttractionService;
-import lombok.RequiredArgsConstructor;
-import org.springframework.boot.CommandLineRunner;
-import org.springframework.stereotype.Component;
-
-// SB 실행 시, attraction data 저장(area 데이터가 먼저 저장되어 있어야 함)
-@Component
-@RequiredArgsConstructor
-public class AttractionDataInit implements CommandLineRunner {
-
-    private final AttractionService attractionService;
-
-    @Override
-    public void run(String... args) throws Exception {
-        attractionService.fetchDataFromVisitSeoul(); // 애플리케이션 시작 시 자동 실행
-    }
-
-}
diff --git a/place-service/src/main/java/com/example/placeservice/init/DataInit.java b/place-service/src/main/java/com/example/placeservice/init/DataInit.java
new file mode 100644
index 0000000..fe247da
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/init/DataInit.java
@@ -0,0 +1,74 @@
+package com.example.placeservice.init;
+
+import com.example.placeservice.repository.CafeRepository;
+import com.example.placeservice.repository.RestaurantRepository;
+import com.example.placeservice.service.AttractionService;
+import com.example.placeservice.service.CafeService;
+import com.example.placeservice.service.CulturalEventService;
+import com.example.placeservice.service.RestaurantService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StreamUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+// SB 실행 시, attraction data 저장(area 데이터가 먼저 저장되어 있어야 함)
+@Component
+@RequiredArgsConstructor
+public class DataInit implements CommandLineRunner {
+
+    private final AttractionService attractionService;
+    private final CulturalEventService culturalEventService;
+    private final RestaurantRepository restaurantRepository;
+    private final RestaurantService restaurantService;
+    private final CafeRepository cafeRepository;
+    private final CafeService cafeService;
+
+    private final JdbcTemplate jdbcTemplate;
+
+
+    @Override
+    public void run(String... args) throws Exception {
+        // 1. data.sql 파일 실행(지역 데이터)
+        try {
+            ClassPathResource resource = new ClassPathResource("data.sql");
+
+            // 파일 내용을 문자열로 읽기
+            String sql = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
+
+            // SQL 실행
+            jdbcTemplate.execute(sql);
+            System.out.println("data.sql 실행 완료");
+
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        // 2. 장소 데이터 삽입
+        attractionService.fetchDataFromVisitSeoul();
+        culturalEventService.fetchAndSaveAllEvents();
+
+        if (restaurantRepository.count() == 0) {
+            System.out.println("음식점 데이터 저장 시작");
+            restaurantService.fetchAndSaveRestaurants();
+            System.out.println("음식점 데이터 저장 완료");
+        } else {
+            System.out.println("음식점 데이터가 이미 존재합니다. 건너뜁니다.");
+        }
+
+        // 카페 데이터 초기화
+        if (cafeRepository.count() == 0) {
+            System.out.println("카페 데이터 저장 시작");
+            cafeService.processAllAreas();
+            System.out.println("카페 데이터 저장 완료");
+        } else {
+            System.out.println("카페 데이터가 이미 존재합니다. 건너뜁니다.");
+        }
+
+    }
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java b/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java
deleted file mode 100644
index 32e5ede..0000000
--- a/place-service/src/main/java/com/example/placeservice/init/EventDataInitializer.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.example.placeservice.init;
-
-import com.example.placeservice.service.CulturalEventService;
-import lombok.RequiredArgsConstructor;
-import org.springframework.boot.CommandLineRunner;
-import org.springframework.stereotype.Component;
-
-@Component
-@RequiredArgsConstructor
-public class EventDataInitializer implements CommandLineRunner {
-
-    private final CulturalEventService culturalEventService;
-
-    @Override
-    public void run(String... args) throws Exception {
-        // 애플리케이션 시작 시 데이터를 자동으로 받아와 저장
-        culturalEventService.fetchAndSaveAllEvents();
-    }
-}
diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties
index dac0e95..75e7543 100644
--- a/place-service/src/main/resources/application.properties
+++ b/place-service/src/main/resources/application.properties
@@ -12,9 +12,8 @@ spring.jpa.defer-datasource-initialization=true
 spring.jpa.hibernate.ddl-auto=update
 #spring.jpa.hibernate.ddl-auto=update
 spring.jpa.show-sql=true
-spring.sql.init.mode=always
-
 
+#spring.sql.init.mode=always
 
 #API
 kakao.api.key=${KAKAO_API_KEY}
diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql
index 2113a37..6d15ed5 100644
--- a/place-service/src/main/resources/data.sql
+++ b/place-service/src/main/resources/data.sql
@@ -114,4 +114,5 @@ INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES
 ('POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264),
 ('POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616),
 ('POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821),
-('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286);
+('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286)
+ON CONFLICT (seoul_id) DO NOTHING;
\ No newline at end of file

From 604241ea12d0752e3bcf9ebb86e37d68a60e9ea3 Mon Sep 17 00:00:00 2001
From: ha-yun 
Date: Sat, 26 Apr 2025 13:08:50 +0900
Subject: [PATCH 126/361] =?UTF-8?q?Feat:=20attraction=20API=EC=97=90=20kak?=
 =?UTF-8?q?aomap=5Furl=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../dto/attraction/AttractionInfoDto.java     |  4 +-
 .../dto/attraction/AttractionListDto.java     |  4 +-
 .../placeservice/entity/Attraction.java       |  3 ++
 .../service/AttractionKakaoMapClient.java     | 52 +++++++++++++++++++
 .../service/AttractionService.java            | 31 +++++++++--
 .../src/main/resources/application.properties |  2 +-
 6 files changed, 90 insertions(+), 6 deletions(-)
 create mode 100644 place-service/src/main/java/com/example/placeservice/service/AttractionKakaoMapClient.java

diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java
index 29357e9..389d057 100644
--- a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java
+++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java
@@ -16,8 +16,9 @@ public class AttractionInfoDto {
     private String homepage_url;
     private String close_day;
     private String use_time;
+    private String kakaomap_url;
 
-    public AttractionInfoDto(Long attractionId, String seoul_attraction_id,String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time) {
+    public AttractionInfoDto(Long attractionId, String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time, String kakaomapUrl) {
         this.attraction_id = attractionId;
         this.seoul_attraction_id = seoul_attraction_id;
         this.name = name;
@@ -28,6 +29,7 @@ public AttractionInfoDto(Long attractionId, String seoul_attraction_id,String na
         this.homepage_url = homepage_url;
         this.close_day = close_day;
         this.use_time = use_time;
+        this.kakaomap_url = kakaomapUrl;
     }
 
 }
diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java
index 1b5b2b9..8abe1e6 100644
--- a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java
+++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java
@@ -12,14 +12,16 @@ public class AttractionListDto {
     private String address;
     private BigDecimal lat;
     private BigDecimal lon;
+    private String kakaomap_url;
 
 
-    public AttractionListDto(Long attraction_id,String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon) {
+    public AttractionListDto(Long attraction_id, String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon, String kakaomapUrl) {
         this.attraction_id = attraction_id;
         this.seoul_attraction_id = seoul_attraction_id;
         this.name = name;
         this.address = address;
         this.lat = lat;
         this.lon = lon;
+        this.kakaomap_url = kakaomapUrl;
     }
 }
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java
index 3b89701..c5b3194 100644
--- a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java
+++ b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java
@@ -40,6 +40,9 @@ public class Attraction {
     @Column(name="use_time", columnDefinition = "TEXT")
     private String useTime;
 
+    @Column(name="kakaomap_url",columnDefinition = "TEXT")
+    private String kakaomapUrl;
+
     @ManyToOne(fetch = FetchType.EAGER)
     @JoinColumn(name = "area_id", nullable = false)
     private Area area;
diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionKakaoMapClient.java b/place-service/src/main/java/com/example/placeservice/service/AttractionKakaoMapClient.java
new file mode 100644
index 0000000..69e33bc
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/service/AttractionKakaoMapClient.java
@@ -0,0 +1,52 @@
+package com.example.placeservice.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+
+@Service
+@RequiredArgsConstructor
+public class AttractionKakaoMapClient {
+
+    @Value("${kakao.api.key}")
+    private String kakaoApiKey;
+
+
+
+    public JsonNode searchAttractionByKeyword(String keyword) {
+        RestTemplate restTemplate = new RestTemplate();
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("Authorization", "KakaoAK " + kakaoApiKey);
+
+        // Kakao API 호출 URL 세팅
+        String builder = UriComponentsBuilder
+                .fromHttpUrl("https://dapi.kakao.com/v2/local/search/keyword.json")
+                .queryParam("query",keyword)
+                .queryParam("sort", "accuracy") // 정확도순 정렬
+                .queryParam("page",1) // 요청 페이지
+                .queryParam("size", 15)
+                .build(false).toString(); // 한 페이지에 최대 15개 결과
+
+        HttpEntity entity = new HttpEntity<>(headers);
+
+        // REST API 호출
+        ResponseEntity response = restTemplate.exchange(
+                builder.toString(),
+                HttpMethod.GET,
+                entity,
+                JsonNode.class
+        );
+
+        return response.getBody(); // JSON 응답 반환
+    }
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java
index 4fc8617..04a593c 100644
--- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java
+++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java
@@ -8,6 +8,7 @@
 import com.example.placeservice.entity.Attraction;
 import com.example.placeservice.repository.AreaRepository;
 import com.example.placeservice.repository.AttractionRepository;
+import com.fasterxml.jackson.databind.JsonNode;
 import jakarta.xml.bind.JAXBContext;
 import jakarta.xml.bind.Unmarshaller;
 import lombok.RequiredArgsConstructor;
@@ -31,6 +32,7 @@ public class AttractionService {
     private final RestTemplate restTemplate = new RestTemplate();
     private final AttractionRepository attractionRepository;
     private final AreaRepository areaRepository;
+    private final AttractionKakaoMapClient attractionKakaoMapClient;
 
     // visitSeoul 관광지 데이터 로드 후 attraction 테이블 저장
     public List fetchDataFromVisitSeoul() {
@@ -52,6 +54,7 @@ public List fetchDataFromVisitSeoul() {
             // 3. DTO → Entity로 변환 (매핑)
             List entities = dto.getAttractions().stream()
                 .map(table -> {
+
                     // seoul_attraction_id 중복 체크
                     if (attractionRepository.existsBySeoulAttractionId(table.getId())) return null;
 
@@ -59,6 +62,7 @@ public List fetchDataFromVisitSeoul() {
                     Area area = findAreaByCondition(table, areaList);  // 예: 주소나 지역코드 등으로 판단
                     if (area == null) return null;
 
+
                     Attraction attraction = new Attraction();
                     attraction.setSeoulAttractionId(table.getId());
                     attraction.setName(table.getTitle());
@@ -69,9 +73,25 @@ public List fetchDataFromVisitSeoul() {
                     attraction.setHomepageUrl(table.getHomepage());
                     attraction.setCloseDay(table.getCloseDay());
                     attraction.setUseTime(table.getUseTime());
-
                     attraction.setArea(area);
 
+                    // kakaomap_url 추가
+                    String title = table.getTitle();
+                    if (title.length() <= 100){ // 키워드 최대 100글자까지 검색가능
+                        JsonNode kakao_response = attractionKakaoMapClient.searchAttractionByKeyword(title);
+                        if (kakao_response.has("documents")) {
+                            for (JsonNode doc : kakao_response.get("documents")) {
+                                String categoryName = doc.get("category_name").asText();
+                                if (categoryName.contains("여행 > 관광") || categoryName.contains("여행 > 명소")) {
+//                                    System.out.println("🎯 관광 관련 장소: " +table.getTitle()+"는 바로바로 :::"+ doc.get("place_name").asText());
+                                    attraction.setKakaomapUrl(doc.get("place_url").asText());
+                                    break;
+                                }
+                            }
+                        }else{System.out.println("카카오 관광지 검색 결과가 없습니다");}
+                    }else{System.out.println(table.getTitle());}
+
+
                 return attraction;
             })
             .filter(Objects::nonNull) // null 필터링
@@ -104,6 +124,9 @@ private Area findAreaByCondition(AttractionDto.AttractionTable table, List
         return null;
     }
 
+
+
+
     // attraction 목록 DB 불러오기
     public List getAttractionData() {
         try {
@@ -118,7 +141,8 @@ public List getAttractionData() {
                                             attraction.getName(),
                                             attraction.getAddress(),
                                             attraction.getLat(),
-                                            attraction.getLon()
+                                            attraction.getLon(),
+                                            attraction.getKakaomapUrl()
                                     )).toList();
 
                             AreaAttractionsDto dto = new AreaAttractionsDto();
@@ -148,7 +172,8 @@ public AttractionInfoDto getAttractionInfoData(long attractionId) {
                     attraction.getPhone(),
                     attraction.getHomepageUrl(),
                     attraction.getCloseDay(),
-                    attraction.getUseTime()
+                    attraction.getUseTime(),
+                    attraction.getKakaomapUrl()
             );
 
         } catch (RuntimeException e) {
diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties
index 75e7543..1c1abb2 100644
--- a/place-service/src/main/resources/application.properties
+++ b/place-service/src/main/resources/application.properties
@@ -10,7 +10,7 @@ spring.datasource.driver-class-name=org.postgresql.Driver
 spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
 spring.jpa.defer-datasource-initialization=true
 spring.jpa.hibernate.ddl-auto=update
-#spring.jpa.hibernate.ddl-auto=update
+# spring.jpa.hibernate.ddl-auto=create
 spring.jpa.show-sql=true
 
 #spring.sql.init.mode=always

From fb3f37c61674774ccf8d6cb625bd2af451141398 Mon Sep 17 00:00:00 2001
From: ha-yun 
Date: Sat, 26 Apr 2025 13:08:50 +0900
Subject: [PATCH 127/361] =?UTF-8?q?Feat:=20attraction=20API=EC=97=90=20kak?=
 =?UTF-8?q?aomap=5Furl=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../dto/attraction/AttractionInfoDto.java     |  4 +-
 .../dto/attraction/AttractionListDto.java     |  4 +-
 .../placeservice/entity/Attraction.java       |  3 ++
 .../service/AttractionKakaoMapClient.java     | 52 +++++++++++++++++++
 .../service/AttractionService.java            | 31 +++++++++--
 .../src/main/resources/application.properties |  2 +-
 6 files changed, 90 insertions(+), 6 deletions(-)
 create mode 100644 place-service/src/main/java/com/example/placeservice/service/AttractionKakaoMapClient.java

diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java
index 29357e9..389d057 100644
--- a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java
+++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java
@@ -16,8 +16,9 @@ public class AttractionInfoDto {
     private String homepage_url;
     private String close_day;
     private String use_time;
+    private String kakaomap_url;
 
-    public AttractionInfoDto(Long attractionId, String seoul_attraction_id,String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time) {
+    public AttractionInfoDto(Long attractionId, String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time, String kakaomapUrl) {
         this.attraction_id = attractionId;
         this.seoul_attraction_id = seoul_attraction_id;
         this.name = name;
@@ -28,6 +29,7 @@ public AttractionInfoDto(Long attractionId, String seoul_attraction_id,String na
         this.homepage_url = homepage_url;
         this.close_day = close_day;
         this.use_time = use_time;
+        this.kakaomap_url = kakaomapUrl;
     }
 
 }
diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java
index 1b5b2b9..8abe1e6 100644
--- a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java
+++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java
@@ -12,14 +12,16 @@ public class AttractionListDto {
     private String address;
     private BigDecimal lat;
     private BigDecimal lon;
+    private String kakaomap_url;
 
 
-    public AttractionListDto(Long attraction_id,String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon) {
+    public AttractionListDto(Long attraction_id, String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon, String kakaomapUrl) {
         this.attraction_id = attraction_id;
         this.seoul_attraction_id = seoul_attraction_id;
         this.name = name;
         this.address = address;
         this.lat = lat;
         this.lon = lon;
+        this.kakaomap_url = kakaomapUrl;
     }
 }
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java
index 3b89701..c5b3194 100644
--- a/place-service/src/main/java/com/example/placeservice/entity/Attraction.java
+++ b/place-service/src/main/java/com/example/placeservice/entity/Attraction.java
@@ -40,6 +40,9 @@ public class Attraction {
     @Column(name="use_time", columnDefinition = "TEXT")
     private String useTime;
 
+    @Column(name="kakaomap_url",columnDefinition = "TEXT")
+    private String kakaomapUrl;
+
     @ManyToOne(fetch = FetchType.EAGER)
     @JoinColumn(name = "area_id", nullable = false)
     private Area area;
diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionKakaoMapClient.java b/place-service/src/main/java/com/example/placeservice/service/AttractionKakaoMapClient.java
new file mode 100644
index 0000000..69e33bc
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/service/AttractionKakaoMapClient.java
@@ -0,0 +1,52 @@
+package com.example.placeservice.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+
+@Service
+@RequiredArgsConstructor
+public class AttractionKakaoMapClient {
+
+    @Value("${kakao.api.key}")
+    private String kakaoApiKey;
+
+
+
+    public JsonNode searchAttractionByKeyword(String keyword) {
+        RestTemplate restTemplate = new RestTemplate();
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("Authorization", "KakaoAK " + kakaoApiKey);
+
+        // Kakao API 호출 URL 세팅
+        String builder = UriComponentsBuilder
+                .fromHttpUrl("https://dapi.kakao.com/v2/local/search/keyword.json")
+                .queryParam("query",keyword)
+                .queryParam("sort", "accuracy") // 정확도순 정렬
+                .queryParam("page",1) // 요청 페이지
+                .queryParam("size", 15)
+                .build(false).toString(); // 한 페이지에 최대 15개 결과
+
+        HttpEntity entity = new HttpEntity<>(headers);
+
+        // REST API 호출
+        ResponseEntity response = restTemplate.exchange(
+                builder.toString(),
+                HttpMethod.GET,
+                entity,
+                JsonNode.class
+        );
+
+        return response.getBody(); // JSON 응답 반환
+    }
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java
index 4fc8617..04a593c 100644
--- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java
+++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java
@@ -8,6 +8,7 @@
 import com.example.placeservice.entity.Attraction;
 import com.example.placeservice.repository.AreaRepository;
 import com.example.placeservice.repository.AttractionRepository;
+import com.fasterxml.jackson.databind.JsonNode;
 import jakarta.xml.bind.JAXBContext;
 import jakarta.xml.bind.Unmarshaller;
 import lombok.RequiredArgsConstructor;
@@ -31,6 +32,7 @@ public class AttractionService {
     private final RestTemplate restTemplate = new RestTemplate();
     private final AttractionRepository attractionRepository;
     private final AreaRepository areaRepository;
+    private final AttractionKakaoMapClient attractionKakaoMapClient;
 
     // visitSeoul 관광지 데이터 로드 후 attraction 테이블 저장
     public List fetchDataFromVisitSeoul() {
@@ -52,6 +54,7 @@ public List fetchDataFromVisitSeoul() {
             // 3. DTO → Entity로 변환 (매핑)
             List entities = dto.getAttractions().stream()
                 .map(table -> {
+
                     // seoul_attraction_id 중복 체크
                     if (attractionRepository.existsBySeoulAttractionId(table.getId())) return null;
 
@@ -59,6 +62,7 @@ public List fetchDataFromVisitSeoul() {
                     Area area = findAreaByCondition(table, areaList);  // 예: 주소나 지역코드 등으로 판단
                     if (area == null) return null;
 
+
                     Attraction attraction = new Attraction();
                     attraction.setSeoulAttractionId(table.getId());
                     attraction.setName(table.getTitle());
@@ -69,9 +73,25 @@ public List fetchDataFromVisitSeoul() {
                     attraction.setHomepageUrl(table.getHomepage());
                     attraction.setCloseDay(table.getCloseDay());
                     attraction.setUseTime(table.getUseTime());
-
                     attraction.setArea(area);
 
+                    // kakaomap_url 추가
+                    String title = table.getTitle();
+                    if (title.length() <= 100){ // 키워드 최대 100글자까지 검색가능
+                        JsonNode kakao_response = attractionKakaoMapClient.searchAttractionByKeyword(title);
+                        if (kakao_response.has("documents")) {
+                            for (JsonNode doc : kakao_response.get("documents")) {
+                                String categoryName = doc.get("category_name").asText();
+                                if (categoryName.contains("여행 > 관광") || categoryName.contains("여행 > 명소")) {
+//                                    System.out.println("🎯 관광 관련 장소: " +table.getTitle()+"는 바로바로 :::"+ doc.get("place_name").asText());
+                                    attraction.setKakaomapUrl(doc.get("place_url").asText());
+                                    break;
+                                }
+                            }
+                        }else{System.out.println("카카오 관광지 검색 결과가 없습니다");}
+                    }else{System.out.println(table.getTitle());}
+
+
                 return attraction;
             })
             .filter(Objects::nonNull) // null 필터링
@@ -104,6 +124,9 @@ private Area findAreaByCondition(AttractionDto.AttractionTable table, List
         return null;
     }
 
+
+
+
     // attraction 목록 DB 불러오기
     public List getAttractionData() {
         try {
@@ -118,7 +141,8 @@ public List getAttractionData() {
                                             attraction.getName(),
                                             attraction.getAddress(),
                                             attraction.getLat(),
-                                            attraction.getLon()
+                                            attraction.getLon(),
+                                            attraction.getKakaomapUrl()
                                     )).toList();
 
                             AreaAttractionsDto dto = new AreaAttractionsDto();
@@ -148,7 +172,8 @@ public AttractionInfoDto getAttractionInfoData(long attractionId) {
                     attraction.getPhone(),
                     attraction.getHomepageUrl(),
                     attraction.getCloseDay(),
-                    attraction.getUseTime()
+                    attraction.getUseTime(),
+                    attraction.getKakaomapUrl()
             );
 
         } catch (RuntimeException e) {
diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties
index 75e7543..1c1abb2 100644
--- a/place-service/src/main/resources/application.properties
+++ b/place-service/src/main/resources/application.properties
@@ -10,7 +10,7 @@ spring.datasource.driver-class-name=org.postgresql.Driver
 spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
 spring.jpa.defer-datasource-initialization=true
 spring.jpa.hibernate.ddl-auto=update
-#spring.jpa.hibernate.ddl-auto=update
+# spring.jpa.hibernate.ddl-auto=create
 spring.jpa.show-sql=true
 
 #spring.sql.init.mode=always

From 24ba2d03883a8ec2b6d6763b08095dba29ca93b9 Mon Sep 17 00:00:00 2001
From: suminmmmmmmm 
Date: Sat, 26 Apr 2025 13:42:35 +0900
Subject: [PATCH 128/361] Merge

---
 .../placeservice/PlaceServiceApplication.java |  1 -
 .../userservice/controller/AdminSignup.java   | 26 ++++++++++++++
 .../example/userservice/dto/AdminLogin.java   |  9 +++++
 .../example/userservice/dto/MemberSign.java   | 26 ++++++++++++++
 .../example/userservice/entity/Member.java    | 34 +++++++++++++++++++
 .../repository/MemberRepository.java          |  9 +++++
 .../userservice/service/MemberService.java    | 29 ++++++++++++++++
 .../src/main/resources/application.properties |  7 ++++
 8 files changed, 140 insertions(+), 1 deletion(-)
 create mode 100644 user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
 create mode 100644 user-service/src/main/java/com/example/userservice/dto/AdminLogin.java
 create mode 100644 user-service/src/main/java/com/example/userservice/dto/MemberSign.java
 create mode 100644 user-service/src/main/java/com/example/userservice/entity/Member.java
 create mode 100644 user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
 create mode 100644 user-service/src/main/java/com/example/userservice/service/MemberService.java

diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
index beac01e..1784560 100644
--- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
+++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
@@ -45,7 +45,6 @@ public CommandLineRunner initData(RestaurantService restaurantService, Restauran
                 System.out.println("카페 데이터가 이미 존재합니다. 건너뜁니다.");
             }
 
-            
         };
     }
 }
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
new file mode 100644
index 0000000..79d254b
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
@@ -0,0 +1,26 @@
+package com.example.userservice.controller;
+
+import com.example.userservice.dto.MemberSign;
+import com.example.userservice.service.MemberService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+@Controller
+@RequiredArgsConstructor
+@RequestMapping("/admin")
+public class AdminSignup {
+    private final MemberService memberService;
+
+    @PostMapping("/signup")
+    public ResponseEntity signup(@RequestBody MemberSign dto) {
+        try{
+            memberService.registerAdmin(dto);
+            return ResponseEntity.ok("관리자 회원가입 성공");
+        }catch (IllegalArgumentException e){
+            return ResponseEntity.badRequest().body("회원가입 실패:"+e.getMessage());
+        }
+    }
+
+}
diff --git a/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java b/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java
new file mode 100644
index 0000000..52890e3
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java
@@ -0,0 +1,9 @@
+package com.example.userservice.dto;
+
+import lombok.Data;
+
+@Data
+public class AdminLogin {
+    private String memberId;
+    private String password;
+}
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
new file mode 100644
index 0000000..40981a0
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
@@ -0,0 +1,26 @@
+package com.example.userservice.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+
+public class MemberSign {
+    private Long memberId;
+    private String password;
+    private String nickName;
+    private String createTime;
+
+    public Long getMemberId() {
+        return memberId;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public String getNickname() {
+        return nickName;
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/entity/Member.java b/user-service/src/main/java/com/example/userservice/entity/Member.java
new file mode 100644
index 0000000..4e6791c
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/Member.java
@@ -0,0 +1,34 @@
+package com.example.userservice.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Getter
+@Setter
+@NoArgsConstructor // JPA용 기본 생성자
+@AllArgsConstructor // 모든 필드로 객체를 편하게 생성
+@Table(name = "member")
+public class Member {
+
+    @Id
+    @Column(name = "member_id",nullable = false)
+    private Long memberId;
+
+    @Column(nullable = false)
+    private String password;
+
+    @Column
+    private String nickname;
+
+    @Column(name = "create_at")
+    private String createAt;
+
+
+}
diff --git a/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java b/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
new file mode 100644
index 0000000..87c910d
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
@@ -0,0 +1,9 @@
+package com.example.userservice.repository;
+
+import com.example.userservice.entity.Member;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface MemberRepository extends JpaRepository {
+    boolean existsByMemberId(Long memberId);
+}
+
diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java
new file mode 100644
index 0000000..927c90a
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java
@@ -0,0 +1,29 @@
+package com.example.userservice.service;
+
+import com.example.userservice.dto.MemberSign;
+import com.example.userservice.entity.Member;
+import com.example.userservice.repository.MemberRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+
+@Service
+@RequiredArgsConstructor
+public class MemberService {
+    private final MemberRepository memberRepository;
+
+    public void registerAdmin(MemberSign dto) {
+        if(memberRepository.existsMemberById(Long.valueOf(String.valueOf(dto.getMemberId())))){
+            throw new IllegalStateException("이미 존재하는 ID입니다.");
+        }
+        Member member = new Member();
+        member.setMemberId(dto.getMemberId());
+        member.setPassword(dto.getPassword());
+        member.setNickname(dto.getNickname());
+        member.setCreateAt(String.valueOf(LocalDateTime.now()));
+
+        memberRepository.save(member);
+    }
+
+}
diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties
index 79b5bda..6d660a6 100644
--- a/user-service/src/main/resources/application.properties
+++ b/user-service/src/main/resources/application.properties
@@ -1 +1,8 @@
 spring.application.name=user-service
+
+spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db
+spring.datasource.username=root
+spring.datasource.password=admin
+spring.datasource.driver-class-name=org.postgresql.Driver
+spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
+spring.jpa.hibernate.ddl-auto=update
\ No newline at end of file

From 898ab868bcfa2dce5a4ba819a4b7f0fac1a94d47 Mon Sep 17 00:00:00 2001
From: suminmmmmmmm 
Date: Sat, 26 Apr 2025 13:42:35 +0900
Subject: [PATCH 129/361] Merge

---
 .../placeservice/PlaceServiceApplication.java |  1 -
 .../userservice/controller/AdminSignup.java   | 26 ++++++++++++++
 .../example/userservice/dto/AdminLogin.java   |  9 +++++
 .../example/userservice/dto/MemberSign.java   | 26 ++++++++++++++
 .../example/userservice/entity/Member.java    | 34 +++++++++++++++++++
 .../repository/MemberRepository.java          |  9 +++++
 .../userservice/service/MemberService.java    | 29 ++++++++++++++++
 .../src/main/resources/application.properties |  7 ++++
 8 files changed, 140 insertions(+), 1 deletion(-)
 create mode 100644 user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
 create mode 100644 user-service/src/main/java/com/example/userservice/dto/AdminLogin.java
 create mode 100644 user-service/src/main/java/com/example/userservice/dto/MemberSign.java
 create mode 100644 user-service/src/main/java/com/example/userservice/entity/Member.java
 create mode 100644 user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
 create mode 100644 user-service/src/main/java/com/example/userservice/service/MemberService.java

diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
index beac01e..1784560 100644
--- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
+++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
@@ -45,7 +45,6 @@ public CommandLineRunner initData(RestaurantService restaurantService, Restauran
                 System.out.println("카페 데이터가 이미 존재합니다. 건너뜁니다.");
             }
 
-            
         };
     }
 }
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
new file mode 100644
index 0000000..79d254b
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
@@ -0,0 +1,26 @@
+package com.example.userservice.controller;
+
+import com.example.userservice.dto.MemberSign;
+import com.example.userservice.service.MemberService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+@Controller
+@RequiredArgsConstructor
+@RequestMapping("/admin")
+public class AdminSignup {
+    private final MemberService memberService;
+
+    @PostMapping("/signup")
+    public ResponseEntity signup(@RequestBody MemberSign dto) {
+        try{
+            memberService.registerAdmin(dto);
+            return ResponseEntity.ok("관리자 회원가입 성공");
+        }catch (IllegalArgumentException e){
+            return ResponseEntity.badRequest().body("회원가입 실패:"+e.getMessage());
+        }
+    }
+
+}
diff --git a/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java b/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java
new file mode 100644
index 0000000..52890e3
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java
@@ -0,0 +1,9 @@
+package com.example.userservice.dto;
+
+import lombok.Data;
+
+@Data
+public class AdminLogin {
+    private String memberId;
+    private String password;
+}
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
new file mode 100644
index 0000000..40981a0
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
@@ -0,0 +1,26 @@
+package com.example.userservice.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+
+public class MemberSign {
+    private Long memberId;
+    private String password;
+    private String nickName;
+    private String createTime;
+
+    public Long getMemberId() {
+        return memberId;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public String getNickname() {
+        return nickName;
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/entity/Member.java b/user-service/src/main/java/com/example/userservice/entity/Member.java
new file mode 100644
index 0000000..4e6791c
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/Member.java
@@ -0,0 +1,34 @@
+package com.example.userservice.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Getter
+@Setter
+@NoArgsConstructor // JPA용 기본 생성자
+@AllArgsConstructor // 모든 필드로 객체를 편하게 생성
+@Table(name = "member")
+public class Member {
+
+    @Id
+    @Column(name = "member_id",nullable = false)
+    private Long memberId;
+
+    @Column(nullable = false)
+    private String password;
+
+    @Column
+    private String nickname;
+
+    @Column(name = "create_at")
+    private String createAt;
+
+
+}
diff --git a/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java b/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
new file mode 100644
index 0000000..87c910d
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
@@ -0,0 +1,9 @@
+package com.example.userservice.repository;
+
+import com.example.userservice.entity.Member;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface MemberRepository extends JpaRepository {
+    boolean existsByMemberId(Long memberId);
+}
+
diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java
new file mode 100644
index 0000000..927c90a
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java
@@ -0,0 +1,29 @@
+package com.example.userservice.service;
+
+import com.example.userservice.dto.MemberSign;
+import com.example.userservice.entity.Member;
+import com.example.userservice.repository.MemberRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+
+@Service
+@RequiredArgsConstructor
+public class MemberService {
+    private final MemberRepository memberRepository;
+
+    public void registerAdmin(MemberSign dto) {
+        if(memberRepository.existsMemberById(Long.valueOf(String.valueOf(dto.getMemberId())))){
+            throw new IllegalStateException("이미 존재하는 ID입니다.");
+        }
+        Member member = new Member();
+        member.setMemberId(dto.getMemberId());
+        member.setPassword(dto.getPassword());
+        member.setNickname(dto.getNickname());
+        member.setCreateAt(String.valueOf(LocalDateTime.now()));
+
+        memberRepository.save(member);
+    }
+
+}
diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties
index 79b5bda..6d660a6 100644
--- a/user-service/src/main/resources/application.properties
+++ b/user-service/src/main/resources/application.properties
@@ -1 +1,8 @@
 spring.application.name=user-service
+
+spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db
+spring.datasource.username=root
+spring.datasource.password=admin
+spring.datasource.driver-class-name=org.postgresql.Driver
+spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
+spring.jpa.hibernate.ddl-auto=update
\ No newline at end of file

From 7c119dbbe52cf8d3425dfd57a9a178f6748cdc8d Mon Sep 17 00:00:00 2001
From: suminmmmmmmm 
Date: Mon, 28 Apr 2025 09:37:49 +0900
Subject: [PATCH 130/361] =?UTF-8?q?Refactor:=20=ED=9A=8C=EC=9B=90=EA=B0=80?=
 =?UTF-8?q?=EC=9E=85=20=EA=B8=B0=EB=8A=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 user-service/.env                             |  1 +
 user-service/build.gradle                     |  1 +
 .../userservice/config/SecurityConfig.java    | 26 +++++++++
 .../userservice/controller/AdminSignup.java   | 33 +++++++++---
 .../example/userservice/dto/MemberSign.java   |  4 +-
 .../example/userservice/entity/Member.java    |  6 +--
 .../userservice/jwt/JwtTokenProvider.java     | 54 +++++++++++++++++++
 .../repository/MemberRepository.java          |  1 +
 .../userservice/service/MemberService.java    | 17 +++---
 .../src/main/resources/application.properties |  5 +-
 10 files changed, 127 insertions(+), 21 deletions(-)
 create mode 100644 user-service/.env
 create mode 100644 user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
 create mode 100644 user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java

diff --git a/user-service/.env b/user-service/.env
new file mode 100644
index 0000000..e5ee787
--- /dev/null
+++ b/user-service/.env
@@ -0,0 +1 @@
+JWT_SECRET_KEY=mySuperSuperSuperSecureKey_ThisIsMoreThanSixtyFourBytesLong_1234567890abcdef
diff --git a/user-service/build.gradle b/user-service/build.gradle
index 6329d25..0f3fee3 100644
--- a/user-service/build.gradle
+++ b/user-service/build.gradle
@@ -47,6 +47,7 @@ dependencies {
     testImplementation 'org.springframework.kafka:spring-kafka-test'
     testImplementation 'org.springframework.security:spring-security-test'
     testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+    implementation 'io.github.cdimascio:java-dotenv:5.2.2'
 }
 
 tasks.named('bootBuildImage') {
diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
new file mode 100644
index 0000000..ff04de3
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
@@ -0,0 +1,26 @@
+package com.example.userservice.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+public class SecurityConfig {
+
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        http.csrf().disable()
+                .authorizeHttpRequests((authz) -> authz
+                        .anyRequest().permitAll()
+                );
+        return http.build();
+    }
+
+    //비밀번호 암호화 적용
+    @Bean
+    public BCryptPasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
index 79d254b..cdbdf8a 100644
--- a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
+++ b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
@@ -1,26 +1,43 @@
 package com.example.userservice.controller;
 
 import com.example.userservice.dto.MemberSign;
+import com.example.userservice.entity.Member;
+import com.example.userservice.jwt.JwtTokenProvider;
 import com.example.userservice.service.MemberService;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.HashMap;
+import java.util.Map;
+
+// AdminSignup.java
 @Controller
 @RequiredArgsConstructor
-@RequestMapping("/admin")
+@RequestMapping("/auth")
 public class AdminSignup {
     private final MemberService memberService;
+    private final JwtTokenProvider jwtTokenProvider;
 
     @PostMapping("/signup")
-    public ResponseEntity signup(@RequestBody MemberSign dto) {
-        try{
-            memberService.registerAdmin(dto);
-            return ResponseEntity.ok("관리자 회원가입 성공");
-        }catch (IllegalArgumentException e){
-            return ResponseEntity.badRequest().body("회원가입 실패:"+e.getMessage());
+    public ResponseEntity> signup(@RequestBody MemberSign dto) {
+        try {
+            Member member = memberService.registerAdmin(dto);
+
+            // 회원가입 성공하면 토큰 발급
+            String accessToken = jwtTokenProvider.generateAccessToken(member.getNickname());
+            String refreshToken = jwtTokenProvider.generateRefreshToken(member.getNickname());
+
+            Map tokens = new HashMap<>();
+            tokens.put("accessToken", accessToken);
+            tokens.put("refreshToken", refreshToken);
+
+            return ResponseEntity.ok(tokens);
+        } catch (IllegalArgumentException e) {
+            Map errorResponse = new HashMap<>();
+            errorResponse.put("error", "회원가입 실패: " + e.getMessage());
+            return ResponseEntity.badRequest().body(errorResponse);
         }
     }
-
 }
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
index 40981a0..ab68e88 100644
--- a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
@@ -9,7 +9,7 @@
 public class MemberSign {
     private Long memberId;
     private String password;
-    private String nickName;
+    private String nickname;
     private String createTime;
 
     public Long getMemberId() {
@@ -21,6 +21,6 @@ public String getPassword() {
     }
 
     public String getNickname() {
-        return nickName;
+        return nickname;
     }
 }
diff --git a/user-service/src/main/java/com/example/userservice/entity/Member.java b/user-service/src/main/java/com/example/userservice/entity/Member.java
index 4e6791c..eee82fb 100644
--- a/user-service/src/main/java/com/example/userservice/entity/Member.java
+++ b/user-service/src/main/java/com/example/userservice/entity/Member.java
@@ -1,9 +1,6 @@
 package com.example.userservice.entity;
 
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import jakarta.persistence.Id;
-import jakarta.persistence.Table;
+import jakarta.persistence.*;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
@@ -18,6 +15,7 @@
 public class Member {
 
     @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
     @Column(name = "member_id",nullable = false)
     private Long memberId;
 
diff --git a/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java b/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java
new file mode 100644
index 0000000..7049e3e
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java
@@ -0,0 +1,54 @@
+package com.example.userservice.jwt;
+
+import io.github.cdimascio.dotenv.Dotenv;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.security.Keys;
+import jakarta.annotation.PostConstruct;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.util.Date;
+
+@Component
+public class JwtTokenProvider {
+
+    private SecretKey secretKey;
+
+    @PostConstruct
+    public void init() {
+        Dotenv dotenv = Dotenv.configure()
+                .ignoreIfMalformed()
+                .ignoreIfMissing()
+                .load();
+        String jwtSecret = dotenv.get("JWT_SECRET_KEY");
+        if (jwtSecret == null) {
+            throw new IllegalStateException("JWT_SECRET_KEY 환경변수가 설정되지 않았습니다!");
+        }
+        this.secretKey = Keys.hmacShaKeyFor(jwtSecret.getBytes());
+    }
+
+    public String generateAccessToken(String nickname) {
+        Date now = new Date();
+        Date expiryDate = new Date(now.getTime() + 1000 * 60 * 30); // 30분
+
+        return Jwts.builder()
+                .setSubject(nickname)
+                .setIssuedAt(now)
+                .setExpiration(expiryDate)
+                .signWith(secretKey, SignatureAlgorithm.HS512)
+                .compact();
+    }
+
+    public String generateRefreshToken(String nickname) {
+        Date now = new Date();
+        Date expiryDate = new Date(now.getTime() + 1000L * 60 * 60 * 24 * 7); // 7일
+
+        return Jwts.builder()
+                .setSubject(nickname)
+                .setIssuedAt(now)
+                .setExpiration(expiryDate)
+                .signWith(secretKey, SignatureAlgorithm.HS512)
+                .compact();
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java b/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
index 87c910d..14955d4 100644
--- a/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
+++ b/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
@@ -5,5 +5,6 @@
 
 public interface MemberRepository extends JpaRepository {
     boolean existsByMemberId(Long memberId);
+    boolean existsByNickname(String nickname);
 }
 
diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java
index 6b5e5d9..8dcf24c 100644
--- a/user-service/src/main/java/com/example/userservice/service/MemberService.java
+++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java
@@ -4,6 +4,7 @@
 import com.example.userservice.entity.Member;
 import com.example.userservice.repository.MemberRepository;
 import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
@@ -12,18 +13,22 @@
 @RequiredArgsConstructor
 public class MemberService {
     private final MemberRepository memberRepository;
+    private final BCryptPasswordEncoder passwordEncoder;
 
-    public void registerAdmin(MemberSign dto) {
-        if(memberRepository.existsByMemberId(Long.valueOf(String.valueOf(dto.getMemberId())))){
+    public Member registerAdmin(MemberSign dto) {
+        if (memberRepository.existsByMemberId(dto.getMemberId())) {
             throw new IllegalStateException("이미 존재하는 ID입니다.");
         }
+        if (memberRepository.existsByNickname(dto.getNickname())) {
+            throw new IllegalStateException("이미 존재하는 닉네임입니다.");
+        }
+
         Member member = new Member();
-        member.setMemberId(dto.getMemberId());
-        member.setPassword(dto.getPassword());
+        member.setPassword(passwordEncoder.encode(dto.getPassword()));
         member.setNickname(dto.getNickname());
         member.setCreateAt(String.valueOf(LocalDateTime.now()));
 
-        memberRepository.save(member);
+        Member savedMember = memberRepository.save(member);
+        return savedMember; // 저장된 객체 리턴
     }
-
 }
diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties
index 6d660a6..27a03fd 100644
--- a/user-service/src/main/resources/application.properties
+++ b/user-service/src/main/resources/application.properties
@@ -5,4 +5,7 @@ spring.datasource.username=root
 spring.datasource.password=admin
 spring.datasource.driver-class-name=org.postgresql.Driver
 spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
-spring.jpa.hibernate.ddl-auto=update
\ No newline at end of file
+spring.jpa.hibernate.ddl-auto=update
+
+
+jwt.secret=${JWT_SECRET_KEY}

From c0393c0de9e6a4489b96c93bb3d85e1116b4892d Mon Sep 17 00:00:00 2001
From: suminmmmmmmm 
Date: Mon, 28 Apr 2025 09:37:49 +0900
Subject: [PATCH 131/361] =?UTF-8?q?Refactor:=20=ED=9A=8C=EC=9B=90=EA=B0=80?=
 =?UTF-8?q?=EC=9E=85=20=EA=B8=B0=EB=8A=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 user-service/.env                             |  1 +
 user-service/build.gradle                     |  1 +
 .../userservice/config/SecurityConfig.java    | 26 +++++++++
 .../userservice/controller/AdminSignup.java   | 33 +++++++++---
 .../example/userservice/dto/MemberSign.java   |  4 +-
 .../example/userservice/entity/Member.java    |  6 +--
 .../userservice/jwt/JwtTokenProvider.java     | 54 +++++++++++++++++++
 .../repository/MemberRepository.java          |  1 +
 .../userservice/service/MemberService.java    | 17 +++---
 .../src/main/resources/application.properties |  5 +-
 10 files changed, 127 insertions(+), 21 deletions(-)
 create mode 100644 user-service/.env
 create mode 100644 user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
 create mode 100644 user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java

diff --git a/user-service/.env b/user-service/.env
new file mode 100644
index 0000000..e5ee787
--- /dev/null
+++ b/user-service/.env
@@ -0,0 +1 @@
+JWT_SECRET_KEY=mySuperSuperSuperSecureKey_ThisIsMoreThanSixtyFourBytesLong_1234567890abcdef
diff --git a/user-service/build.gradle b/user-service/build.gradle
index 6329d25..0f3fee3 100644
--- a/user-service/build.gradle
+++ b/user-service/build.gradle
@@ -47,6 +47,7 @@ dependencies {
     testImplementation 'org.springframework.kafka:spring-kafka-test'
     testImplementation 'org.springframework.security:spring-security-test'
     testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+    implementation 'io.github.cdimascio:java-dotenv:5.2.2'
 }
 
 tasks.named('bootBuildImage') {
diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
new file mode 100644
index 0000000..ff04de3
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
@@ -0,0 +1,26 @@
+package com.example.userservice.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+public class SecurityConfig {
+
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        http.csrf().disable()
+                .authorizeHttpRequests((authz) -> authz
+                        .anyRequest().permitAll()
+                );
+        return http.build();
+    }
+
+    //비밀번호 암호화 적용
+    @Bean
+    public BCryptPasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
index 79d254b..cdbdf8a 100644
--- a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
+++ b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
@@ -1,26 +1,43 @@
 package com.example.userservice.controller;
 
 import com.example.userservice.dto.MemberSign;
+import com.example.userservice.entity.Member;
+import com.example.userservice.jwt.JwtTokenProvider;
 import com.example.userservice.service.MemberService;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.HashMap;
+import java.util.Map;
+
+// AdminSignup.java
 @Controller
 @RequiredArgsConstructor
-@RequestMapping("/admin")
+@RequestMapping("/auth")
 public class AdminSignup {
     private final MemberService memberService;
+    private final JwtTokenProvider jwtTokenProvider;
 
     @PostMapping("/signup")
-    public ResponseEntity signup(@RequestBody MemberSign dto) {
-        try{
-            memberService.registerAdmin(dto);
-            return ResponseEntity.ok("관리자 회원가입 성공");
-        }catch (IllegalArgumentException e){
-            return ResponseEntity.badRequest().body("회원가입 실패:"+e.getMessage());
+    public ResponseEntity> signup(@RequestBody MemberSign dto) {
+        try {
+            Member member = memberService.registerAdmin(dto);
+
+            // 회원가입 성공하면 토큰 발급
+            String accessToken = jwtTokenProvider.generateAccessToken(member.getNickname());
+            String refreshToken = jwtTokenProvider.generateRefreshToken(member.getNickname());
+
+            Map tokens = new HashMap<>();
+            tokens.put("accessToken", accessToken);
+            tokens.put("refreshToken", refreshToken);
+
+            return ResponseEntity.ok(tokens);
+        } catch (IllegalArgumentException e) {
+            Map errorResponse = new HashMap<>();
+            errorResponse.put("error", "회원가입 실패: " + e.getMessage());
+            return ResponseEntity.badRequest().body(errorResponse);
         }
     }
-
 }
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
index 40981a0..ab68e88 100644
--- a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
@@ -9,7 +9,7 @@
 public class MemberSign {
     private Long memberId;
     private String password;
-    private String nickName;
+    private String nickname;
     private String createTime;
 
     public Long getMemberId() {
@@ -21,6 +21,6 @@ public String getPassword() {
     }
 
     public String getNickname() {
-        return nickName;
+        return nickname;
     }
 }
diff --git a/user-service/src/main/java/com/example/userservice/entity/Member.java b/user-service/src/main/java/com/example/userservice/entity/Member.java
index 4e6791c..eee82fb 100644
--- a/user-service/src/main/java/com/example/userservice/entity/Member.java
+++ b/user-service/src/main/java/com/example/userservice/entity/Member.java
@@ -1,9 +1,6 @@
 package com.example.userservice.entity;
 
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import jakarta.persistence.Id;
-import jakarta.persistence.Table;
+import jakarta.persistence.*;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
@@ -18,6 +15,7 @@
 public class Member {
 
     @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
     @Column(name = "member_id",nullable = false)
     private Long memberId;
 
diff --git a/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java b/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java
new file mode 100644
index 0000000..7049e3e
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java
@@ -0,0 +1,54 @@
+package com.example.userservice.jwt;
+
+import io.github.cdimascio.dotenv.Dotenv;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.security.Keys;
+import jakarta.annotation.PostConstruct;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.util.Date;
+
+@Component
+public class JwtTokenProvider {
+
+    private SecretKey secretKey;
+
+    @PostConstruct
+    public void init() {
+        Dotenv dotenv = Dotenv.configure()
+                .ignoreIfMalformed()
+                .ignoreIfMissing()
+                .load();
+        String jwtSecret = dotenv.get("JWT_SECRET_KEY");
+        if (jwtSecret == null) {
+            throw new IllegalStateException("JWT_SECRET_KEY 환경변수가 설정되지 않았습니다!");
+        }
+        this.secretKey = Keys.hmacShaKeyFor(jwtSecret.getBytes());
+    }
+
+    public String generateAccessToken(String nickname) {
+        Date now = new Date();
+        Date expiryDate = new Date(now.getTime() + 1000 * 60 * 30); // 30분
+
+        return Jwts.builder()
+                .setSubject(nickname)
+                .setIssuedAt(now)
+                .setExpiration(expiryDate)
+                .signWith(secretKey, SignatureAlgorithm.HS512)
+                .compact();
+    }
+
+    public String generateRefreshToken(String nickname) {
+        Date now = new Date();
+        Date expiryDate = new Date(now.getTime() + 1000L * 60 * 60 * 24 * 7); // 7일
+
+        return Jwts.builder()
+                .setSubject(nickname)
+                .setIssuedAt(now)
+                .setExpiration(expiryDate)
+                .signWith(secretKey, SignatureAlgorithm.HS512)
+                .compact();
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java b/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
index 87c910d..14955d4 100644
--- a/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
+++ b/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
@@ -5,5 +5,6 @@
 
 public interface MemberRepository extends JpaRepository {
     boolean existsByMemberId(Long memberId);
+    boolean existsByNickname(String nickname);
 }
 
diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java
index 6b5e5d9..8dcf24c 100644
--- a/user-service/src/main/java/com/example/userservice/service/MemberService.java
+++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java
@@ -4,6 +4,7 @@
 import com.example.userservice.entity.Member;
 import com.example.userservice.repository.MemberRepository;
 import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
@@ -12,18 +13,22 @@
 @RequiredArgsConstructor
 public class MemberService {
     private final MemberRepository memberRepository;
+    private final BCryptPasswordEncoder passwordEncoder;
 
-    public void registerAdmin(MemberSign dto) {
-        if(memberRepository.existsByMemberId(Long.valueOf(String.valueOf(dto.getMemberId())))){
+    public Member registerAdmin(MemberSign dto) {
+        if (memberRepository.existsByMemberId(dto.getMemberId())) {
             throw new IllegalStateException("이미 존재하는 ID입니다.");
         }
+        if (memberRepository.existsByNickname(dto.getNickname())) {
+            throw new IllegalStateException("이미 존재하는 닉네임입니다.");
+        }
+
         Member member = new Member();
-        member.setMemberId(dto.getMemberId());
-        member.setPassword(dto.getPassword());
+        member.setPassword(passwordEncoder.encode(dto.getPassword()));
         member.setNickname(dto.getNickname());
         member.setCreateAt(String.valueOf(LocalDateTime.now()));
 
-        memberRepository.save(member);
+        Member savedMember = memberRepository.save(member);
+        return savedMember; // 저장된 객체 리턴
     }
-
 }
diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties
index 6d660a6..27a03fd 100644
--- a/user-service/src/main/resources/application.properties
+++ b/user-service/src/main/resources/application.properties
@@ -5,4 +5,7 @@ spring.datasource.username=root
 spring.datasource.password=admin
 spring.datasource.driver-class-name=org.postgresql.Driver
 spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
-spring.jpa.hibernate.ddl-auto=update
\ No newline at end of file
+spring.jpa.hibernate.ddl-auto=update
+
+
+jwt.secret=${JWT_SECRET_KEY}

From 55328bd4b51e0cc669f43a432e89a9bf7e2d8108 Mon Sep 17 00:00:00 2001
From: suminmmmmmmm 
Date: Mon, 28 Apr 2025 10:00:12 +0900
Subject: [PATCH 132/361] =?UTF-8?q?Refactor:=20member=5Fid=20=EC=88=9C?=
 =?UTF-8?q?=EC=B0=A8=EC=A0=81=20=EC=A6=9D=EA=B0=80=20=EC=88=98=EC=A0=95?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../main/java/com/example/userservice/dto/AdminLogin.java | 2 +-
 .../main/java/com/example/userservice/dto/MemberSign.java | 8 ++++----
 .../main/java/com/example/userservice/entity/Member.java  | 2 +-
 .../com/example/userservice/service/MemberService.java    | 6 +++---
 user-service/src/main/resources/application.properties    | 2 +-
 5 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java b/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java
index 52890e3..cbff3ed 100644
--- a/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java
+++ b/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java
@@ -4,6 +4,6 @@
 
 @Data
 public class AdminLogin {
-    private String memberId;
+    private Long memberId;
     private String password;
 }
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
index ab68e88..a648540 100644
--- a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
@@ -7,14 +7,14 @@
 @Setter
 
 public class MemberSign {
-    private Long memberId;
+    //private Long memberId;
     private String password;
     private String nickname;
     private String createTime;
 
-    public Long getMemberId() {
-        return memberId;
-    }
+//    public Long getMemberId() {
+//        return memberId;
+//    }
 
     public String getPassword() {
         return password;
diff --git a/user-service/src/main/java/com/example/userservice/entity/Member.java b/user-service/src/main/java/com/example/userservice/entity/Member.java
index eee82fb..a6e4b46 100644
--- a/user-service/src/main/java/com/example/userservice/entity/Member.java
+++ b/user-service/src/main/java/com/example/userservice/entity/Member.java
@@ -15,7 +15,7 @@
 public class Member {
 
     @Id
-    @GeneratedValue(strategy = GenerationType.AUTO)
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
     @Column(name = "member_id",nullable = false)
     private Long memberId;
 
diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java
index 8dcf24c..6f2f58f 100644
--- a/user-service/src/main/java/com/example/userservice/service/MemberService.java
+++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java
@@ -16,9 +16,9 @@ public class MemberService {
     private final BCryptPasswordEncoder passwordEncoder;
 
     public Member registerAdmin(MemberSign dto) {
-        if (memberRepository.existsByMemberId(dto.getMemberId())) {
-            throw new IllegalStateException("이미 존재하는 ID입니다.");
-        }
+//        if (memberRepository.existsByMemberId(dto.getMemberId())) {
+//            throw new IllegalStateException("이미 존재하는 ID입니다.");
+//        }
         if (memberRepository.existsByNickname(dto.getNickname())) {
             throw new IllegalStateException("이미 존재하는 닉네임입니다.");
         }
diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties
index 27a03fd..a2c035e 100644
--- a/user-service/src/main/resources/application.properties
+++ b/user-service/src/main/resources/application.properties
@@ -5,7 +5,7 @@ spring.datasource.username=root
 spring.datasource.password=admin
 spring.datasource.driver-class-name=org.postgresql.Driver
 spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
-spring.jpa.hibernate.ddl-auto=update
+spring.jpa.hibernate.ddl-auto=create
 
 
 jwt.secret=${JWT_SECRET_KEY}

From 419f2add00452faca7e2752a44daef2430eafb33 Mon Sep 17 00:00:00 2001
From: suminmmmmmmm 
Date: Mon, 28 Apr 2025 10:00:12 +0900
Subject: [PATCH 133/361] =?UTF-8?q?Refactor:=20member=5Fid=20=EC=88=9C?=
 =?UTF-8?q?=EC=B0=A8=EC=A0=81=20=EC=A6=9D=EA=B0=80=20=EC=88=98=EC=A0=95?=
 =?UTF-8?q?=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../main/java/com/example/userservice/dto/AdminLogin.java | 2 +-
 .../main/java/com/example/userservice/dto/MemberSign.java | 8 ++++----
 .../main/java/com/example/userservice/entity/Member.java  | 2 +-
 .../com/example/userservice/service/MemberService.java    | 6 +++---
 user-service/src/main/resources/application.properties    | 2 +-
 5 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java b/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java
index 52890e3..cbff3ed 100644
--- a/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java
+++ b/user-service/src/main/java/com/example/userservice/dto/AdminLogin.java
@@ -4,6 +4,6 @@
 
 @Data
 public class AdminLogin {
-    private String memberId;
+    private Long memberId;
     private String password;
 }
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
index ab68e88..a648540 100644
--- a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
@@ -7,14 +7,14 @@
 @Setter
 
 public class MemberSign {
-    private Long memberId;
+    //private Long memberId;
     private String password;
     private String nickname;
     private String createTime;
 
-    public Long getMemberId() {
-        return memberId;
-    }
+//    public Long getMemberId() {
+//        return memberId;
+//    }
 
     public String getPassword() {
         return password;
diff --git a/user-service/src/main/java/com/example/userservice/entity/Member.java b/user-service/src/main/java/com/example/userservice/entity/Member.java
index eee82fb..a6e4b46 100644
--- a/user-service/src/main/java/com/example/userservice/entity/Member.java
+++ b/user-service/src/main/java/com/example/userservice/entity/Member.java
@@ -15,7 +15,7 @@
 public class Member {
 
     @Id
-    @GeneratedValue(strategy = GenerationType.AUTO)
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
     @Column(name = "member_id",nullable = false)
     private Long memberId;
 
diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java
index 8dcf24c..6f2f58f 100644
--- a/user-service/src/main/java/com/example/userservice/service/MemberService.java
+++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java
@@ -16,9 +16,9 @@ public class MemberService {
     private final BCryptPasswordEncoder passwordEncoder;
 
     public Member registerAdmin(MemberSign dto) {
-        if (memberRepository.existsByMemberId(dto.getMemberId())) {
-            throw new IllegalStateException("이미 존재하는 ID입니다.");
-        }
+//        if (memberRepository.existsByMemberId(dto.getMemberId())) {
+//            throw new IllegalStateException("이미 존재하는 ID입니다.");
+//        }
         if (memberRepository.existsByNickname(dto.getNickname())) {
             throw new IllegalStateException("이미 존재하는 닉네임입니다.");
         }
diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties
index 27a03fd..a2c035e 100644
--- a/user-service/src/main/resources/application.properties
+++ b/user-service/src/main/resources/application.properties
@@ -5,7 +5,7 @@ spring.datasource.username=root
 spring.datasource.password=admin
 spring.datasource.driver-class-name=org.postgresql.Driver
 spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
-spring.jpa.hibernate.ddl-auto=update
+spring.jpa.hibernate.ddl-auto=create
 
 
 jwt.secret=${JWT_SECRET_KEY}

From 22381f70fa0d8fb02b4d99c460ec14504df2e82f Mon Sep 17 00:00:00 2001
From: YunHwa0905 
Date: Mon, 28 Apr 2025 10:00:56 +0900
Subject: [PATCH 134/361] Feat:login&logout

---
 .../userservice/config/ApplicationConfig.java |  50 +++++++++
 .../userservice/config/SecurityConfig.java    |  39 +++++++
 .../controller/AuthController.java            |  48 +++++++++
 .../com/example/userservice/dto/AuthDto.java  |  54 ++++++++++
 .../example/userservice/dto/MemberDto.java    |  44 ++++++++
 .../example/userservice/entity/Member.java    |  34 ++++++
 .../userservice/entity/RefreshToken.java      |  20 ++++
 .../repository/MemberRepository.java          |  12 +++
 .../repository/RefreshTokenRepository.java    |  13 +++
 .../security/JwtAuthenticationFilter.java     |  79 ++++++++++++++
 .../example/userservice/security/JwtUtil.java | 100 ++++++++++++++++++
 .../userservice/service/AuthService.java      |  69 ++++++++++++
 .../userservice/service/CookieUtil.java       |  21 ++++
 .../userservice/service/TokenService.java     |  58 ++++++++++
 .../src/main/resources/application.properties |  25 +++++
 15 files changed, 666 insertions(+)
 create mode 100644 user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
 create mode 100644 user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
 create mode 100644 user-service/src/main/java/com/example/userservice/controller/AuthController.java
 create mode 100644 user-service/src/main/java/com/example/userservice/dto/AuthDto.java
 create mode 100644 user-service/src/main/java/com/example/userservice/dto/MemberDto.java
 create mode 100644 user-service/src/main/java/com/example/userservice/entity/Member.java
 create mode 100644 user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
 create mode 100644 user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
 create mode 100644 user-service/src/main/java/com/example/userservice/repository/RefreshTokenRepository.java
 create mode 100644 user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
 create mode 100644 user-service/src/main/java/com/example/userservice/security/JwtUtil.java
 create mode 100644 user-service/src/main/java/com/example/userservice/service/AuthService.java
 create mode 100644 user-service/src/main/java/com/example/userservice/service/CookieUtil.java
 create mode 100644 user-service/src/main/java/com/example/userservice/service/TokenService.java

diff --git a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
new file mode 100644
index 0000000..68fa8dd
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
@@ -0,0 +1,50 @@
+package com.example.userservice.config;
+
+import com.example.userservice.repository.MemberRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@Configuration
+@RequiredArgsConstructor
+public class ApplicationConfig {
+
+    private final MemberRepository memberRepository;
+
+    @Bean
+    public UserDetailsService userDetailsService() {
+        return username -> memberRepository.findByMemberId(username)
+                .map(member -> new org.springframework.security.core.userdetails.User(
+                        member.getMemberId(),
+                        member.getPassword(),
+                        java.util.Collections.emptyList()
+                ))
+                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
+    }
+
+    @Bean
+    public AuthenticationProvider authenticationProvider() {
+        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
+        authProvider.setUserDetailsService(userDetailsService());
+        authProvider.setPasswordEncoder(passwordEncoder());
+        return authProvider;
+    }
+
+    @Bean
+    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
+        return config.getAuthenticationManager();
+    }
+
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
new file mode 100644
index 0000000..3b7ad88
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
@@ -0,0 +1,39 @@
+package com.example.userservice.config;
+
+import com.example.userservice.security.JwtAuthenticationFilter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+    private final JwtAuthenticationFilter jwtAuthenticationFilter;
+    private final AuthenticationProvider authenticationProvider;
+
+    @Bean
+    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+        http
+                .csrf(csrf -> csrf.disable())
+                .authorizeHttpRequests(auth -> auth
+                        .requestMatchers("/auth/**").permitAll()
+                        .requestMatchers("/admin/**").authenticated()
+                        .anyRequest().authenticated()
+                )
+                .sessionManagement(session -> session
+                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+                )
+                .authenticationProvider(authenticationProvider)
+                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
+
+        return http.build();
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/controller/AuthController.java b/user-service/src/main/java/com/example/userservice/controller/AuthController.java
new file mode 100644
index 0000000..6183070
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/controller/AuthController.java
@@ -0,0 +1,48 @@
+package com.example.userservice.controller;
+
+import com.example.userservice.dto.AuthDto;
+import com.example.userservice.service.AuthService;
+import com.example.userservice.service.CookieUtil;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/auth")
+@RequiredArgsConstructor
+public class AuthController {
+
+    private final AuthService authService;
+    private final CookieUtil cookieUtil;
+
+    @PostMapping("/login")
+    public ResponseEntity login(
+            @RequestBody AuthDto.LoginRequest request,
+            HttpServletResponse response) {
+
+        AuthDto.LoginResponse loginResponse = authService.login(request);
+
+        // 쿠키에 토큰 저장
+        cookieUtil.addCookie(response, "access_token", loginResponse.getAccessToken(), cookieUtil.getDaysInSeconds(1));
+        cookieUtil.addCookie(response, "refresh_token", loginResponse.getRefreshToken(), cookieUtil.getDaysInSeconds(30));
+
+        return ResponseEntity.ok(loginResponse);
+    }
+
+    @PostMapping("/logout")
+    public ResponseEntity logout(
+            @RequestBody AuthDto.LogoutRequest request,
+            HttpServletResponse response) {
+
+        // 쿠키 삭제
+        cookieUtil.addCookie(response, "access_token", "", 0);
+        cookieUtil.addCookie(response, "refresh_token", "", 0);
+
+        // authService의 logout 메서드 호출
+        return ResponseEntity.ok(authService.logout(request));
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/AuthDto.java b/user-service/src/main/java/com/example/userservice/dto/AuthDto.java
new file mode 100644
index 0000000..a2511cf
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/AuthDto.java
@@ -0,0 +1,54 @@
+package com.example.userservice.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+public class AuthDto {
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LoginRequest {
+        private String user_id;
+        private String password;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LoginResponse {
+        private String accessToken;
+        private String refreshToken;  // 리프레시 토큰 필드 추가
+        private String user_id;
+        private String nickname;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LogoutRequest {
+        private String accessToken;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LogoutResponse {
+        private boolean success;
+        private String message;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class RefreshTokenRequest {
+        private String refreshToken;
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java
new file mode 100644
index 0000000..3648c46
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java
@@ -0,0 +1,44 @@
+package com.example.userservice.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+public class MemberDto {
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class MemberResponse {
+        private String user_id;
+        private String nickname;
+        private Short birth_year;
+        private LocalDateTime created_at;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class UpdateRequest {
+        private String nickname;
+        private Short birth_year;
+        private String current_password;
+        private String new_password;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class RegisterRequest {
+        private String user_id;
+        private String password;
+        private String nickname;
+        private Short birth_year;
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/Member.java b/user-service/src/main/java/com/example/userservice/entity/Member.java
new file mode 100644
index 0000000..d0817a0
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/Member.java
@@ -0,0 +1,34 @@
+package com.example.userservice.entity;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "member")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Member {
+
+    @Id
+    @Column(name = "member_id")
+    private String memberId;
+
+    @Column(nullable = false)
+    private String password;
+
+    @Column(name = "nickname")
+    private String nickname;
+
+    @Column(name = "birth_year")
+    private Short birthYear;
+
+    @Column(name = "created_at")
+    private LocalDateTime createdAt;
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
new file mode 100644
index 0000000..9b0d8ee
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
@@ -0,0 +1,20 @@
+package com.example.userservice.entity;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "refresh_token")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class RefreshToken {
+    @Id
+    @Column(name = "user_id")
+    private Long userId;
+
+    @Column(name = "refresh_token", length = 512)
+    private String refreshToken;
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java b/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
new file mode 100644
index 0000000..631119c
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
@@ -0,0 +1,12 @@
+package com.example.userservice.repository;
+
+import com.example.userservice.entity.Member;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface MemberRepository extends JpaRepository {
+    Optional findByMemberId(String memberId);
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/repository/RefreshTokenRepository.java b/user-service/src/main/java/com/example/userservice/repository/RefreshTokenRepository.java
new file mode 100644
index 0000000..65adb8a
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/RefreshTokenRepository.java
@@ -0,0 +1,13 @@
+package com.example.userservice.repository;
+
+import com.example.userservice.entity.RefreshToken;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface RefreshTokenRepository extends JpaRepository {
+    Optional findByUserId(Long userId);
+    Optional findByRefreshToken(String refreshToken);
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..e265a4a
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
@@ -0,0 +1,79 @@
+package com.example.userservice.security;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+    private final JwtUtil jwtUtil;
+    private final UserDetailsService userDetailsService;
+
+    @Override
+    protected void doFilterInternal(
+            HttpServletRequest request,
+            HttpServletResponse response,
+            FilterChain filterChain
+    ) throws ServletException, IOException {
+
+        // Authorization 헤더에서 토큰 추출 시도
+        final String authHeader = request.getHeader("Authorization");
+        String jwt = null;
+
+        // Authorization 헤더가 있으면 해당 토큰 사용
+        if (authHeader != null && authHeader.startsWith("Bearer ")) {
+            jwt = authHeader.substring(7);
+        }
+        // 아니면 쿠키에서 토큰 찾기
+        else {
+            Cookie[] cookies = request.getCookies();
+            if (cookies != null) {
+                for (Cookie cookie : cookies) {
+                    if ("access_token".equals(cookie.getName())) {
+                        jwt = cookie.getValue();
+                        break;
+                    }
+                }
+            }
+        }
+
+        // 토큰이 없으면 다음 필터로 이동
+        if (jwt == null) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        final String userEmail = jwtUtil.extractUsername(jwt);
+
+        if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+            UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
+
+            if (jwtUtil.validateToken(jwt, userDetails)) {
+                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
+                        userDetails,
+                        null,
+                        userDetails.getAuthorities()
+                );
+                authToken.setDetails(
+                        new WebAuthenticationDetailsSource().buildDetails(request)
+                );
+                SecurityContextHolder.getContext().setAuthentication(authToken);
+            }
+        }
+        filterChain.doFilter(request, response);
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/security/JwtUtil.java b/user-service/src/main/java/com/example/userservice/security/JwtUtil.java
new file mode 100644
index 0000000..26e4b05
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/security/JwtUtil.java
@@ -0,0 +1,100 @@
+package com.example.userservice.security;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+@Component
+public class JwtUtil {
+
+    @Value("${jwt.secret}")
+    private String secretKey;
+
+    @Value("${jwt.expiration}")
+    private Long expiration;
+
+    @Value("${jwt.refresh-expiration}")
+    private Long refreshExpiration;
+
+    public String extractUsername(String token) {
+        return extractClaim(token, Claims::getSubject);
+    }
+
+    public Date extractExpiration(String token) {
+        return extractClaim(token, Claims::getExpiration);
+    }
+
+    public  T extractClaim(String token, Function claimsResolver) {
+        final Claims claims = extractAllClaims(token);
+        return claimsResolver.apply(claims);
+    }
+
+    private Claims extractAllClaims(String token) {
+        return Jwts
+                .parser()
+                .verifyWith(getSignInKey())
+                .build()
+                .parseSignedClaims(token)
+                .getPayload();
+    }
+
+    private Boolean isTokenExpired(String token) {
+        return extractExpiration(token).before(new Date());
+    }
+
+    // 일반 액세스 토큰 생성
+    public String generateToken(UserDetails userDetails) {
+        Map claims = new HashMap<>();
+        return createToken(claims, userDetails.getUsername(), expiration);
+    }
+
+    // 리프레시 토큰 생성
+    public String generateRefreshToken(UserDetails userDetails) {
+        Map claims = new HashMap<>();
+        return createToken(claims, userDetails.getUsername(), refreshExpiration);
+    }
+
+    // 토큰 생성 메소드 (유효기간 파라미터 추가)
+    private String createToken(Map claims, String subject, Long expirationTime) {
+        return Jwts.builder()
+                .claims(claims)
+                .subject(subject)
+                .issuedAt(new Date(System.currentTimeMillis()))
+                .expiration(new Date(System.currentTimeMillis() + expirationTime))
+                .signWith(getSignInKey())
+                .compact();
+    }
+
+    // 토큰 유효성 검증
+    public Boolean validateToken(String token, UserDetails userDetails) {
+        final String username = extractUsername(token);
+        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
+    }
+
+    // 토큰 유효성만 검사 (사용자 정보 없이)
+    public Boolean validateToken(String token) {
+        try {
+            Jwts.parser()
+                    .verifyWith(getSignInKey())
+                    .build()
+                    .parseSignedClaims(token);
+            return !isTokenExpired(token);
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    private SecretKey getSignInKey() {
+        byte[] keyBytes = secretKey.getBytes();
+        return Keys.hmacShaKeyFor(keyBytes);
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/AuthService.java b/user-service/src/main/java/com/example/userservice/service/AuthService.java
new file mode 100644
index 0000000..893d4fc
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/AuthService.java
@@ -0,0 +1,69 @@
+package com.example.userservice.service;
+
+import com.example.userservice.dto.AuthDto;
+import com.example.userservice.entity.Member;
+import com.example.userservice.repository.MemberRepository;
+import com.example.userservice.security.JwtUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class AuthService {
+
+    private final MemberRepository memberRepository;
+    private final JwtUtil jwtUtil;
+    private final AuthenticationManager authenticationManager;
+    private final TokenService tokenService;
+
+    public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
+        // 인증 시도
+        authenticationManager.authenticate(
+                new UsernamePasswordAuthenticationToken(
+                        request.getUser_id(),
+                        request.getPassword()
+                )
+        );
+
+        // 사용자 조회
+        Optional memberOptional = memberRepository.findByMemberId(request.getUser_id());
+        if (memberOptional.isEmpty()) {
+            throw new RuntimeException("User not found");
+        }
+
+        Member member = memberOptional.get();
+
+        // JWT 토큰 생성
+        UserDetails userDetails = new User(member.getMemberId(), member.getPassword(), Collections.emptyList());
+        String accessToken = jwtUtil.generateToken(userDetails);
+        String refreshToken = jwtUtil.generateRefreshToken(userDetails);
+
+        // 리프레시 토큰 저장
+        tokenService.saveRefreshToken(member.getMemberId(), refreshToken);
+
+        return AuthDto.LoginResponse.builder()
+                .accessToken(accessToken)
+                .refreshToken(refreshToken)
+                .user_id(member.getMemberId())
+                .nickname(member.getNickname())
+                .build();
+    }
+
+    // LogoutRequest 파라미터를 받는 메서드로 수정
+    public AuthDto.LogoutResponse logout(AuthDto.LogoutRequest request) {
+        // JWT는 stateless이므로 서버 측에서는 특별히 처리할 작업이 없음
+        // 클라이언트 측에서 토큰을 제거하도록 안내
+
+        return AuthDto.LogoutResponse.builder()
+                .success(true)
+                .message("Logout successful")
+                .build();
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/CookieUtil.java b/user-service/src/main/java/com/example/userservice/service/CookieUtil.java
new file mode 100644
index 0000000..2a1ab1f
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/CookieUtil.java
@@ -0,0 +1,21 @@
+package com.example.userservice.service;
+
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Service;
+
+@Service
+public class CookieUtil {
+
+    public void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
+        Cookie cookie = new Cookie(name, value);
+        cookie.setPath("/");
+        cookie.setHttpOnly(true);
+        cookie.setMaxAge(maxAge);
+        response.addCookie(cookie);
+    }
+
+    public int getDaysInSeconds(int days) {
+        return days * 24 * 60 * 60;
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/TokenService.java b/user-service/src/main/java/com/example/userservice/service/TokenService.java
new file mode 100644
index 0000000..931ab2e
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/TokenService.java
@@ -0,0 +1,58 @@
+package com.example.userservice.service;
+
+import com.example.userservice.entity.Member;
+import com.example.userservice.entity.RefreshToken;
+import com.example.userservice.repository.MemberRepository;
+import com.example.userservice.repository.RefreshTokenRepository;
+import com.example.userservice.security.JwtUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.UUID;
+
+@Service
+@RequiredArgsConstructor
+public class TokenService {
+
+    private final JwtUtil jwtUtil;
+    private final MemberRepository memberRepository;
+    private final RefreshTokenRepository refreshTokenRepository;
+
+    public String createNewAccessToken(String refreshToken) {
+        // 리프레시 토큰 유효성 검사
+        if (!jwtUtil.validateToken(refreshToken)) {
+            throw new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다.");
+        }
+
+        // 데이터베이스에서 리프레시 토큰 확인
+        Optional storedToken = refreshTokenRepository.findByRefreshToken(refreshToken);
+        if (storedToken.isEmpty()) {
+            throw new IllegalArgumentException("존재하지 않는 리프레시 토큰입니다.");
+        }
+
+        // 사용자 ID를 문자열로 변환
+        Long userId = storedToken.get().getUserId();
+        String userIdStr = String.valueOf(userId);
+
+        // 사용자 조회
+        Member member = memberRepository.findById(userIdStr)
+                .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
+
+        // 새 액세스 토큰 생성
+        UserDetails userDetails = new User(member.getMemberId(), member.getPassword(), Collections.emptyList());
+        return jwtUtil.generateToken(userDetails);
+    }
+
+    public void saveRefreshToken(String memberId, String refreshToken) {
+        // String memberId를 고유 숫자 ID로 변환
+        // 여기서는 memberId 문자열 자체 대신 해시값을 사용하여 숫자로 변환
+        long userId = Math.abs(UUID.nameUUIDFromBytes(memberId.getBytes()).getMostSignificantBits());
+
+        RefreshToken token = new RefreshToken(userId, refreshToken);
+        refreshTokenRepository.save(token);
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties
index 79b5bda..64e20b9 100644
--- a/user-service/src/main/resources/application.properties
+++ b/user-service/src/main/resources/application.properties
@@ -1 +1,26 @@
 spring.application.name=user-service
+
+# ?? ??
+server.port=8082
+
+spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db
+spring.datasource.username=root
+spring.datasource.password=admin
+spring.datasource.driver-class-name=org.postgresql.Driver
+
+# JPA ??
+spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
+spring.jpa.hibernate.ddl-auto=update
+spring.jpa.show-sql=true
+
+# Hibernate ?? ??
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
+
+# ?? ??
+logging.level.org.hibernate.SQL=DEBUG
+logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
+
+# JWT ??
+jwt.secret=WRmYXNkZmFzZGZhc2RmYXNkZmFzZGZhc2Rm12345
+jwt.expiration=86400000
+jwt.refresh-expiration=2592000000
\ No newline at end of file

From 7f506de8d8903a74807e521188083b71d8a4f359 Mon Sep 17 00:00:00 2001
From: YunHwa0905 
Date: Mon, 28 Apr 2025 10:00:56 +0900
Subject: [PATCH 135/361] Feat:login&logout

---
 .../userservice/config/ApplicationConfig.java |  50 +++++++++
 .../userservice/config/SecurityConfig.java    |  39 +++++++
 .../controller/AuthController.java            |  48 +++++++++
 .../com/example/userservice/dto/AuthDto.java  |  54 ++++++++++
 .../example/userservice/dto/MemberDto.java    |  44 ++++++++
 .../example/userservice/entity/Member.java    |  34 ++++++
 .../userservice/entity/RefreshToken.java      |  20 ++++
 .../repository/MemberRepository.java          |  12 +++
 .../repository/RefreshTokenRepository.java    |  13 +++
 .../security/JwtAuthenticationFilter.java     |  79 ++++++++++++++
 .../example/userservice/security/JwtUtil.java | 100 ++++++++++++++++++
 .../userservice/service/AuthService.java      |  69 ++++++++++++
 .../userservice/service/CookieUtil.java       |  21 ++++
 .../userservice/service/TokenService.java     |  58 ++++++++++
 .../src/main/resources/application.properties |  25 +++++
 15 files changed, 666 insertions(+)
 create mode 100644 user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
 create mode 100644 user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
 create mode 100644 user-service/src/main/java/com/example/userservice/controller/AuthController.java
 create mode 100644 user-service/src/main/java/com/example/userservice/dto/AuthDto.java
 create mode 100644 user-service/src/main/java/com/example/userservice/dto/MemberDto.java
 create mode 100644 user-service/src/main/java/com/example/userservice/entity/Member.java
 create mode 100644 user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
 create mode 100644 user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
 create mode 100644 user-service/src/main/java/com/example/userservice/repository/RefreshTokenRepository.java
 create mode 100644 user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
 create mode 100644 user-service/src/main/java/com/example/userservice/security/JwtUtil.java
 create mode 100644 user-service/src/main/java/com/example/userservice/service/AuthService.java
 create mode 100644 user-service/src/main/java/com/example/userservice/service/CookieUtil.java
 create mode 100644 user-service/src/main/java/com/example/userservice/service/TokenService.java

diff --git a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
new file mode 100644
index 0000000..68fa8dd
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
@@ -0,0 +1,50 @@
+package com.example.userservice.config;
+
+import com.example.userservice.repository.MemberRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@Configuration
+@RequiredArgsConstructor
+public class ApplicationConfig {
+
+    private final MemberRepository memberRepository;
+
+    @Bean
+    public UserDetailsService userDetailsService() {
+        return username -> memberRepository.findByMemberId(username)
+                .map(member -> new org.springframework.security.core.userdetails.User(
+                        member.getMemberId(),
+                        member.getPassword(),
+                        java.util.Collections.emptyList()
+                ))
+                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
+    }
+
+    @Bean
+    public AuthenticationProvider authenticationProvider() {
+        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
+        authProvider.setUserDetailsService(userDetailsService());
+        authProvider.setPasswordEncoder(passwordEncoder());
+        return authProvider;
+    }
+
+    @Bean
+    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
+        return config.getAuthenticationManager();
+    }
+
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
new file mode 100644
index 0000000..3b7ad88
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
@@ -0,0 +1,39 @@
+package com.example.userservice.config;
+
+import com.example.userservice.security.JwtAuthenticationFilter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+    private final JwtAuthenticationFilter jwtAuthenticationFilter;
+    private final AuthenticationProvider authenticationProvider;
+
+    @Bean
+    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+        http
+                .csrf(csrf -> csrf.disable())
+                .authorizeHttpRequests(auth -> auth
+                        .requestMatchers("/auth/**").permitAll()
+                        .requestMatchers("/admin/**").authenticated()
+                        .anyRequest().authenticated()
+                )
+                .sessionManagement(session -> session
+                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+                )
+                .authenticationProvider(authenticationProvider)
+                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
+
+        return http.build();
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/controller/AuthController.java b/user-service/src/main/java/com/example/userservice/controller/AuthController.java
new file mode 100644
index 0000000..6183070
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/controller/AuthController.java
@@ -0,0 +1,48 @@
+package com.example.userservice.controller;
+
+import com.example.userservice.dto.AuthDto;
+import com.example.userservice.service.AuthService;
+import com.example.userservice.service.CookieUtil;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/auth")
+@RequiredArgsConstructor
+public class AuthController {
+
+    private final AuthService authService;
+    private final CookieUtil cookieUtil;
+
+    @PostMapping("/login")
+    public ResponseEntity login(
+            @RequestBody AuthDto.LoginRequest request,
+            HttpServletResponse response) {
+
+        AuthDto.LoginResponse loginResponse = authService.login(request);
+
+        // 쿠키에 토큰 저장
+        cookieUtil.addCookie(response, "access_token", loginResponse.getAccessToken(), cookieUtil.getDaysInSeconds(1));
+        cookieUtil.addCookie(response, "refresh_token", loginResponse.getRefreshToken(), cookieUtil.getDaysInSeconds(30));
+
+        return ResponseEntity.ok(loginResponse);
+    }
+
+    @PostMapping("/logout")
+    public ResponseEntity logout(
+            @RequestBody AuthDto.LogoutRequest request,
+            HttpServletResponse response) {
+
+        // 쿠키 삭제
+        cookieUtil.addCookie(response, "access_token", "", 0);
+        cookieUtil.addCookie(response, "refresh_token", "", 0);
+
+        // authService의 logout 메서드 호출
+        return ResponseEntity.ok(authService.logout(request));
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/AuthDto.java b/user-service/src/main/java/com/example/userservice/dto/AuthDto.java
new file mode 100644
index 0000000..a2511cf
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/AuthDto.java
@@ -0,0 +1,54 @@
+package com.example.userservice.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+public class AuthDto {
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LoginRequest {
+        private String user_id;
+        private String password;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LoginResponse {
+        private String accessToken;
+        private String refreshToken;  // 리프레시 토큰 필드 추가
+        private String user_id;
+        private String nickname;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LogoutRequest {
+        private String accessToken;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LogoutResponse {
+        private boolean success;
+        private String message;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class RefreshTokenRequest {
+        private String refreshToken;
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java
new file mode 100644
index 0000000..3648c46
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java
@@ -0,0 +1,44 @@
+package com.example.userservice.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+public class MemberDto {
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class MemberResponse {
+        private String user_id;
+        private String nickname;
+        private Short birth_year;
+        private LocalDateTime created_at;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class UpdateRequest {
+        private String nickname;
+        private Short birth_year;
+        private String current_password;
+        private String new_password;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class RegisterRequest {
+        private String user_id;
+        private String password;
+        private String nickname;
+        private Short birth_year;
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/Member.java b/user-service/src/main/java/com/example/userservice/entity/Member.java
new file mode 100644
index 0000000..d0817a0
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/Member.java
@@ -0,0 +1,34 @@
+package com.example.userservice.entity;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "member")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Member {
+
+    @Id
+    @Column(name = "member_id")
+    private String memberId;
+
+    @Column(nullable = false)
+    private String password;
+
+    @Column(name = "nickname")
+    private String nickname;
+
+    @Column(name = "birth_year")
+    private Short birthYear;
+
+    @Column(name = "created_at")
+    private LocalDateTime createdAt;
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
new file mode 100644
index 0000000..9b0d8ee
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
@@ -0,0 +1,20 @@
+package com.example.userservice.entity;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "refresh_token")
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class RefreshToken {
+    @Id
+    @Column(name = "user_id")
+    private Long userId;
+
+    @Column(name = "refresh_token", length = 512)
+    private String refreshToken;
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java b/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
new file mode 100644
index 0000000..631119c
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
@@ -0,0 +1,12 @@
+package com.example.userservice.repository;
+
+import com.example.userservice.entity.Member;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface MemberRepository extends JpaRepository {
+    Optional findByMemberId(String memberId);
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/repository/RefreshTokenRepository.java b/user-service/src/main/java/com/example/userservice/repository/RefreshTokenRepository.java
new file mode 100644
index 0000000..65adb8a
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/RefreshTokenRepository.java
@@ -0,0 +1,13 @@
+package com.example.userservice.repository;
+
+import com.example.userservice.entity.RefreshToken;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface RefreshTokenRepository extends JpaRepository {
+    Optional findByUserId(Long userId);
+    Optional findByRefreshToken(String refreshToken);
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..e265a4a
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
@@ -0,0 +1,79 @@
+package com.example.userservice.security;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+    private final JwtUtil jwtUtil;
+    private final UserDetailsService userDetailsService;
+
+    @Override
+    protected void doFilterInternal(
+            HttpServletRequest request,
+            HttpServletResponse response,
+            FilterChain filterChain
+    ) throws ServletException, IOException {
+
+        // Authorization 헤더에서 토큰 추출 시도
+        final String authHeader = request.getHeader("Authorization");
+        String jwt = null;
+
+        // Authorization 헤더가 있으면 해당 토큰 사용
+        if (authHeader != null && authHeader.startsWith("Bearer ")) {
+            jwt = authHeader.substring(7);
+        }
+        // 아니면 쿠키에서 토큰 찾기
+        else {
+            Cookie[] cookies = request.getCookies();
+            if (cookies != null) {
+                for (Cookie cookie : cookies) {
+                    if ("access_token".equals(cookie.getName())) {
+                        jwt = cookie.getValue();
+                        break;
+                    }
+                }
+            }
+        }
+
+        // 토큰이 없으면 다음 필터로 이동
+        if (jwt == null) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        final String userEmail = jwtUtil.extractUsername(jwt);
+
+        if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+            UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
+
+            if (jwtUtil.validateToken(jwt, userDetails)) {
+                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
+                        userDetails,
+                        null,
+                        userDetails.getAuthorities()
+                );
+                authToken.setDetails(
+                        new WebAuthenticationDetailsSource().buildDetails(request)
+                );
+                SecurityContextHolder.getContext().setAuthentication(authToken);
+            }
+        }
+        filterChain.doFilter(request, response);
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/security/JwtUtil.java b/user-service/src/main/java/com/example/userservice/security/JwtUtil.java
new file mode 100644
index 0000000..26e4b05
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/security/JwtUtil.java
@@ -0,0 +1,100 @@
+package com.example.userservice.security;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+@Component
+public class JwtUtil {
+
+    @Value("${jwt.secret}")
+    private String secretKey;
+
+    @Value("${jwt.expiration}")
+    private Long expiration;
+
+    @Value("${jwt.refresh-expiration}")
+    private Long refreshExpiration;
+
+    public String extractUsername(String token) {
+        return extractClaim(token, Claims::getSubject);
+    }
+
+    public Date extractExpiration(String token) {
+        return extractClaim(token, Claims::getExpiration);
+    }
+
+    public  T extractClaim(String token, Function claimsResolver) {
+        final Claims claims = extractAllClaims(token);
+        return claimsResolver.apply(claims);
+    }
+
+    private Claims extractAllClaims(String token) {
+        return Jwts
+                .parser()
+                .verifyWith(getSignInKey())
+                .build()
+                .parseSignedClaims(token)
+                .getPayload();
+    }
+
+    private Boolean isTokenExpired(String token) {
+        return extractExpiration(token).before(new Date());
+    }
+
+    // 일반 액세스 토큰 생성
+    public String generateToken(UserDetails userDetails) {
+        Map claims = new HashMap<>();
+        return createToken(claims, userDetails.getUsername(), expiration);
+    }
+
+    // 리프레시 토큰 생성
+    public String generateRefreshToken(UserDetails userDetails) {
+        Map claims = new HashMap<>();
+        return createToken(claims, userDetails.getUsername(), refreshExpiration);
+    }
+
+    // 토큰 생성 메소드 (유효기간 파라미터 추가)
+    private String createToken(Map claims, String subject, Long expirationTime) {
+        return Jwts.builder()
+                .claims(claims)
+                .subject(subject)
+                .issuedAt(new Date(System.currentTimeMillis()))
+                .expiration(new Date(System.currentTimeMillis() + expirationTime))
+                .signWith(getSignInKey())
+                .compact();
+    }
+
+    // 토큰 유효성 검증
+    public Boolean validateToken(String token, UserDetails userDetails) {
+        final String username = extractUsername(token);
+        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
+    }
+
+    // 토큰 유효성만 검사 (사용자 정보 없이)
+    public Boolean validateToken(String token) {
+        try {
+            Jwts.parser()
+                    .verifyWith(getSignInKey())
+                    .build()
+                    .parseSignedClaims(token);
+            return !isTokenExpired(token);
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    private SecretKey getSignInKey() {
+        byte[] keyBytes = secretKey.getBytes();
+        return Keys.hmacShaKeyFor(keyBytes);
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/AuthService.java b/user-service/src/main/java/com/example/userservice/service/AuthService.java
new file mode 100644
index 0000000..893d4fc
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/AuthService.java
@@ -0,0 +1,69 @@
+package com.example.userservice.service;
+
+import com.example.userservice.dto.AuthDto;
+import com.example.userservice.entity.Member;
+import com.example.userservice.repository.MemberRepository;
+import com.example.userservice.security.JwtUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class AuthService {
+
+    private final MemberRepository memberRepository;
+    private final JwtUtil jwtUtil;
+    private final AuthenticationManager authenticationManager;
+    private final TokenService tokenService;
+
+    public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
+        // 인증 시도
+        authenticationManager.authenticate(
+                new UsernamePasswordAuthenticationToken(
+                        request.getUser_id(),
+                        request.getPassword()
+                )
+        );
+
+        // 사용자 조회
+        Optional memberOptional = memberRepository.findByMemberId(request.getUser_id());
+        if (memberOptional.isEmpty()) {
+            throw new RuntimeException("User not found");
+        }
+
+        Member member = memberOptional.get();
+
+        // JWT 토큰 생성
+        UserDetails userDetails = new User(member.getMemberId(), member.getPassword(), Collections.emptyList());
+        String accessToken = jwtUtil.generateToken(userDetails);
+        String refreshToken = jwtUtil.generateRefreshToken(userDetails);
+
+        // 리프레시 토큰 저장
+        tokenService.saveRefreshToken(member.getMemberId(), refreshToken);
+
+        return AuthDto.LoginResponse.builder()
+                .accessToken(accessToken)
+                .refreshToken(refreshToken)
+                .user_id(member.getMemberId())
+                .nickname(member.getNickname())
+                .build();
+    }
+
+    // LogoutRequest 파라미터를 받는 메서드로 수정
+    public AuthDto.LogoutResponse logout(AuthDto.LogoutRequest request) {
+        // JWT는 stateless이므로 서버 측에서는 특별히 처리할 작업이 없음
+        // 클라이언트 측에서 토큰을 제거하도록 안내
+
+        return AuthDto.LogoutResponse.builder()
+                .success(true)
+                .message("Logout successful")
+                .build();
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/CookieUtil.java b/user-service/src/main/java/com/example/userservice/service/CookieUtil.java
new file mode 100644
index 0000000..2a1ab1f
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/CookieUtil.java
@@ -0,0 +1,21 @@
+package com.example.userservice.service;
+
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Service;
+
+@Service
+public class CookieUtil {
+
+    public void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
+        Cookie cookie = new Cookie(name, value);
+        cookie.setPath("/");
+        cookie.setHttpOnly(true);
+        cookie.setMaxAge(maxAge);
+        response.addCookie(cookie);
+    }
+
+    public int getDaysInSeconds(int days) {
+        return days * 24 * 60 * 60;
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/TokenService.java b/user-service/src/main/java/com/example/userservice/service/TokenService.java
new file mode 100644
index 0000000..931ab2e
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/TokenService.java
@@ -0,0 +1,58 @@
+package com.example.userservice.service;
+
+import com.example.userservice.entity.Member;
+import com.example.userservice.entity.RefreshToken;
+import com.example.userservice.repository.MemberRepository;
+import com.example.userservice.repository.RefreshTokenRepository;
+import com.example.userservice.security.JwtUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.UUID;
+
+@Service
+@RequiredArgsConstructor
+public class TokenService {
+
+    private final JwtUtil jwtUtil;
+    private final MemberRepository memberRepository;
+    private final RefreshTokenRepository refreshTokenRepository;
+
+    public String createNewAccessToken(String refreshToken) {
+        // 리프레시 토큰 유효성 검사
+        if (!jwtUtil.validateToken(refreshToken)) {
+            throw new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다.");
+        }
+
+        // 데이터베이스에서 리프레시 토큰 확인
+        Optional storedToken = refreshTokenRepository.findByRefreshToken(refreshToken);
+        if (storedToken.isEmpty()) {
+            throw new IllegalArgumentException("존재하지 않는 리프레시 토큰입니다.");
+        }
+
+        // 사용자 ID를 문자열로 변환
+        Long userId = storedToken.get().getUserId();
+        String userIdStr = String.valueOf(userId);
+
+        // 사용자 조회
+        Member member = memberRepository.findById(userIdStr)
+                .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
+
+        // 새 액세스 토큰 생성
+        UserDetails userDetails = new User(member.getMemberId(), member.getPassword(), Collections.emptyList());
+        return jwtUtil.generateToken(userDetails);
+    }
+
+    public void saveRefreshToken(String memberId, String refreshToken) {
+        // String memberId를 고유 숫자 ID로 변환
+        // 여기서는 memberId 문자열 자체 대신 해시값을 사용하여 숫자로 변환
+        long userId = Math.abs(UUID.nameUUIDFromBytes(memberId.getBytes()).getMostSignificantBits());
+
+        RefreshToken token = new RefreshToken(userId, refreshToken);
+        refreshTokenRepository.save(token);
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties
index 79b5bda..64e20b9 100644
--- a/user-service/src/main/resources/application.properties
+++ b/user-service/src/main/resources/application.properties
@@ -1 +1,26 @@
 spring.application.name=user-service
+
+# ?? ??
+server.port=8082
+
+spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db
+spring.datasource.username=root
+spring.datasource.password=admin
+spring.datasource.driver-class-name=org.postgresql.Driver
+
+# JPA ??
+spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
+spring.jpa.hibernate.ddl-auto=update
+spring.jpa.show-sql=true
+
+# Hibernate ?? ??
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
+
+# ?? ??
+logging.level.org.hibernate.SQL=DEBUG
+logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
+
+# JWT ??
+jwt.secret=WRmYXNkZmFzZGZhc2RmYXNkZmFzZGZhc2Rm12345
+jwt.expiration=86400000
+jwt.refresh-expiration=2592000000
\ No newline at end of file

From 43c441b9a2eefbb7139b47211c91788bc04d0cff Mon Sep 17 00:00:00 2001
From: ha-yun 
Date: Mon, 28 Apr 2025 10:59:45 +0900
Subject: [PATCH 136/361] =?UTF-8?q?Feat:=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?=
 =?UTF-8?q?=ED=98=BC=EC=9E=A1=EB=8F=84=20API=20&=20=ED=98=BC=EC=9E=A1?=
 =?UTF-8?q?=EB=8F=84=20=EC=95=8C=EB=A6=BC=20API=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 congestion-service/build.gradle               |  2 -
 .../CongestionServiceApplication.java         |  2 +
 .../congestionservice/config/AppConfig.java   | 14 +++
 .../config/SecurityConfig.java                | 23 +++++
 .../controller/CongestionController.java      | 62 +++++++++++++
 .../controller/CongestionService.java         |  4 -
 .../scheduler/PushScheduler.java              | 61 +++++++++++--
 .../service/CongestionService.java            | 86 +++++++++++++++++++
 .../service/ESRoadService.java                |  3 +-
 .../service/ParkEsService.java                |  4 +-
 .../service/WeatherEsService.java             |  4 +-
 11 files changed, 247 insertions(+), 18 deletions(-)
 create mode 100644 congestion-service/src/main/java/com/example/congestionservice/config/AppConfig.java
 create mode 100644 congestion-service/src/main/java/com/example/congestionservice/config/SecurityConfig.java
 create mode 100644 congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java
 delete mode 100644 congestion-service/src/main/java/com/example/congestionservice/controller/CongestionService.java

diff --git a/congestion-service/build.gradle b/congestion-service/build.gradle
index 61be6f8..9367d46 100644
--- a/congestion-service/build.gradle
+++ b/congestion-service/build.gradle
@@ -24,8 +24,6 @@ repositories {
 }
 
 dependencies {
-    // 비동기 처리를 위한 webClient 추가
-    implementation 'org.springframework.boot:spring-boot-starter-webflux'
 
     // JAXB (XML 파싱)
     implementation 'javax.xml.bind:jaxb-api:2.3.1'
diff --git a/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java b/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java
index 459ab5a..0525823 100644
--- a/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java
+++ b/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java
@@ -2,8 +2,10 @@
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
 
 @SpringBootApplication
+@EnableScheduling
 public class CongestionServiceApplication {
 
     public static void main(String[] args) {
diff --git a/congestion-service/src/main/java/com/example/congestionservice/config/AppConfig.java b/congestion-service/src/main/java/com/example/congestionservice/config/AppConfig.java
new file mode 100644
index 0000000..771be0d
--- /dev/null
+++ b/congestion-service/src/main/java/com/example/congestionservice/config/AppConfig.java
@@ -0,0 +1,14 @@
+package com.example.congestionservice.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class AppConfig {
+    @Bean
+    public RestTemplate restTemplate() {
+        return new RestTemplate();
+    }
+
+}
diff --git a/congestion-service/src/main/java/com/example/congestionservice/config/SecurityConfig.java b/congestion-service/src/main/java/com/example/congestionservice/config/SecurityConfig.java
new file mode 100644
index 0000000..9213994
--- /dev/null
+++ b/congestion-service/src/main/java/com/example/congestionservice/config/SecurityConfig.java
@@ -0,0 +1,23 @@
+package com.example.congestionservice.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+public class SecurityConfig {
+
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        http
+                .csrf(csrf -> csrf.disable())  // CSRF 비활성화
+                .authorizeHttpRequests(auth -> auth
+                        .anyRequest().permitAll()   // 모든 요청 허용
+                );
+
+        return http.build();
+    }
+
+
+}
diff --git a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java
new file mode 100644
index 0000000..30b205d
--- /dev/null
+++ b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java
@@ -0,0 +1,62 @@
+package com.example.congestionservice.controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/main")
+public class CongestionController {
+    private final List emitters = new CopyOnWriteArrayList<>();
+
+    @GetMapping("/congestion")
+    public SseEmitter streamCongestion() {
+        SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음
+        emitters.add(emitter);
+
+        emitter.onCompletion(() -> emitters.remove(emitter));
+        emitter.onTimeout(() -> emitters.remove(emitter));
+        emitter.onError((e) -> emitters.remove(emitter));
+
+        return emitter;
+
+    }
+
+    // 주기적으로 클라이언트에게 push
+    public void sendToClients(JsonNode congestionList) {
+        for (SseEmitter emitter : emitters) {
+            try {
+                emitter.send(SseEmitter.event()
+                        .name("congestion-update")
+                        .data(congestionList));
+
+            } catch (IOException e) {
+                emitter.completeWithError(e);
+                emitters.remove(emitter);
+            }
+        }
+    }
+
+    // 주기적으로 클라이언트에게 push
+    public void sendAlertToClients(JsonNode changedList) {
+        for (SseEmitter emitter : emitters) {
+            try {
+                emitter.send(SseEmitter.event()
+                        .name("congestion-alert")
+                        .data(changedList));
+
+            } catch (IOException e) {
+                emitter.completeWithError(e);
+                emitters.remove(emitter);
+            }
+        }
+    }
+}
diff --git a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionService.java b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionService.java
deleted file mode 100644
index 4c92846..0000000
--- a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionService.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.example.congestionservice.controller;
-
-public class CongestionService {
-}
diff --git a/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java b/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java
index 2b152eb..2c59ba7 100644
--- a/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java
+++ b/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java
@@ -1,19 +1,68 @@
 package com.example.congestionservice.scheduler;
 
+import com.example.congestionservice.controller.CongestionController;
+import com.example.congestionservice.service.CongestionService;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
 import lombok.RequiredArgsConstructor;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
+import java.util.HashMap;
+import java.util.Map;
+
 @Component
 @RequiredArgsConstructor
 public class PushScheduler {
+    private final CongestionService congestionService;
+    private final CongestionController congestionController;
 
 
-    // 5분마다 날씨 데이터를 SSE 클라이언트에 push
-    @Scheduled(fixedRate = 300_000) // 5분 = 300초
-    public void pushWeatherToClients() {
-        var trafficList = roadService.getTrafficData();
-        streamController.sendToClients(trafficList);
-    }
+    // 이전 혼잡도 상태를 저장하는 필드 추가(혼잡도 알림용)
+    private Map previousLevels = new HashMap<>();
+
+
+    @Scheduled(fixedRate = 300_000)
+    public void push() {
+        System.out.println("혼잡도 푸시 중...");
+        var congestionList = congestionService.getCongestion();
+
+        congestionController.sendToClients(congestionList); // 모든 지역 혼잡도 전송
+
+        // 혼잡도 알림 전송 위한 로직
+        Map currentLevels = new HashMap<>();
+        ArrayNode changedList = new ObjectMapper().createArrayNode();
+        for (JsonNode area : congestionList) {
+            String areaName = area.get("area_nm").asText();
+            String currentLevel = area.get("area_congest_lvl").asText();
+            currentLevels.put(areaName, currentLevel);
 
+            String previousLevel = previousLevels.get(areaName);
+            System.out.println(areaName+"의"+previousLevel+":ㅣㅣ"+currentLevel);
+
+            // 혼잡도 알림 전송(조건 : 1,2단계 -> 3,4 단계 or 3단계 -> 4단계)
+            if (previousLevel == null && (currentLevel.equals("약간 붐빔") || currentLevel.equals("붐빔"))) {
+                changedList.add(area);
+            }
+            else if (previousLevel != null){
+                if ((previousLevel.equals("여유") || previousLevel.equals("보통")) && (currentLevel.equals("약간 붐빔") || currentLevel.equals("붐빔"))) {
+                    changedList.add(area);
+                }else if(previousLevel.equals("약간 붐빔") && currentLevel.equals("붐빔")){
+                    changedList.add(area);
+                }
+
+            }
+        }
+
+        // 변화 없으면 return
+        if (changedList.isEmpty()) {
+            System.out.println("혼잡도 변화 없음");
+        }else{
+            System.out.println("혼잡도 변화 있음 → alert-update로 SSE 전송");
+            congestionController.sendAlertToClients(changedList); // ✨ 바뀐 것만 보냄
+        }
+        previousLevels = currentLevels;
+
+    }
 }
diff --git a/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java b/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java
index 8ec7631..bdbac83 100644
--- a/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java
+++ b/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java
@@ -1,8 +1,94 @@
 package com.example.congestionservice.service;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import org.apache.kafka.common.errors.InvalidRequestException;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
 
 @Service
+@RequiredArgsConstructor
 public class CongestionService {
+    private static final RestTemplate restTemplate = new RestTemplate();
+
+    public static JsonNode getCongestion() {
+        try{
+            // 오늘 날짜 구하기
+            String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+            String apiUrl = String.format("http://elasticsearch.seoultravel.life/seoul_citydata_congestion_%s/_search", today);
+
+            // JSON Body 생성
+            String jsonBody = String.format("{\n" +
+                    "  \"size\": 0,\n" +
+                    "  \"aggs\": {\n" +
+                    "    \"by_area\": {\n" +
+                    "      \"terms\": {\n" +
+                    "        \"field\": \"congestion.area_nm\",\n" +
+                    "        \"size\": 1000  // area_nm 종류 수만큼 충분히\n" +
+                    "      },\n" +
+                    "      \"aggs\": {\n" +
+                    "        \"latest_hit\": {\n" +
+                    "          \"top_hits\": {\n" +
+                    "            \"size\": 1,\n" +
+                    "            \"sort\": [\n" +
+                    "              {\n" +
+                    "                \"congestion.ppltn_time\": {\n" +
+                    "                  \"order\": \"desc\"\n" +
+                    "                }\n" +
+                    "              }\n" +
+                    "            ]\n" +
+                    "          }\n" +
+                    "        }\n" +
+                    "      }\n" +
+                    "    }\n" +
+                    "  }\n" +
+                    "}\n");
+
+            // 헤더 설정
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            // 요청 엔티티 만들기
+            HttpEntity requestEntity = new HttpEntity<>(jsonBody, headers);
+
+            // POST 요청 보내기
+            ResponseEntity response = restTemplate.postForEntity(apiUrl, requestEntity, String.class);
+
+            String body = response.getBody();
+            // congestion 데이터만 가져오기
+            ObjectMapper mapper = new ObjectMapper();
+            JsonNode root = mapper.readTree(String.valueOf(body)); // response는 JSON 문자열
+
+            JsonNode buckets = root
+                    .path("aggregations")
+                    .path("by_area")
+                    .path("buckets");
+
+            List congestionList = new ArrayList<>();
+            for (JsonNode bucket : buckets) {
+                JsonNode latestHit = bucket.path("latest_hit").path("hits").path("hits").get(0); // 첫 번째 hit 선택
+                JsonNode congestion = latestHit.path("_source").path("congestion");
+                congestionList.add(congestion);
+            }
+            ObjectMapper objectMapper = new ObjectMapper();
+            JsonNode result = objectMapper.valueToTree(congestionList);
+
+            return result;
 
+        }catch (InvalidRequestException e) {
+            throw new InvalidRequestException(e.getMessage(), e);
+        }catch (Exception e){
+            System.out.println(e);
+            throw new RuntimeException("예상치 못한 오류",e);
+        }
+    }
 }
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java
index 1145c3a..d201a06 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java
@@ -8,7 +8,6 @@
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
-import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 import org.springframework.web.client.RestTemplate;
 
@@ -35,7 +34,7 @@ public static JsonNode getTrafficData() {
                     "  \"aggs\": {\n" +
                     "    \"by_area\": {\n" +
                     "      \"terms\": {\n" +
-                    "        \"field\": \"road_traffic.area_nm.keyword\",\n" +
+                    "        \"field\": \"road_traffic.area_nm\",\n" +
                     "        \"size\": 1000  // area_nm 종류 수만큼 충분히\n" +
                     "      },\n" +
                     "      \"aggs\": {\n" +
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java
index 16b1ee9..5336d7f 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java
@@ -20,7 +20,7 @@ public Map getParkFromES(String areaId) {
         String url = "http://elasticsearch.seoultravel.life/seoul_citydata_parking_20250424/_search";
         log.info("Requesting park info for areaId: {}", areaId);
 
-        Map term = Map.of("parking.area_nm.keyword", areaId);
+        Map term = Map.of("parking.area_nm", areaId);
         Map query = Map.of("term", term);
         Map body = Map.of("size", 100, "query", query);
 
@@ -59,7 +59,7 @@ public List> getAllParkFromES() {
                 "size", 0,
                 "aggs", Map.of(
                         "by_area", Map.of(
-                                "terms", Map.of("field", "parking.area_nm.keyword", "size", 1000),
+                                "terms", Map.of("field", "parking.area_nm", "size", 1000),
                                 "aggs", Map.of(
                                         "latest_hit", Map.of(
                                                 "top_hits", Map.of(
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java
index 5faa5c8..93316d7 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java
@@ -18,7 +18,7 @@ public class WeatherEsService {
     public Map getWeatherFromES(String area) {
         String url = "http://elasticsearch.seoultravel.life/seoul_citydata_weather_20250424/_search";
 
-        Map term = Map.of("weather.area_nm.keyword", area);
+        Map term = Map.of("weather.area_nm", area);
         Map query = Map.of("term", term);
         Map body = Map.of("size", 100, "query", query);
 
@@ -45,7 +45,7 @@ public List> getAllWeatherFromES() {
                 "size", 0,
                 "aggs", Map.of(
                         "by_area", Map.of(
-                                "terms", Map.of("field", "weather.area_nm.keyword", "size", 1000),
+                                "terms", Map.of("field", "weather.area_nm", "size", 1000),
                                 "aggs", Map.of(
                                         "latest_hit", Map.of(
                                                 "top_hits", Map.of(

From 8d9aff590e73264e0b7033aed9dc6d4bb9feb539 Mon Sep 17 00:00:00 2001
From: ha-yun 
Date: Mon, 28 Apr 2025 10:59:45 +0900
Subject: [PATCH 137/361] =?UTF-8?q?Feat:=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?=
 =?UTF-8?q?=ED=98=BC=EC=9E=A1=EB=8F=84=20API=20&=20=ED=98=BC=EC=9E=A1?=
 =?UTF-8?q?=EB=8F=84=20=EC=95=8C=EB=A6=BC=20API=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 congestion-service/build.gradle               |  2 -
 .../CongestionServiceApplication.java         |  2 +
 .../congestionservice/config/AppConfig.java   | 14 +++
 .../config/SecurityConfig.java                | 23 +++++
 .../controller/CongestionController.java      | 62 +++++++++++++
 .../controller/CongestionService.java         |  4 -
 .../scheduler/PushScheduler.java              | 61 +++++++++++--
 .../service/CongestionService.java            | 86 +++++++++++++++++++
 .../service/ESRoadService.java                |  3 +-
 .../service/ParkEsService.java                |  4 +-
 .../service/WeatherEsService.java             |  4 +-
 11 files changed, 247 insertions(+), 18 deletions(-)
 create mode 100644 congestion-service/src/main/java/com/example/congestionservice/config/AppConfig.java
 create mode 100644 congestion-service/src/main/java/com/example/congestionservice/config/SecurityConfig.java
 create mode 100644 congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java
 delete mode 100644 congestion-service/src/main/java/com/example/congestionservice/controller/CongestionService.java

diff --git a/congestion-service/build.gradle b/congestion-service/build.gradle
index 61be6f8..9367d46 100644
--- a/congestion-service/build.gradle
+++ b/congestion-service/build.gradle
@@ -24,8 +24,6 @@ repositories {
 }
 
 dependencies {
-    // 비동기 처리를 위한 webClient 추가
-    implementation 'org.springframework.boot:spring-boot-starter-webflux'
 
     // JAXB (XML 파싱)
     implementation 'javax.xml.bind:jaxb-api:2.3.1'
diff --git a/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java b/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java
index 459ab5a..0525823 100644
--- a/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java
+++ b/congestion-service/src/main/java/com/example/congestionservice/CongestionServiceApplication.java
@@ -2,8 +2,10 @@
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
 
 @SpringBootApplication
+@EnableScheduling
 public class CongestionServiceApplication {
 
     public static void main(String[] args) {
diff --git a/congestion-service/src/main/java/com/example/congestionservice/config/AppConfig.java b/congestion-service/src/main/java/com/example/congestionservice/config/AppConfig.java
new file mode 100644
index 0000000..771be0d
--- /dev/null
+++ b/congestion-service/src/main/java/com/example/congestionservice/config/AppConfig.java
@@ -0,0 +1,14 @@
+package com.example.congestionservice.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class AppConfig {
+    @Bean
+    public RestTemplate restTemplate() {
+        return new RestTemplate();
+    }
+
+}
diff --git a/congestion-service/src/main/java/com/example/congestionservice/config/SecurityConfig.java b/congestion-service/src/main/java/com/example/congestionservice/config/SecurityConfig.java
new file mode 100644
index 0000000..9213994
--- /dev/null
+++ b/congestion-service/src/main/java/com/example/congestionservice/config/SecurityConfig.java
@@ -0,0 +1,23 @@
+package com.example.congestionservice.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+public class SecurityConfig {
+
+    @Bean
+    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+        http
+                .csrf(csrf -> csrf.disable())  // CSRF 비활성화
+                .authorizeHttpRequests(auth -> auth
+                        .anyRequest().permitAll()   // 모든 요청 허용
+                );
+
+        return http.build();
+    }
+
+
+}
diff --git a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java
new file mode 100644
index 0000000..30b205d
--- /dev/null
+++ b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java
@@ -0,0 +1,62 @@
+package com.example.congestionservice.controller;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/main")
+public class CongestionController {
+    private final List emitters = new CopyOnWriteArrayList<>();
+
+    @GetMapping("/congestion")
+    public SseEmitter streamCongestion() {
+        SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음
+        emitters.add(emitter);
+
+        emitter.onCompletion(() -> emitters.remove(emitter));
+        emitter.onTimeout(() -> emitters.remove(emitter));
+        emitter.onError((e) -> emitters.remove(emitter));
+
+        return emitter;
+
+    }
+
+    // 주기적으로 클라이언트에게 push
+    public void sendToClients(JsonNode congestionList) {
+        for (SseEmitter emitter : emitters) {
+            try {
+                emitter.send(SseEmitter.event()
+                        .name("congestion-update")
+                        .data(congestionList));
+
+            } catch (IOException e) {
+                emitter.completeWithError(e);
+                emitters.remove(emitter);
+            }
+        }
+    }
+
+    // 주기적으로 클라이언트에게 push
+    public void sendAlertToClients(JsonNode changedList) {
+        for (SseEmitter emitter : emitters) {
+            try {
+                emitter.send(SseEmitter.event()
+                        .name("congestion-alert")
+                        .data(changedList));
+
+            } catch (IOException e) {
+                emitter.completeWithError(e);
+                emitters.remove(emitter);
+            }
+        }
+    }
+}
diff --git a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionService.java b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionService.java
deleted file mode 100644
index 4c92846..0000000
--- a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionService.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.example.congestionservice.controller;
-
-public class CongestionService {
-}
diff --git a/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java b/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java
index 2b152eb..2c59ba7 100644
--- a/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java
+++ b/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java
@@ -1,19 +1,68 @@
 package com.example.congestionservice.scheduler;
 
+import com.example.congestionservice.controller.CongestionController;
+import com.example.congestionservice.service.CongestionService;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
 import lombok.RequiredArgsConstructor;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
+import java.util.HashMap;
+import java.util.Map;
+
 @Component
 @RequiredArgsConstructor
 public class PushScheduler {
+    private final CongestionService congestionService;
+    private final CongestionController congestionController;
 
 
-    // 5분마다 날씨 데이터를 SSE 클라이언트에 push
-    @Scheduled(fixedRate = 300_000) // 5분 = 300초
-    public void pushWeatherToClients() {
-        var trafficList = roadService.getTrafficData();
-        streamController.sendToClients(trafficList);
-    }
+    // 이전 혼잡도 상태를 저장하는 필드 추가(혼잡도 알림용)
+    private Map previousLevels = new HashMap<>();
+
+
+    @Scheduled(fixedRate = 300_000)
+    public void push() {
+        System.out.println("혼잡도 푸시 중...");
+        var congestionList = congestionService.getCongestion();
+
+        congestionController.sendToClients(congestionList); // 모든 지역 혼잡도 전송
+
+        // 혼잡도 알림 전송 위한 로직
+        Map currentLevels = new HashMap<>();
+        ArrayNode changedList = new ObjectMapper().createArrayNode();
+        for (JsonNode area : congestionList) {
+            String areaName = area.get("area_nm").asText();
+            String currentLevel = area.get("area_congest_lvl").asText();
+            currentLevels.put(areaName, currentLevel);
 
+            String previousLevel = previousLevels.get(areaName);
+            System.out.println(areaName+"의"+previousLevel+":ㅣㅣ"+currentLevel);
+
+            // 혼잡도 알림 전송(조건 : 1,2단계 -> 3,4 단계 or 3단계 -> 4단계)
+            if (previousLevel == null && (currentLevel.equals("약간 붐빔") || currentLevel.equals("붐빔"))) {
+                changedList.add(area);
+            }
+            else if (previousLevel != null){
+                if ((previousLevel.equals("여유") || previousLevel.equals("보통")) && (currentLevel.equals("약간 붐빔") || currentLevel.equals("붐빔"))) {
+                    changedList.add(area);
+                }else if(previousLevel.equals("약간 붐빔") && currentLevel.equals("붐빔")){
+                    changedList.add(area);
+                }
+
+            }
+        }
+
+        // 변화 없으면 return
+        if (changedList.isEmpty()) {
+            System.out.println("혼잡도 변화 없음");
+        }else{
+            System.out.println("혼잡도 변화 있음 → alert-update로 SSE 전송");
+            congestionController.sendAlertToClients(changedList); // ✨ 바뀐 것만 보냄
+        }
+        previousLevels = currentLevels;
+
+    }
 }
diff --git a/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java b/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java
index 8ec7631..bdbac83 100644
--- a/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java
+++ b/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java
@@ -1,8 +1,94 @@
 package com.example.congestionservice.service;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import org.apache.kafka.common.errors.InvalidRequestException;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
 
 @Service
+@RequiredArgsConstructor
 public class CongestionService {
+    private static final RestTemplate restTemplate = new RestTemplate();
+
+    public static JsonNode getCongestion() {
+        try{
+            // 오늘 날짜 구하기
+            String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+            String apiUrl = String.format("http://elasticsearch.seoultravel.life/seoul_citydata_congestion_%s/_search", today);
+
+            // JSON Body 생성
+            String jsonBody = String.format("{\n" +
+                    "  \"size\": 0,\n" +
+                    "  \"aggs\": {\n" +
+                    "    \"by_area\": {\n" +
+                    "      \"terms\": {\n" +
+                    "        \"field\": \"congestion.area_nm\",\n" +
+                    "        \"size\": 1000  // area_nm 종류 수만큼 충분히\n" +
+                    "      },\n" +
+                    "      \"aggs\": {\n" +
+                    "        \"latest_hit\": {\n" +
+                    "          \"top_hits\": {\n" +
+                    "            \"size\": 1,\n" +
+                    "            \"sort\": [\n" +
+                    "              {\n" +
+                    "                \"congestion.ppltn_time\": {\n" +
+                    "                  \"order\": \"desc\"\n" +
+                    "                }\n" +
+                    "              }\n" +
+                    "            ]\n" +
+                    "          }\n" +
+                    "        }\n" +
+                    "      }\n" +
+                    "    }\n" +
+                    "  }\n" +
+                    "}\n");
+
+            // 헤더 설정
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            // 요청 엔티티 만들기
+            HttpEntity requestEntity = new HttpEntity<>(jsonBody, headers);
+
+            // POST 요청 보내기
+            ResponseEntity response = restTemplate.postForEntity(apiUrl, requestEntity, String.class);
+
+            String body = response.getBody();
+            // congestion 데이터만 가져오기
+            ObjectMapper mapper = new ObjectMapper();
+            JsonNode root = mapper.readTree(String.valueOf(body)); // response는 JSON 문자열
+
+            JsonNode buckets = root
+                    .path("aggregations")
+                    .path("by_area")
+                    .path("buckets");
+
+            List congestionList = new ArrayList<>();
+            for (JsonNode bucket : buckets) {
+                JsonNode latestHit = bucket.path("latest_hit").path("hits").path("hits").get(0); // 첫 번째 hit 선택
+                JsonNode congestion = latestHit.path("_source").path("congestion");
+                congestionList.add(congestion);
+            }
+            ObjectMapper objectMapper = new ObjectMapper();
+            JsonNode result = objectMapper.valueToTree(congestionList);
+
+            return result;
 
+        }catch (InvalidRequestException e) {
+            throw new InvalidRequestException(e.getMessage(), e);
+        }catch (Exception e){
+            System.out.println(e);
+            throw new RuntimeException("예상치 못한 오류",e);
+        }
+    }
 }
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java
index 1145c3a..d201a06 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java
@@ -8,7 +8,6 @@
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
-import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 import org.springframework.web.client.RestTemplate;
 
@@ -35,7 +34,7 @@ public static JsonNode getTrafficData() {
                     "  \"aggs\": {\n" +
                     "    \"by_area\": {\n" +
                     "      \"terms\": {\n" +
-                    "        \"field\": \"road_traffic.area_nm.keyword\",\n" +
+                    "        \"field\": \"road_traffic.area_nm\",\n" +
                     "        \"size\": 1000  // area_nm 종류 수만큼 충분히\n" +
                     "      },\n" +
                     "      \"aggs\": {\n" +
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java
index 16b1ee9..5336d7f 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java
@@ -20,7 +20,7 @@ public Map getParkFromES(String areaId) {
         String url = "http://elasticsearch.seoultravel.life/seoul_citydata_parking_20250424/_search";
         log.info("Requesting park info for areaId: {}", areaId);
 
-        Map term = Map.of("parking.area_nm.keyword", areaId);
+        Map term = Map.of("parking.area_nm", areaId);
         Map query = Map.of("term", term);
         Map body = Map.of("size", 100, "query", query);
 
@@ -59,7 +59,7 @@ public List> getAllParkFromES() {
                 "size", 0,
                 "aggs", Map.of(
                         "by_area", Map.of(
-                                "terms", Map.of("field", "parking.area_nm.keyword", "size", 1000),
+                                "terms", Map.of("field", "parking.area_nm", "size", 1000),
                                 "aggs", Map.of(
                                         "latest_hit", Map.of(
                                                 "top_hits", Map.of(
diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java
index 5faa5c8..93316d7 100644
--- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java
+++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java
@@ -18,7 +18,7 @@ public class WeatherEsService {
     public Map getWeatherFromES(String area) {
         String url = "http://elasticsearch.seoultravel.life/seoul_citydata_weather_20250424/_search";
 
-        Map term = Map.of("weather.area_nm.keyword", area);
+        Map term = Map.of("weather.area_nm", area);
         Map query = Map.of("term", term);
         Map body = Map.of("size", 100, "query", query);
 
@@ -45,7 +45,7 @@ public List> getAllWeatherFromES() {
                 "size", 0,
                 "aggs", Map.of(
                         "by_area", Map.of(
-                                "terms", Map.of("field", "weather.area_nm.keyword", "size", 1000),
+                                "terms", Map.of("field", "weather.area_nm", "size", 1000),
                                 "aggs", Map.of(
                                         "latest_hit", Map.of(
                                                 "top_hits", Map.of(

From decfcd861b661173a74a8e654f0688848f58478f Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 13:16:55 +0900
Subject: [PATCH 138/361] =?UTF-8?q?kakao=20API=20=EC=88=99=EB=B0=95=20?=
 =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EC=99=84=EB=A3=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 place-service/build.gradle                    |  13 --
 .../placeservice/PlaceServiceApplication.java |  25 +--
 .../placeservice/config/DataInitializer.java  |  13 +-
 .../controller/AccommodationController.java   |  15 +-
 .../dto/accommodation/AccommodationDto.java   |  48 ++++-
 .../accommodation/AccommodationResponse.java  |  81 --------
 .../dto/accommodation/KakaoApiResponse.java   |  55 ++++++
 .../placeservice/entity/Accommodation.java    |  12 +-
 .../com/example/placeservice/entity/Area.java |  35 +---
 .../repository/AccommodationRepository.java   |   1 +
 .../repository/AreaRepository.java            |   7 +-
 .../service/AccommodationParseService.java    |  85 --------
 .../service/AccommodationSaveService.java     | 159 ---------------
 .../service/AccommodationService.java         | 182 +++++++++++++++++-
 .../service/RestaurantListService.java        |   2 +-
 .../service/RestaurantService.java            |   2 +-
 .../src/main/resources/application.properties |  12 +-
 place-service/src/main/resources/data.sql     |  14 +-
 18 files changed, 294 insertions(+), 467 deletions(-)
 delete mode 100644 place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationResponse.java
 create mode 100644 place-service/src/main/java/com/example/placeservice/dto/accommodation/KakaoApiResponse.java
 delete mode 100644 place-service/src/main/java/com/example/placeservice/service/AccommodationParseService.java
 delete mode 100644 place-service/src/main/java/com/example/placeservice/service/AccommodationSaveService.java

diff --git a/place-service/build.gradle b/place-service/build.gradle
index 4e68f5d..07bd32c 100644
--- a/place-service/build.gradle
+++ b/place-service/build.gradle
@@ -24,36 +24,24 @@ repositories {
 }
 
 dependencies {
-<<<<<<< HEAD
-=======
     // JAXB (XML 파싱)
     implementation 'javax.xml.bind:jaxb-api:2.3.1'
     // JAXB Runtime (XML 파싱을 위해 필요)
     implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3'
 
 
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
     // JWT 처리
     implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
     runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
     runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
 
-<<<<<<< HEAD
-
-=======
     implementation 'org.postgresql:postgresql:42.7.2'
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
     implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
     implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
     //implementation 'org.springframework.boot:spring-boot-starter-data-redis'
     implementation 'org.springframework.boot:spring-boot-starter-security'
     implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
     implementation 'org.springframework.boot:spring-boot-starter-web'
-<<<<<<< HEAD
-    implementation 'org.apache.kafka:kafka-streams'
-    implementation 'org.springframework.kafka:spring-kafka'
-    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
-=======
     //implementation 'io.github.cdimascio:dotenv-java:3.0.0' // 최신 버전
     implementation 'io.github.cdimascio:dotenv-java:2.2.4'
     implementation 'org.apache.kafka:kafka-streams'
@@ -62,7 +50,6 @@ dependencies {
     implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0'
     implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3'
 
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
     compileOnly 'org.projectlombok:lombok'
     developmentOnly 'org.springframework.boot:spring-boot-devtools'
     runtimeOnly 'org.postgresql:postgresql'
diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
index 14665a2..a53bd16 100644
--- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
+++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
@@ -1,21 +1,6 @@
 package com.example.placeservice;
 
-<<<<<<< HEAD
-<<<<<<< HEAD
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-@SpringBootApplication
-public class PlaceServiceApplication {
-
-    public static void main(String[] args) {
-        SpringApplication.run(PlaceServiceApplication.class, args);
-    }
-
-=======
-=======
 import com.example.placeservice.repository.CafeRepository;
->>>>>>> 7d9e41f58fcc018b3cd8b94bb5aa79ab788ea9b5
 import com.example.placeservice.repository.RestaurantRepository;
 import com.example.placeservice.repository.CafeRepository;
 import com.example.placeservice.service.RestaurantService;
@@ -60,13 +45,7 @@ public CommandLineRunner initData(RestaurantService restaurantService, Restauran
                 System.out.println("카페 데이터가 이미 존재합니다. 건너뜁니다.");
             }
 
-            
+
         };
     }
-<<<<<<< HEAD
-
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
-}
-=======
-}
->>>>>>> 7d9e41f58fcc018b3cd8b94bb5aa79ab788ea9b5
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java b/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java
index 7d3aa92..550051d 100644
--- a/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java
+++ b/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java
@@ -1,26 +1,19 @@
 package com.example.placeservice.config;
 
-import com.example.placeservice.dto.accommodation.AccommodationResponse;
-import com.example.placeservice.service.AccommodationParseService;
-import com.example.placeservice.service.AccommodationSaveService;
+import com.example.placeservice.service.AccommodationService;
 import lombok.RequiredArgsConstructor;
 import org.springframework.boot.CommandLineRunner;
 import org.springframework.stereotype.Component;
 
-import java.util.List;
-
 @Component
 @RequiredArgsConstructor
 public class DataInitializer implements CommandLineRunner {
-    private final AccommodationSaveService accommodationSaveService;
-    private final AccommodationParseService accommodationParseService;
+    private final AccommodationService accommodationService;
 
     @Override
     public void run(String... args) throws Exception {
         // API 호출 및 XML 파싱
-        List items = accommodationParseService.parseXmlData();
-        // 파싱된 데이터 저장
-        accommodationSaveService.saveAccommodation(items);
+        accommodationService.saveAccommodations();
 
         System.out.println("데이터 초기화가 완료되었습니다.");
     }
diff --git a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
index 3db30bb..82a562a 100644
--- a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
+++ b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
@@ -1,7 +1,6 @@
 package com.example.placeservice.controller;
 
-import com.example.placeservice.dto.accommodation.AccommodationResponse;
-import com.example.placeservice.service.AccommodationSaveService;
+import com.example.placeservice.dto.accommodation.AccommodationDto;
 import com.example.placeservice.service.AccommodationService;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -13,17 +12,15 @@
 @RestController
 @RequestMapping("/main/accommodation")
 public class AccommodationController {
-    private final AccommodationService accommodationService;
-    private final AccommodationSaveService accommodationSaveService;
+    private final AccommodationService accommodationParseService;
 
     // 생성자 주입
-    public AccommodationController(AccommodationService accommodationService, AccommodationSaveService accommodationSaveService) {
-        this.accommodationService = accommodationService;
-        this.accommodationSaveService = accommodationSaveService;
+    public AccommodationController(AccommodationService accommodationParseService) {
+        this.accommodationParseService = accommodationParseService;
     }
 
     @GetMapping("/list")
-    public List getAccommodations() throws IOException {
-        return accommodationService.getAllAccommodationData();
+    public List getAccommodations() throws IOException {
+        return accommodationParseService.showAccommodations();
     }
 }
diff --git a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java
index 698bea1..0c6291b 100644
--- a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java
+++ b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java
@@ -1,21 +1,59 @@
 package com.example.placeservice.dto.accommodation;
 
-import lombok.*;
+import com.example.placeservice.entity.Accommodation;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
 
 import java.math.BigDecimal;
 
 @Data
 @NoArgsConstructor
 @AllArgsConstructor
-@Builder
 public class AccommodationDto {
+
     private Long accommodationId;
     private Long areaId;
+    private String areaName;
     private String name;
     private String address;
     private BigDecimal lat;
     private BigDecimal lon;
     private String phone;
-    private String sigunguCode;
-    private String imageUrl;
-}
+    private String gu;
+    private String type;
+    private String kakaoUrl;
+
+    /**
+     * Accommodation 엔티티를 AccommodationDto로 변환하는 정적 팩토리 메소드
+     * @param entity 변환할 Accommodation 엔티티
+     * @return 변환된 AccommodationDto 객체
+     */
+    public static AccommodationDto fromEntity(Accommodation entity) {
+        if (entity == null) {
+            return null;
+        }
+
+        // Area 정보 추출 (N+1 문제를 방지하기 위해 Fetch Join 고려)
+        Long areaIdValue = null;
+        String areaNameValue = null;
+        if (entity.getArea() != null) {
+            areaIdValue = entity.getArea().getAreaId();
+            areaNameValue = entity.getArea().getName(); // Area 엔티티에 getName() 메소드가 있다고 가정
+        }
+
+        return new AccommodationDto(
+                entity.getAccommodationId(),
+                areaIdValue,
+                areaNameValue,
+                entity.getName(),
+                entity.getAddress(),
+                entity.getLat(),
+                entity.getLon(),
+                entity.getPhone(),
+                entity.getGu(),
+                entity.getType(),
+                entity.getKakaoUrl()
+        );
+    }
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationResponse.java b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationResponse.java
deleted file mode 100644
index 3b375ff..0000000
--- a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationResponse.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package com.example.placeservice.dto.accommodation;
-
-import jakarta.xml.bind.annotation.XmlAccessType;
-import jakarta.xml.bind.annotation.XmlAccessorType;
-import jakarta.xml.bind.annotation.XmlElement;
-import jakarta.xml.bind.annotation.XmlRootElement;
-import lombok.Data;
-
-import java.util.List;
-
-@Data
-@XmlRootElement(name = "response")
-@XmlAccessorType(XmlAccessType.FIELD)
-public class AccommodationResponse {
-    @XmlElement(name = "header")
-    private Header header;
-
-    @XmlElement(name = "body")
-    private Body body;
-
-    @Data
-    @XmlAccessorType(XmlAccessType.FIELD)
-    public static class Header {
-        @XmlElement(name = "resultCode")
-        private String resultCode;
-
-        @XmlElement(name = "resultMsg")
-        private String resultMsg;
-    }
-
-    @Data
-    @XmlAccessorType(XmlAccessType.FIELD)
-    public static class Body {
-        @XmlElement(name = "items")
-        private Items items;
-
-        @XmlElement(name = "numOfRows")
-        private int numOfRows;
-
-        @XmlElement(name = "pageNo")
-        private int pageNo;
-
-        @XmlElement(name = "totalCount")
-        private int totalCount;
-
-        @Data
-        @XmlAccessorType(XmlAccessType.FIELD)
-        public static class Items {
-            @XmlElement(name = "item")
-            private List item; // 여기를 List으로 변경
-
-            @Data
-            @XmlAccessorType(XmlAccessType.FIELD)
-            public static class Item {
-                private String addr1;
-                private String addr2;
-                private String areacode;
-                private String benikia;
-                private String cat1;
-                private String cat2;
-                private String cat3;
-                private String contentid;
-                private String contenttypeid;
-                private String createdtime;
-                private String firstimage;
-                private String firstimage2;
-                private String cpyrhtDivCd;
-                private String goodstay;
-                private String hanok;
-                private String mapx;
-                private String mapy;
-                private String mlevel;
-                private String modifiedtime;
-                private String tel;
-                private String title;
-                private String booktour;
-                private String sigungucode;
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/dto/accommodation/KakaoApiResponse.java b/place-service/src/main/java/com/example/placeservice/dto/accommodation/KakaoApiResponse.java
new file mode 100644
index 0000000..dbea9ef
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/accommodation/KakaoApiResponse.java
@@ -0,0 +1,55 @@
+package com.example.placeservice.dto.accommodation; // 적절한 패키지 경로로 변경
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true) // API 응답에 모르는 필드가 있어도 무시
+public class KakaoApiResponse {
+    private Meta meta;
+    private List documents;
+
+    @Data
+    @NoArgsConstructor
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Meta {
+        @JsonProperty("total_count")
+        private Integer totalCount;
+        @JsonProperty("pageable_count")
+        private Integer pageableCount;
+        @JsonProperty("is_end")
+        private Boolean isEnd;
+        // 필요시 다른 meta 필드 추가
+    }
+
+    @Data
+    @NoArgsConstructor
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Document {
+        private String id; // 카카오에서 부여하는 장소 ID (String이지만 숫자 형태)
+        @JsonProperty("place_name")
+        private String placeName; // 장소명, 업체명
+        @JsonProperty("category_name")
+        private String categoryName; // 카테고리 이름 (예: 여행 > 숙박 > 호텔)
+        @JsonProperty("category_group_code")
+        private String categoryGroupCode; // 카테고리 그룹 코드
+        @JsonProperty("category_group_name")
+        private String categoryGroupName; // 카테고리 그룹명
+        private String phone; // 전화번호
+        @JsonProperty("address_name")
+        private String addressName; // 전체 지번 주소
+        @JsonProperty("road_address_name")
+        private String roadAddressName; // 전체 도로명 주소
+        private String x; // 경도(longitude) - String 형태
+        private String y; // 위도(latitude) - String 형태
+        @JsonProperty("place_url")
+        private String placeUrl; // 장소 상세페이지 URL
+        @JsonProperty("distance")
+        private String distance; // 중심좌표와의 거리 (단위: m) - radius 파라미터 사용 시
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java
index c69ad47..4595176 100644
--- a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java
+++ b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java
@@ -1,21 +1,20 @@
 package com.example.placeservice.entity;
 
 import jakarta.persistence.*;
+import lombok.Data;
 
 import java.math.BigDecimal;
 
+@Data
 @Entity
 public class Accommodation {
     @Id
-    @GeneratedValue(strategy = GenerationType.IDENTITY)
-    private Long accommodation_id;
+    private Long accommodationId;
 
     @ManyToOne(fetch = FetchType.EAGER)
     @JoinColumn(name = "area_id", referencedColumnName = "area_id", nullable = false)
     private Area area;
 
-    private Long content_id;
-    private String category;
     private String name;
     private String address;
     @Column(precision = 10, scale = 6)
@@ -23,6 +22,7 @@ public class Accommodation {
     @Column(precision = 10, scale = 6)
     private BigDecimal lon;
     private String phone;
-    private String sigungu_code;
-    private String image_url;
+    private String gu;
+    private String type;
+    private String kakaoUrl;
 }
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java
index 9dbd488..808d938 100644
--- a/place-service/src/main/java/com/example/placeservice/entity/Area.java
+++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java
@@ -1,62 +1,33 @@
 package com.example.placeservice.entity;
 
-<<<<<<< HEAD
-import jakarta.persistence.*;
-
-import java.math.BigDecimal;
-
-@Entity
-=======
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import jakarta.persistence.*;
 
 import lombok.Data;
-import lombok.Getter;
 
 import java.math.BigDecimal;
 import java.util.List;
 
 @Entity
 @Data
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
 public class Area {
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     @Column(name = "area_id")
-<<<<<<< HEAD
-    private Long area_id;
-
-    @Column(name = "seoul_id", length = 10, nullable = false)
-    private String seoul_id;
-=======
     private Long areaId;
 
     @Column(name = "seoul_id", length = 10, nullable = false, unique = true)
     private String seoulId;
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
 
     @Column(name = "name", length = 50, nullable = false)
     private String name;  // 지역명
 
     @Column(name = "name_eng", length = 100, nullable = false)
-<<<<<<< HEAD
-    private String name_eng;
-=======
     private String nameEng;
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
 
     @Column(name = "category", length = 20, nullable = false)
     private String category;  // 구분
 
-<<<<<<< HEAD
-    @Column(name = "x", precision = 10, scale = 6, nullable = false)
-    private BigDecimal x; // 위도
-
-    @Column(name = "y", precision = 10, scale = 6, nullable = false)
-    private BigDecimal y; // 경도
-
-
-=======
     @Column(name = "lat", precision = 10, scale = 6, nullable = false)
     private BigDecimal lat; // 위도
 
@@ -66,10 +37,6 @@ public class Area {
     @OneToMany(mappedBy = "area", fetch = FetchType.LAZY)
     @JsonIgnore
     private List attractions;
-<<<<<<< HEAD
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
-=======
 
 
->>>>>>> 7d9e41f58fcc018b3cd8b94bb5aa79ab788ea9b5
-}
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java
index 3b73765..53efab6 100644
--- a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java
+++ b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java
@@ -4,4 +4,5 @@
 import org.springframework.data.jpa.repository.JpaRepository;
 
 public interface AccommodationRepository extends JpaRepository {
+    boolean existsByAccommodationId(Long accommodationId);
 }
diff --git a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java
index 6c7f756..d33237f 100644
--- a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java
+++ b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java
@@ -2,10 +2,6 @@
 
 import com.example.placeservice.entity.Area;
 import org.springframework.data.jpa.repository.JpaRepository;
-<<<<<<< HEAD
-
-public interface AreaRepository extends JpaRepository {
-=======
 import org.springframework.data.jpa.repository.Query;
 
 import java.util.List;
@@ -15,5 +11,4 @@ public interface AreaRepository extends JpaRepository {
 
     @Query("SELECT a.name FROM Area a")
     List findAllAreaNames();
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
-}
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationParseService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationParseService.java
deleted file mode 100644
index 0e8ae17..0000000
--- a/place-service/src/main/java/com/example/placeservice/service/AccommodationParseService.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package com.example.placeservice.service;
-
-import com.example.placeservice.dto.accommodation.AccommodationResponse;
-import jakarta.xml.bind.JAXBContext;
-import jakarta.xml.bind.JAXBException;
-import jakarta.xml.bind.Unmarshaller;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Service;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.StringReader;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.List;
-
-@Service
-public class AccommodationParseService {
-    private final String serviceKey;
-    private final String baseApiUrl = "http://apis.data.go.kr/B551011/KorService1/searchStay1";
-
-    public AccommodationParseService(@Value("${TOUR_API_KEY}") String serviceKey) {
-        this.serviceKey = serviceKey;
-    }
-
-    public List parseXmlData() throws IOException {
-        // API 요청 URL 구성
-        String apiUrl = baseApiUrl + "?arrange=A&areaCode=1&ServiceKey=" + serviceKey
-                + "&listYN=Y&MobileOS=ETC&MobileApp=AppTest&numOfRows=500&pageNo=1";
-
-        // HTTP 연결 및 데이터 가져오기
-        String xmlData = fetchXmlData(apiUrl);
-
-        if (xmlData == null || xmlData.trim().isEmpty()) {
-            throw new IllegalArgumentException("XML 데이터가 비어있습니다.");
-        }
-
-        try {
-            // JAXB 컨텍스트 생성
-            JAXBContext jaxbContext = JAXBContext.newInstance(AccommodationResponse.class);
-
-            // Unmarshaller 생성
-            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
-
-            // XML 문자열을 Java 객체로 변환
-            StringReader reader = new StringReader(xmlData);
-            AccommodationResponse accommodationResponse = (AccommodationResponse) unmarshaller.unmarshal(reader);
-
-            // null 체크 추가
-            if (accommodationResponse == null ||
-                    accommodationResponse.getBody() == null ||
-                    accommodationResponse.getBody().getItems() == null ||
-                    accommodationResponse.getBody().getItems().getItem() == null) {
-                return new ArrayList<>(); // 빈 리스트 반환
-            }
-
-            // 이제 getItem()은 List을 반환하므로 바로 사용 가능
-            return accommodationResponse.getBody().getItems().getItem();
-
-        } catch (JAXBException e) {
-            throw new RuntimeException("XML 파싱 중 오류 발생: " + e.getMessage(), e);
-        } catch (Exception e) {
-            throw new RuntimeException("예상치 못한 오류 발생: " + e.getMessage(), e);
-        }
-    }
-
-    private String fetchXmlData(String apiUrl) throws IOException {
-        URL url = new URL(apiUrl);
-        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
-        connection.setRequestMethod("GET");
-
-        try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
-            StringBuilder xmlData = new StringBuilder();
-            String line;
-            while ((line = reader.readLine()) != null) {
-                xmlData.append(line);
-            }
-            return xmlData.toString();
-        } finally {
-            connection.disconnect(); // 연결 해제
-        }
-    }
-}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationSaveService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationSaveService.java
deleted file mode 100644
index c64fc73..0000000
--- a/place-service/src/main/java/com/example/placeservice/service/AccommodationSaveService.java
+++ /dev/null
@@ -1,159 +0,0 @@
-package com.example.placeservice.service;
-
-import com.example.placeservice.dto.accommodation.AccommodationResponse;
-import com.example.placeservice.entity.Area;
-import jakarta.persistence.EntityManager;
-import jakarta.transaction.Transactional;
-import org.springframework.jdbc.core.BatchPreparedStatementSetter;
-import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.stereotype.Service;
-
-import java.math.BigDecimal;
-import java.sql.PreparedStatement;
-import java.sql.SQLException;
-import java.sql.Types;
-import java.util.List;
-
-@Service
-public class AccommodationSaveService {
-    private final JdbcTemplate jdbcTemplate;
-    private final EntityManager entityManager;
-
-    public AccommodationSaveService(JdbcTemplate jdbcTemplate, EntityManager entityManager) {
-        this.jdbcTemplate = jdbcTemplate;
-        this.entityManager = entityManager;
-    }
-
-    @Transactional
-    public void saveAccommodation(List items) {
-        String sql = "INSERT INTO accommodation (area_id, content_id, category, name, address, lat, lon, phone, sigungu_code, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
-        List allAreas = entityManager.createQuery("SELECT a FROM Area a", Area.class).getResultList();
-
-        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
-            @Override
-            public void setValues(PreparedStatement ps, int i) throws SQLException {
-                AccommodationResponse.Body.Items.Item item = items.get(i);
-
-                Long areaId = findClosestAreaInMemory(new BigDecimal(item.getMapy()), new BigDecimal(item.getMapx()), allAreas);
-                ps.setObject(1, areaId);
-
-                // content_id
-                try {
-                    ps.setLong(2, Long.parseLong(item.getContentid()));
-                } catch (NumberFormatException e) {
-                    ps.setNull(2, Types.BIGINT);
-                }
-
-                // category
-                // 소분류 코드
-                // 0100 : 관광호텔 / 0500 : 콘도미니엄 / 0600 : 유스호스텔 / 0700 : 펜션 / 0900 : 모텔 / 1100 : 게하 / 1600 : 한옥
-                try {
-                    switch (item.getCat3().substring(5)) {
-                        case "0100":
-                            ps.setString(3, "관광호텔"); break;
-                        case "0500":
-                            ps.setString(3, "콘도미니엄"); break;
-                        case "0600":
-                            ps.setString(3, "유스호스텔"); break;
-                        case "0700":
-                            ps.setString(3, "펜션"); break;
-                        case "0900":
-                            ps.setString(3, "모텔"); break;
-                        case "1100":
-                            ps.setString(3, "게스트하우스"); break;
-                        case "1600":
-                            ps.setString(3, "한옥"); break;
-                        default:
-                            ps.setString(3, "구분없음"); break;
-                    }
-                } catch (NumberFormatException e) {
-                    ps.setNull(3, Types.VARCHAR);
-                }
-
-                ps.setString(4, item.getTitle());
-                ps.setString(5, item.getAddr1());
-
-                // lat
-                try {
-                    if (item.getMapy() != null && !item.getMapy().isEmpty()) {
-                        ps.setBigDecimal(6, new BigDecimal(item.getMapy()));
-                    } else {
-                        ps.setNull(6, Types.NUMERIC);
-                    }
-                } catch (NumberFormatException e) {
-                    ps.setNull(6, Types.NUMERIC);
-                }
-
-                // lon
-                try {
-                    if (item.getMapx() != null && !item.getMapx().isEmpty()) {
-                        ps.setBigDecimal(7, new BigDecimal(item.getMapx()));
-                    } else {
-                        ps.setNull(7, Types.NUMERIC);
-                    }
-                } catch (NumberFormatException e) {
-                    ps.setNull(7, Types.NUMERIC);
-                }
-
-                ps.setString(8, item.getTel());
-                ps.setString(9, item.getSigungucode());
-
-                if (item.getFirstimage() != null && !item.getFirstimage().isEmpty()) {
-                    ps.setString(10, item.getFirstimage());
-                } else {
-                    ps.setNull(10, Types.VARCHAR);
-                }
-            }
-
-            @Override
-            public int getBatchSize() {
-                return items.size();
-            }
-        });
-    }
-
-    private Long findClosestAreaInMemory(BigDecimal lat, BigDecimal lon, List areas) {
-        double minDistance = 8000.0;
-        Long closestAreaId = null;
-
-        for (Area area : areas) {
-            if (area.getLat() == null || area.getLon() == null) {
-                System.out.println("지역 좌표 오류 : 지역번호 " + area.getAreaId());
-                continue;
-            }
-            double distance = calculateHaversineDistance(
-                lat.doubleValue(), lon.doubleValue(),
-                area.getLat().doubleValue(), area.getLon().doubleValue()
-            );
-            if (Double.isNaN(distance)) {
-                System.out.println("거리가 숫자가 아닙니다 : 지역번호 " + area.getAreaId());
-                continue;
-            }
-            if (distance < minDistance) {
-                minDistance = distance;
-                closestAreaId = area.getAreaId();
-            }
-        }
-
-        return closestAreaId;
-    }
-
-    private double calculateHaversineDistance(double lat1, double lon1, double lat2, double lon2) {
-        // 지구 반지름 (km)
-        final double R = 6371.0;
-
-        // 라디안으로 변환
-        double latDistance = Math.toRadians(lat2 - lat1);
-        double lonDistance = Math.toRadians(lon2 - lon1);
-
-        // 하버사인 공식
-        double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2)
-                + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
-                * Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2);
-
-        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
-
-        // 거리 계산 (km)
-        return R * c;
-    }
-}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java
index 2dbd975..e215972 100644
--- a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java
+++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java
@@ -1,22 +1,184 @@
 package com.example.placeservice.service;
 
-import com.example.placeservice.dto.accommodation.AccommodationResponse;
-import org.springframework.beans.factory.annotation.Autowired;
+import com.example.placeservice.dto.accommodation.AccommodationDto;
+import com.example.placeservice.dto.accommodation.KakaoApiResponse;
+import com.example.placeservice.entity.Accommodation;
+import com.example.placeservice.entity.Area;
+import com.example.placeservice.repository.AccommodationRepository;
+import com.example.placeservice.repository.AreaRepository;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.github.cdimascio.dotenv.Dotenv;
+import jakarta.transaction.Transactional;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
 
-import java.io.IOException;
+import java.math.BigDecimal;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.List;
 
 @Service
 public class AccommodationService {
-    private final AccommodationParseService accommodationParseService;
+    private final AreaRepository areaRepository;
+    private final AccommodationRepository accommodationRepository; // AccommodationRepository 주입
+    private final ObjectMapper objectMapper;
 
-    @Autowired
-    public AccommodationService(AccommodationParseService accommodationParseService) {
-        this.accommodationParseService = accommodationParseService;
+    private final String baseUrl = "https://dapi.kakao.com/v2/local/search/category.json";
+    private final RestTemplate restTemplate;
+    private final Dotenv dotenv;
+
+    public AccommodationService(AreaRepository areaRepository, AccommodationRepository accommodationRepository, ObjectMapper objectMapper, RestTemplate restTemplate, Dotenv dotenv) {
+        this.areaRepository = areaRepository;
+        this.accommodationRepository = accommodationRepository;
+        this.objectMapper = objectMapper;
+        this.restTemplate = restTemplate;
+        this.dotenv = dotenv;
+    }
+
+    public String searchAccommodations(BigDecimal x, BigDecimal y) {
+        HttpHeaders headers = new HttpHeaders();
+        String kakaoApiKey = dotenv.get("KAKAO_API_KEY");
+        headers.set("Authorization", "KakaoAK " + kakaoApiKey);
+        HttpEntity entity = new HttpEntity<>(headers);
+
+        URI uri = UriComponentsBuilder
+                .fromUriString(baseUrl)
+                .queryParam("category_group_code", "AD5")
+                .queryParam("radius", 1000)
+                .queryParam("x", x.toString()) // BigDecimal을 문자열로 변환
+                .queryParam("y", y.toString()) // BigDecimal을 문자열로 변환
+                .build()
+                .encode(StandardCharsets.UTF_8) // UTF-8 인코딩 명시
+                .toUri();
+
+        try {
+            ResponseEntity response = restTemplate.exchange(
+                    uri,
+                    HttpMethod.GET,
+                    entity,
+                    String.class // 응답 본문을 문자열(JSON)로 받음
+            );
+
+            // 응답 상태 코드가 OK(200) 인지 확인
+            if (response.getStatusCode() == HttpStatus.OK) {
+                return response.getBody(); // 성공 시 응답 본문(JSON 문자열) 반환 [1]
+            } else {
+                // API 호출은 성공했으나, 응답 코드가 200 OK가 아닌 경우
+                System.out.println("Error calling Kakao API. Status code: " + response.getStatusCode() + " Response: " + response.getBody());
+                return null; // 실패 시 null 반환 [1]
+            }
+        } catch (RestClientException e) {
+            // RestTemplate 사용 중 네트워크 오류 등 예외 발생 시
+            System.out.println("Exception during Kakao Api call to " + uri + ": " + e.getMessage() + e);
+            return null; // 예외 발생 시 null 반환
+        }
+    }
+
+    @Transactional
+    public void saveAccommodations() throws JsonProcessingException {
+        List areas = areaRepository.findAll();
+
+        List accommodationsToSave = new ArrayList<>();
+
+        for (Area area : areas) {
+            BigDecimal lon = area.getLon(); // Area에서 경도(lon) 가져오기
+            BigDecimal lat = area.getLat(); // Area에서 위도(lat) 가져오기
+
+            String jsonResponse = searchAccommodations(lon, lat); // API 호출 함수 분리
+
+            if (jsonResponse != null && !jsonResponse.isEmpty()) {
+                try {
+                    KakaoApiResponse apiResponse = objectMapper.readValue(jsonResponse, KakaoApiResponse.class);
+
+                    if (apiResponse != null && apiResponse.getDocuments() != null) {
+                        for (KakaoApiResponse.Document doc : apiResponse.getDocuments()) {
+                            Long kakaoId = Long.parseLong(doc.getId());
+
+                            if (!accommodationRepository.existsByAccommodationId(kakaoId)) { // 중복 여부 확인
+                                Accommodation accommodation = mapDocumentToAccommodation(doc, area); // 매핑 함수 사용
+                                accommodationsToSave.add(accommodation);
+
+                            } else {
+                                System.out.println("새롭게 저장할 숙박업소가 없습니다.");
+                            }
+                        }
+                    }
+                } catch (JsonProcessingException e) {
+                    System.out.println("Json 파싱 실패");
+                } catch (NumberFormatException e) {
+                    System.out.println("숙박업소 Id 오류");
+                }
+            } else {
+                System.out.println("유효한 응답이 없습니다.");
+            }
+            // try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
+        }
+
+        // 5. 수집된 숙소 정보 일괄 저장
+        if (!accommodationsToSave.isEmpty()) {
+            accommodationRepository.saveAll(accommodationsToSave); // saveAll로 성능 향상
+            System.out.println("저장 성공");
+        } else {
+            System.out.println("신규 저장 숙박업소가 없습니다.");
+        }
     }
 
-    public List getAllAccommodationData() throws IOException {
-        return accommodationParseService.parseXmlData();
+    private Accommodation mapDocumentToAccommodation(KakaoApiResponse.Document doc, Area area) {
+        Accommodation accommodation = new Accommodation();
+        try {
+            accommodation.setAccommodationId(Long.parseLong(doc.getId())); // String ID를 Long으로 변환
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("Invalid accommodation ID format: " + doc.getId(), e);
+        }
+        accommodation.setArea(area);
+        accommodation.setName(doc.getPlaceName());
+        accommodation.setAddress(doc.getRoadAddressName());
+        try {
+            accommodation.setLat(new BigDecimal(doc.getY())); // String 좌표를 BigDecimal로 변환
+            accommodation.setLon(new BigDecimal(doc.getX())); // String 좌표를 BigDecimal로 변환
+        } catch (NumberFormatException e) {
+            System.out.println("좌표 설정 실패");
+            accommodation.setLat(null);
+            accommodation.setLon(null);
+        }
+        accommodation.setPhone(doc.getPhone());
+
+        String[] parts = doc.getRoadAddressName().split(" ");
+        for (String part : parts) {
+            if (part.endsWith("구")) {
+                accommodation.setGu(part);
+            }
+        }
+
+        parts = doc.getCategoryName().split(" > ");
+        if (parts.length > 2) {
+            accommodation.setType(parts[2]);
+        }
+        else {
+            accommodation.setType(null);
+        }
+        accommodation.setKakaoUrl(doc.getPlaceUrl());
+
+        return accommodation;
+    }
+
+    public List showAccommodations() {
+        List accommodations = accommodationRepository.findAll();
+        List accommodationDtos = new ArrayList<>();
+        for (Accommodation accommodation : accommodations) {
+            AccommodationDto accommodationDto = AccommodationDto.fromEntity(accommodation);
+            accommodationDtos.add(accommodationDto);
+        }
+
+        return accommodationDtos;
     }
-}
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java
index fb1f491..dc33e3e 100644
--- a/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java
+++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java
@@ -31,4 +31,4 @@ public RestaurantDetailResponse getRestaurantDetail(Long id) {
                 .orElseThrow(() -> new IllegalArgumentException("해당 음식점이 존재하지 않습니다. id=" + id));
         return RestaurantDetailResponse.fromEntity(restaurant);
     }
-}
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java
index 4a71e18..c30a866 100644
--- a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java
+++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java
@@ -106,4 +106,4 @@ private double calculateDistance(double lat1, double lon1, double lat2, double l
 
         return EARTH_RADIUS * c * 1000; // meter 반환
     }
-}
+}
\ No newline at end of file
diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties
index 17afdc3..27952cf 100644
--- a/place-service/src/main/resources/application.properties
+++ b/place-service/src/main/resources/application.properties
@@ -1,10 +1,6 @@
 spring.application.name=place-service
 
-<<<<<<< HEAD
-
-=======
 spring.config.import=optional:file:.env[.properties]
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
 # postgreSQL ??
 spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db
 spring.datasource.username=root
@@ -13,11 +9,6 @@ spring.datasource.driver-class-name=org.postgresql.Driver
 
 spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
 spring.jpa.defer-datasource-initialization=true
-<<<<<<< HEAD
-spring.jpa.hibernate.ddl-auto=create
-spring.jpa.show-sql=true
-spring.sql.init.mode=always
-=======
 spring.jpa.hibernate.ddl-auto=update
 # spring.jpa.hibernate.ddl-auto=create
 spring.jpa.show-sql=true
@@ -28,5 +19,4 @@ spring.jpa.show-sql=true
 kakao.api.key=${KAKAO_API_KEY}
 
 # ?? ??
-logging.level.com.example.placeservice=INFO
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
+logging.level.com.example.placeservice=INFO
\ No newline at end of file
diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql
index 66e5ffe..6d15ed5 100644
--- a/place-service/src/main/resources/data.sql
+++ b/place-service/src/main/resources/data.sql
@@ -1,8 +1,4 @@
-<<<<<<< HEAD
-INSERT INTO area (seoul_id, name, name_eng, category, x, y) VALUES
-=======
 INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
 ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705),
 ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576),
 ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864),
@@ -118,13 +114,5 @@ INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES
 ('POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264),
 ('POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616),
 ('POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821),
-<<<<<<< HEAD
-<<<<<<< HEAD
-('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286);
-=======
-('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286);
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
-=======
 ('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286)
-ON CONFLICT (seoul_id) DO NOTHING;
->>>>>>> 7d9e41f58fcc018b3cd8b94bb5aa79ab788ea9b5
+ON CONFLICT (seoul_id) DO NOTHING;
\ No newline at end of file

From 32df58cfaa80d5b8c105ecc582eebcb53ab6d625 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 13:16:55 +0900
Subject: [PATCH 139/361] =?UTF-8?q?kakao=20API=20=EC=88=99=EB=B0=95=20?=
 =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EC=99=84=EB=A3=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 place-service/build.gradle                    |  13 --
 .../placeservice/PlaceServiceApplication.java |  25 +--
 .../placeservice/config/DataInitializer.java  |  13 +-
 .../controller/AccommodationController.java   |  15 +-
 .../dto/accommodation/AccommodationDto.java   |  48 ++++-
 .../accommodation/AccommodationResponse.java  |  81 --------
 .../dto/accommodation/KakaoApiResponse.java   |  55 ++++++
 .../placeservice/entity/Accommodation.java    |  12 +-
 .../com/example/placeservice/entity/Area.java |  35 +---
 .../repository/AccommodationRepository.java   |   1 +
 .../repository/AreaRepository.java            |   7 +-
 .../service/AccommodationParseService.java    |  85 --------
 .../service/AccommodationSaveService.java     | 159 ---------------
 .../service/AccommodationService.java         | 182 +++++++++++++++++-
 .../service/RestaurantListService.java        |   2 +-
 .../service/RestaurantService.java            |   2 +-
 .../src/main/resources/application.properties |  12 +-
 place-service/src/main/resources/data.sql     |  14 +-
 18 files changed, 294 insertions(+), 467 deletions(-)
 delete mode 100644 place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationResponse.java
 create mode 100644 place-service/src/main/java/com/example/placeservice/dto/accommodation/KakaoApiResponse.java
 delete mode 100644 place-service/src/main/java/com/example/placeservice/service/AccommodationParseService.java
 delete mode 100644 place-service/src/main/java/com/example/placeservice/service/AccommodationSaveService.java

diff --git a/place-service/build.gradle b/place-service/build.gradle
index 4e68f5d..07bd32c 100644
--- a/place-service/build.gradle
+++ b/place-service/build.gradle
@@ -24,36 +24,24 @@ repositories {
 }
 
 dependencies {
-<<<<<<< HEAD
-=======
     // JAXB (XML 파싱)
     implementation 'javax.xml.bind:jaxb-api:2.3.1'
     // JAXB Runtime (XML 파싱을 위해 필요)
     implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3'
 
 
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
     // JWT 처리
     implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
     runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
     runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
 
-<<<<<<< HEAD
-
-=======
     implementation 'org.postgresql:postgresql:42.7.2'
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
     implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
     implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
     //implementation 'org.springframework.boot:spring-boot-starter-data-redis'
     implementation 'org.springframework.boot:spring-boot-starter-security'
     implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
     implementation 'org.springframework.boot:spring-boot-starter-web'
-<<<<<<< HEAD
-    implementation 'org.apache.kafka:kafka-streams'
-    implementation 'org.springframework.kafka:spring-kafka'
-    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
-=======
     //implementation 'io.github.cdimascio:dotenv-java:3.0.0' // 최신 버전
     implementation 'io.github.cdimascio:dotenv-java:2.2.4'
     implementation 'org.apache.kafka:kafka-streams'
@@ -62,7 +50,6 @@ dependencies {
     implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0'
     implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3'
 
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
     compileOnly 'org.projectlombok:lombok'
     developmentOnly 'org.springframework.boot:spring-boot-devtools'
     runtimeOnly 'org.postgresql:postgresql'
diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
index 14665a2..a53bd16 100644
--- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
+++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
@@ -1,21 +1,6 @@
 package com.example.placeservice;
 
-<<<<<<< HEAD
-<<<<<<< HEAD
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-@SpringBootApplication
-public class PlaceServiceApplication {
-
-    public static void main(String[] args) {
-        SpringApplication.run(PlaceServiceApplication.class, args);
-    }
-
-=======
-=======
 import com.example.placeservice.repository.CafeRepository;
->>>>>>> 7d9e41f58fcc018b3cd8b94bb5aa79ab788ea9b5
 import com.example.placeservice.repository.RestaurantRepository;
 import com.example.placeservice.repository.CafeRepository;
 import com.example.placeservice.service.RestaurantService;
@@ -60,13 +45,7 @@ public CommandLineRunner initData(RestaurantService restaurantService, Restauran
                 System.out.println("카페 데이터가 이미 존재합니다. 건너뜁니다.");
             }
 
-            
+
         };
     }
-<<<<<<< HEAD
-
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
-}
-=======
-}
->>>>>>> 7d9e41f58fcc018b3cd8b94bb5aa79ab788ea9b5
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java b/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java
index 7d3aa92..550051d 100644
--- a/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java
+++ b/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java
@@ -1,26 +1,19 @@
 package com.example.placeservice.config;
 
-import com.example.placeservice.dto.accommodation.AccommodationResponse;
-import com.example.placeservice.service.AccommodationParseService;
-import com.example.placeservice.service.AccommodationSaveService;
+import com.example.placeservice.service.AccommodationService;
 import lombok.RequiredArgsConstructor;
 import org.springframework.boot.CommandLineRunner;
 import org.springframework.stereotype.Component;
 
-import java.util.List;
-
 @Component
 @RequiredArgsConstructor
 public class DataInitializer implements CommandLineRunner {
-    private final AccommodationSaveService accommodationSaveService;
-    private final AccommodationParseService accommodationParseService;
+    private final AccommodationService accommodationService;
 
     @Override
     public void run(String... args) throws Exception {
         // API 호출 및 XML 파싱
-        List items = accommodationParseService.parseXmlData();
-        // 파싱된 데이터 저장
-        accommodationSaveService.saveAccommodation(items);
+        accommodationService.saveAccommodations();
 
         System.out.println("데이터 초기화가 완료되었습니다.");
     }
diff --git a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
index 3db30bb..82a562a 100644
--- a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
+++ b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
@@ -1,7 +1,6 @@
 package com.example.placeservice.controller;
 
-import com.example.placeservice.dto.accommodation.AccommodationResponse;
-import com.example.placeservice.service.AccommodationSaveService;
+import com.example.placeservice.dto.accommodation.AccommodationDto;
 import com.example.placeservice.service.AccommodationService;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -13,17 +12,15 @@
 @RestController
 @RequestMapping("/main/accommodation")
 public class AccommodationController {
-    private final AccommodationService accommodationService;
-    private final AccommodationSaveService accommodationSaveService;
+    private final AccommodationService accommodationParseService;
 
     // 생성자 주입
-    public AccommodationController(AccommodationService accommodationService, AccommodationSaveService accommodationSaveService) {
-        this.accommodationService = accommodationService;
-        this.accommodationSaveService = accommodationSaveService;
+    public AccommodationController(AccommodationService accommodationParseService) {
+        this.accommodationParseService = accommodationParseService;
     }
 
     @GetMapping("/list")
-    public List getAccommodations() throws IOException {
-        return accommodationService.getAllAccommodationData();
+    public List getAccommodations() throws IOException {
+        return accommodationParseService.showAccommodations();
     }
 }
diff --git a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java
index 698bea1..0c6291b 100644
--- a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java
+++ b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java
@@ -1,21 +1,59 @@
 package com.example.placeservice.dto.accommodation;
 
-import lombok.*;
+import com.example.placeservice.entity.Accommodation;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
 
 import java.math.BigDecimal;
 
 @Data
 @NoArgsConstructor
 @AllArgsConstructor
-@Builder
 public class AccommodationDto {
+
     private Long accommodationId;
     private Long areaId;
+    private String areaName;
     private String name;
     private String address;
     private BigDecimal lat;
     private BigDecimal lon;
     private String phone;
-    private String sigunguCode;
-    private String imageUrl;
-}
+    private String gu;
+    private String type;
+    private String kakaoUrl;
+
+    /**
+     * Accommodation 엔티티를 AccommodationDto로 변환하는 정적 팩토리 메소드
+     * @param entity 변환할 Accommodation 엔티티
+     * @return 변환된 AccommodationDto 객체
+     */
+    public static AccommodationDto fromEntity(Accommodation entity) {
+        if (entity == null) {
+            return null;
+        }
+
+        // Area 정보 추출 (N+1 문제를 방지하기 위해 Fetch Join 고려)
+        Long areaIdValue = null;
+        String areaNameValue = null;
+        if (entity.getArea() != null) {
+            areaIdValue = entity.getArea().getAreaId();
+            areaNameValue = entity.getArea().getName(); // Area 엔티티에 getName() 메소드가 있다고 가정
+        }
+
+        return new AccommodationDto(
+                entity.getAccommodationId(),
+                areaIdValue,
+                areaNameValue,
+                entity.getName(),
+                entity.getAddress(),
+                entity.getLat(),
+                entity.getLon(),
+                entity.getPhone(),
+                entity.getGu(),
+                entity.getType(),
+                entity.getKakaoUrl()
+        );
+    }
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationResponse.java b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationResponse.java
deleted file mode 100644
index 3b375ff..0000000
--- a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationResponse.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package com.example.placeservice.dto.accommodation;
-
-import jakarta.xml.bind.annotation.XmlAccessType;
-import jakarta.xml.bind.annotation.XmlAccessorType;
-import jakarta.xml.bind.annotation.XmlElement;
-import jakarta.xml.bind.annotation.XmlRootElement;
-import lombok.Data;
-
-import java.util.List;
-
-@Data
-@XmlRootElement(name = "response")
-@XmlAccessorType(XmlAccessType.FIELD)
-public class AccommodationResponse {
-    @XmlElement(name = "header")
-    private Header header;
-
-    @XmlElement(name = "body")
-    private Body body;
-
-    @Data
-    @XmlAccessorType(XmlAccessType.FIELD)
-    public static class Header {
-        @XmlElement(name = "resultCode")
-        private String resultCode;
-
-        @XmlElement(name = "resultMsg")
-        private String resultMsg;
-    }
-
-    @Data
-    @XmlAccessorType(XmlAccessType.FIELD)
-    public static class Body {
-        @XmlElement(name = "items")
-        private Items items;
-
-        @XmlElement(name = "numOfRows")
-        private int numOfRows;
-
-        @XmlElement(name = "pageNo")
-        private int pageNo;
-
-        @XmlElement(name = "totalCount")
-        private int totalCount;
-
-        @Data
-        @XmlAccessorType(XmlAccessType.FIELD)
-        public static class Items {
-            @XmlElement(name = "item")
-            private List item; // 여기를 List으로 변경
-
-            @Data
-            @XmlAccessorType(XmlAccessType.FIELD)
-            public static class Item {
-                private String addr1;
-                private String addr2;
-                private String areacode;
-                private String benikia;
-                private String cat1;
-                private String cat2;
-                private String cat3;
-                private String contentid;
-                private String contenttypeid;
-                private String createdtime;
-                private String firstimage;
-                private String firstimage2;
-                private String cpyrhtDivCd;
-                private String goodstay;
-                private String hanok;
-                private String mapx;
-                private String mapy;
-                private String mlevel;
-                private String modifiedtime;
-                private String tel;
-                private String title;
-                private String booktour;
-                private String sigungucode;
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/dto/accommodation/KakaoApiResponse.java b/place-service/src/main/java/com/example/placeservice/dto/accommodation/KakaoApiResponse.java
new file mode 100644
index 0000000..dbea9ef
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/accommodation/KakaoApiResponse.java
@@ -0,0 +1,55 @@
+package com.example.placeservice.dto.accommodation; // 적절한 패키지 경로로 변경
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true) // API 응답에 모르는 필드가 있어도 무시
+public class KakaoApiResponse {
+    private Meta meta;
+    private List documents;
+
+    @Data
+    @NoArgsConstructor
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Meta {
+        @JsonProperty("total_count")
+        private Integer totalCount;
+        @JsonProperty("pageable_count")
+        private Integer pageableCount;
+        @JsonProperty("is_end")
+        private Boolean isEnd;
+        // 필요시 다른 meta 필드 추가
+    }
+
+    @Data
+    @NoArgsConstructor
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static class Document {
+        private String id; // 카카오에서 부여하는 장소 ID (String이지만 숫자 형태)
+        @JsonProperty("place_name")
+        private String placeName; // 장소명, 업체명
+        @JsonProperty("category_name")
+        private String categoryName; // 카테고리 이름 (예: 여행 > 숙박 > 호텔)
+        @JsonProperty("category_group_code")
+        private String categoryGroupCode; // 카테고리 그룹 코드
+        @JsonProperty("category_group_name")
+        private String categoryGroupName; // 카테고리 그룹명
+        private String phone; // 전화번호
+        @JsonProperty("address_name")
+        private String addressName; // 전체 지번 주소
+        @JsonProperty("road_address_name")
+        private String roadAddressName; // 전체 도로명 주소
+        private String x; // 경도(longitude) - String 형태
+        private String y; // 위도(latitude) - String 형태
+        @JsonProperty("place_url")
+        private String placeUrl; // 장소 상세페이지 URL
+        @JsonProperty("distance")
+        private String distance; // 중심좌표와의 거리 (단위: m) - radius 파라미터 사용 시
+    }
+}
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java
index c69ad47..4595176 100644
--- a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java
+++ b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java
@@ -1,21 +1,20 @@
 package com.example.placeservice.entity;
 
 import jakarta.persistence.*;
+import lombok.Data;
 
 import java.math.BigDecimal;
 
+@Data
 @Entity
 public class Accommodation {
     @Id
-    @GeneratedValue(strategy = GenerationType.IDENTITY)
-    private Long accommodation_id;
+    private Long accommodationId;
 
     @ManyToOne(fetch = FetchType.EAGER)
     @JoinColumn(name = "area_id", referencedColumnName = "area_id", nullable = false)
     private Area area;
 
-    private Long content_id;
-    private String category;
     private String name;
     private String address;
     @Column(precision = 10, scale = 6)
@@ -23,6 +22,7 @@ public class Accommodation {
     @Column(precision = 10, scale = 6)
     private BigDecimal lon;
     private String phone;
-    private String sigungu_code;
-    private String image_url;
+    private String gu;
+    private String type;
+    private String kakaoUrl;
 }
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java
index 9dbd488..808d938 100644
--- a/place-service/src/main/java/com/example/placeservice/entity/Area.java
+++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java
@@ -1,62 +1,33 @@
 package com.example.placeservice.entity;
 
-<<<<<<< HEAD
-import jakarta.persistence.*;
-
-import java.math.BigDecimal;
-
-@Entity
-=======
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import jakarta.persistence.*;
 
 import lombok.Data;
-import lombok.Getter;
 
 import java.math.BigDecimal;
 import java.util.List;
 
 @Entity
 @Data
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
 public class Area {
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     @Column(name = "area_id")
-<<<<<<< HEAD
-    private Long area_id;
-
-    @Column(name = "seoul_id", length = 10, nullable = false)
-    private String seoul_id;
-=======
     private Long areaId;
 
     @Column(name = "seoul_id", length = 10, nullable = false, unique = true)
     private String seoulId;
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
 
     @Column(name = "name", length = 50, nullable = false)
     private String name;  // 지역명
 
     @Column(name = "name_eng", length = 100, nullable = false)
-<<<<<<< HEAD
-    private String name_eng;
-=======
     private String nameEng;
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
 
     @Column(name = "category", length = 20, nullable = false)
     private String category;  // 구분
 
-<<<<<<< HEAD
-    @Column(name = "x", precision = 10, scale = 6, nullable = false)
-    private BigDecimal x; // 위도
-
-    @Column(name = "y", precision = 10, scale = 6, nullable = false)
-    private BigDecimal y; // 경도
-
-
-=======
     @Column(name = "lat", precision = 10, scale = 6, nullable = false)
     private BigDecimal lat; // 위도
 
@@ -66,10 +37,6 @@ public class Area {
     @OneToMany(mappedBy = "area", fetch = FetchType.LAZY)
     @JsonIgnore
     private List attractions;
-<<<<<<< HEAD
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
-=======
 
 
->>>>>>> 7d9e41f58fcc018b3cd8b94bb5aa79ab788ea9b5
-}
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java
index 3b73765..53efab6 100644
--- a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java
+++ b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java
@@ -4,4 +4,5 @@
 import org.springframework.data.jpa.repository.JpaRepository;
 
 public interface AccommodationRepository extends JpaRepository {
+    boolean existsByAccommodationId(Long accommodationId);
 }
diff --git a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java
index 6c7f756..d33237f 100644
--- a/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java
+++ b/place-service/src/main/java/com/example/placeservice/repository/AreaRepository.java
@@ -2,10 +2,6 @@
 
 import com.example.placeservice.entity.Area;
 import org.springframework.data.jpa.repository.JpaRepository;
-<<<<<<< HEAD
-
-public interface AreaRepository extends JpaRepository {
-=======
 import org.springframework.data.jpa.repository.Query;
 
 import java.util.List;
@@ -15,5 +11,4 @@ public interface AreaRepository extends JpaRepository {
 
     @Query("SELECT a.name FROM Area a")
     List findAllAreaNames();
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
-}
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationParseService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationParseService.java
deleted file mode 100644
index 0e8ae17..0000000
--- a/place-service/src/main/java/com/example/placeservice/service/AccommodationParseService.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package com.example.placeservice.service;
-
-import com.example.placeservice.dto.accommodation.AccommodationResponse;
-import jakarta.xml.bind.JAXBContext;
-import jakarta.xml.bind.JAXBException;
-import jakarta.xml.bind.Unmarshaller;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Service;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.StringReader;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.List;
-
-@Service
-public class AccommodationParseService {
-    private final String serviceKey;
-    private final String baseApiUrl = "http://apis.data.go.kr/B551011/KorService1/searchStay1";
-
-    public AccommodationParseService(@Value("${TOUR_API_KEY}") String serviceKey) {
-        this.serviceKey = serviceKey;
-    }
-
-    public List parseXmlData() throws IOException {
-        // API 요청 URL 구성
-        String apiUrl = baseApiUrl + "?arrange=A&areaCode=1&ServiceKey=" + serviceKey
-                + "&listYN=Y&MobileOS=ETC&MobileApp=AppTest&numOfRows=500&pageNo=1";
-
-        // HTTP 연결 및 데이터 가져오기
-        String xmlData = fetchXmlData(apiUrl);
-
-        if (xmlData == null || xmlData.trim().isEmpty()) {
-            throw new IllegalArgumentException("XML 데이터가 비어있습니다.");
-        }
-
-        try {
-            // JAXB 컨텍스트 생성
-            JAXBContext jaxbContext = JAXBContext.newInstance(AccommodationResponse.class);
-
-            // Unmarshaller 생성
-            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
-
-            // XML 문자열을 Java 객체로 변환
-            StringReader reader = new StringReader(xmlData);
-            AccommodationResponse accommodationResponse = (AccommodationResponse) unmarshaller.unmarshal(reader);
-
-            // null 체크 추가
-            if (accommodationResponse == null ||
-                    accommodationResponse.getBody() == null ||
-                    accommodationResponse.getBody().getItems() == null ||
-                    accommodationResponse.getBody().getItems().getItem() == null) {
-                return new ArrayList<>(); // 빈 리스트 반환
-            }
-
-            // 이제 getItem()은 List을 반환하므로 바로 사용 가능
-            return accommodationResponse.getBody().getItems().getItem();
-
-        } catch (JAXBException e) {
-            throw new RuntimeException("XML 파싱 중 오류 발생: " + e.getMessage(), e);
-        } catch (Exception e) {
-            throw new RuntimeException("예상치 못한 오류 발생: " + e.getMessage(), e);
-        }
-    }
-
-    private String fetchXmlData(String apiUrl) throws IOException {
-        URL url = new URL(apiUrl);
-        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
-        connection.setRequestMethod("GET");
-
-        try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
-            StringBuilder xmlData = new StringBuilder();
-            String line;
-            while ((line = reader.readLine()) != null) {
-                xmlData.append(line);
-            }
-            return xmlData.toString();
-        } finally {
-            connection.disconnect(); // 연결 해제
-        }
-    }
-}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationSaveService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationSaveService.java
deleted file mode 100644
index c64fc73..0000000
--- a/place-service/src/main/java/com/example/placeservice/service/AccommodationSaveService.java
+++ /dev/null
@@ -1,159 +0,0 @@
-package com.example.placeservice.service;
-
-import com.example.placeservice.dto.accommodation.AccommodationResponse;
-import com.example.placeservice.entity.Area;
-import jakarta.persistence.EntityManager;
-import jakarta.transaction.Transactional;
-import org.springframework.jdbc.core.BatchPreparedStatementSetter;
-import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.stereotype.Service;
-
-import java.math.BigDecimal;
-import java.sql.PreparedStatement;
-import java.sql.SQLException;
-import java.sql.Types;
-import java.util.List;
-
-@Service
-public class AccommodationSaveService {
-    private final JdbcTemplate jdbcTemplate;
-    private final EntityManager entityManager;
-
-    public AccommodationSaveService(JdbcTemplate jdbcTemplate, EntityManager entityManager) {
-        this.jdbcTemplate = jdbcTemplate;
-        this.entityManager = entityManager;
-    }
-
-    @Transactional
-    public void saveAccommodation(List items) {
-        String sql = "INSERT INTO accommodation (area_id, content_id, category, name, address, lat, lon, phone, sigungu_code, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
-        List allAreas = entityManager.createQuery("SELECT a FROM Area a", Area.class).getResultList();
-
-        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
-            @Override
-            public void setValues(PreparedStatement ps, int i) throws SQLException {
-                AccommodationResponse.Body.Items.Item item = items.get(i);
-
-                Long areaId = findClosestAreaInMemory(new BigDecimal(item.getMapy()), new BigDecimal(item.getMapx()), allAreas);
-                ps.setObject(1, areaId);
-
-                // content_id
-                try {
-                    ps.setLong(2, Long.parseLong(item.getContentid()));
-                } catch (NumberFormatException e) {
-                    ps.setNull(2, Types.BIGINT);
-                }
-
-                // category
-                // 소분류 코드
-                // 0100 : 관광호텔 / 0500 : 콘도미니엄 / 0600 : 유스호스텔 / 0700 : 펜션 / 0900 : 모텔 / 1100 : 게하 / 1600 : 한옥
-                try {
-                    switch (item.getCat3().substring(5)) {
-                        case "0100":
-                            ps.setString(3, "관광호텔"); break;
-                        case "0500":
-                            ps.setString(3, "콘도미니엄"); break;
-                        case "0600":
-                            ps.setString(3, "유스호스텔"); break;
-                        case "0700":
-                            ps.setString(3, "펜션"); break;
-                        case "0900":
-                            ps.setString(3, "모텔"); break;
-                        case "1100":
-                            ps.setString(3, "게스트하우스"); break;
-                        case "1600":
-                            ps.setString(3, "한옥"); break;
-                        default:
-                            ps.setString(3, "구분없음"); break;
-                    }
-                } catch (NumberFormatException e) {
-                    ps.setNull(3, Types.VARCHAR);
-                }
-
-                ps.setString(4, item.getTitle());
-                ps.setString(5, item.getAddr1());
-
-                // lat
-                try {
-                    if (item.getMapy() != null && !item.getMapy().isEmpty()) {
-                        ps.setBigDecimal(6, new BigDecimal(item.getMapy()));
-                    } else {
-                        ps.setNull(6, Types.NUMERIC);
-                    }
-                } catch (NumberFormatException e) {
-                    ps.setNull(6, Types.NUMERIC);
-                }
-
-                // lon
-                try {
-                    if (item.getMapx() != null && !item.getMapx().isEmpty()) {
-                        ps.setBigDecimal(7, new BigDecimal(item.getMapx()));
-                    } else {
-                        ps.setNull(7, Types.NUMERIC);
-                    }
-                } catch (NumberFormatException e) {
-                    ps.setNull(7, Types.NUMERIC);
-                }
-
-                ps.setString(8, item.getTel());
-                ps.setString(9, item.getSigungucode());
-
-                if (item.getFirstimage() != null && !item.getFirstimage().isEmpty()) {
-                    ps.setString(10, item.getFirstimage());
-                } else {
-                    ps.setNull(10, Types.VARCHAR);
-                }
-            }
-
-            @Override
-            public int getBatchSize() {
-                return items.size();
-            }
-        });
-    }
-
-    private Long findClosestAreaInMemory(BigDecimal lat, BigDecimal lon, List areas) {
-        double minDistance = 8000.0;
-        Long closestAreaId = null;
-
-        for (Area area : areas) {
-            if (area.getLat() == null || area.getLon() == null) {
-                System.out.println("지역 좌표 오류 : 지역번호 " + area.getAreaId());
-                continue;
-            }
-            double distance = calculateHaversineDistance(
-                lat.doubleValue(), lon.doubleValue(),
-                area.getLat().doubleValue(), area.getLon().doubleValue()
-            );
-            if (Double.isNaN(distance)) {
-                System.out.println("거리가 숫자가 아닙니다 : 지역번호 " + area.getAreaId());
-                continue;
-            }
-            if (distance < minDistance) {
-                minDistance = distance;
-                closestAreaId = area.getAreaId();
-            }
-        }
-
-        return closestAreaId;
-    }
-
-    private double calculateHaversineDistance(double lat1, double lon1, double lat2, double lon2) {
-        // 지구 반지름 (km)
-        final double R = 6371.0;
-
-        // 라디안으로 변환
-        double latDistance = Math.toRadians(lat2 - lat1);
-        double lonDistance = Math.toRadians(lon2 - lon1);
-
-        // 하버사인 공식
-        double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2)
-                + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
-                * Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2);
-
-        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
-
-        // 거리 계산 (km)
-        return R * c;
-    }
-}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java
index 2dbd975..e215972 100644
--- a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java
+++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java
@@ -1,22 +1,184 @@
 package com.example.placeservice.service;
 
-import com.example.placeservice.dto.accommodation.AccommodationResponse;
-import org.springframework.beans.factory.annotation.Autowired;
+import com.example.placeservice.dto.accommodation.AccommodationDto;
+import com.example.placeservice.dto.accommodation.KakaoApiResponse;
+import com.example.placeservice.entity.Accommodation;
+import com.example.placeservice.entity.Area;
+import com.example.placeservice.repository.AccommodationRepository;
+import com.example.placeservice.repository.AreaRepository;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.github.cdimascio.dotenv.Dotenv;
+import jakarta.transaction.Transactional;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
 
-import java.io.IOException;
+import java.math.BigDecimal;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.List;
 
 @Service
 public class AccommodationService {
-    private final AccommodationParseService accommodationParseService;
+    private final AreaRepository areaRepository;
+    private final AccommodationRepository accommodationRepository; // AccommodationRepository 주입
+    private final ObjectMapper objectMapper;
 
-    @Autowired
-    public AccommodationService(AccommodationParseService accommodationParseService) {
-        this.accommodationParseService = accommodationParseService;
+    private final String baseUrl = "https://dapi.kakao.com/v2/local/search/category.json";
+    private final RestTemplate restTemplate;
+    private final Dotenv dotenv;
+
+    public AccommodationService(AreaRepository areaRepository, AccommodationRepository accommodationRepository, ObjectMapper objectMapper, RestTemplate restTemplate, Dotenv dotenv) {
+        this.areaRepository = areaRepository;
+        this.accommodationRepository = accommodationRepository;
+        this.objectMapper = objectMapper;
+        this.restTemplate = restTemplate;
+        this.dotenv = dotenv;
+    }
+
+    public String searchAccommodations(BigDecimal x, BigDecimal y) {
+        HttpHeaders headers = new HttpHeaders();
+        String kakaoApiKey = dotenv.get("KAKAO_API_KEY");
+        headers.set("Authorization", "KakaoAK " + kakaoApiKey);
+        HttpEntity entity = new HttpEntity<>(headers);
+
+        URI uri = UriComponentsBuilder
+                .fromUriString(baseUrl)
+                .queryParam("category_group_code", "AD5")
+                .queryParam("radius", 1000)
+                .queryParam("x", x.toString()) // BigDecimal을 문자열로 변환
+                .queryParam("y", y.toString()) // BigDecimal을 문자열로 변환
+                .build()
+                .encode(StandardCharsets.UTF_8) // UTF-8 인코딩 명시
+                .toUri();
+
+        try {
+            ResponseEntity response = restTemplate.exchange(
+                    uri,
+                    HttpMethod.GET,
+                    entity,
+                    String.class // 응답 본문을 문자열(JSON)로 받음
+            );
+
+            // 응답 상태 코드가 OK(200) 인지 확인
+            if (response.getStatusCode() == HttpStatus.OK) {
+                return response.getBody(); // 성공 시 응답 본문(JSON 문자열) 반환 [1]
+            } else {
+                // API 호출은 성공했으나, 응답 코드가 200 OK가 아닌 경우
+                System.out.println("Error calling Kakao API. Status code: " + response.getStatusCode() + " Response: " + response.getBody());
+                return null; // 실패 시 null 반환 [1]
+            }
+        } catch (RestClientException e) {
+            // RestTemplate 사용 중 네트워크 오류 등 예외 발생 시
+            System.out.println("Exception during Kakao Api call to " + uri + ": " + e.getMessage() + e);
+            return null; // 예외 발생 시 null 반환
+        }
+    }
+
+    @Transactional
+    public void saveAccommodations() throws JsonProcessingException {
+        List areas = areaRepository.findAll();
+
+        List accommodationsToSave = new ArrayList<>();
+
+        for (Area area : areas) {
+            BigDecimal lon = area.getLon(); // Area에서 경도(lon) 가져오기
+            BigDecimal lat = area.getLat(); // Area에서 위도(lat) 가져오기
+
+            String jsonResponse = searchAccommodations(lon, lat); // API 호출 함수 분리
+
+            if (jsonResponse != null && !jsonResponse.isEmpty()) {
+                try {
+                    KakaoApiResponse apiResponse = objectMapper.readValue(jsonResponse, KakaoApiResponse.class);
+
+                    if (apiResponse != null && apiResponse.getDocuments() != null) {
+                        for (KakaoApiResponse.Document doc : apiResponse.getDocuments()) {
+                            Long kakaoId = Long.parseLong(doc.getId());
+
+                            if (!accommodationRepository.existsByAccommodationId(kakaoId)) { // 중복 여부 확인
+                                Accommodation accommodation = mapDocumentToAccommodation(doc, area); // 매핑 함수 사용
+                                accommodationsToSave.add(accommodation);
+
+                            } else {
+                                System.out.println("새롭게 저장할 숙박업소가 없습니다.");
+                            }
+                        }
+                    }
+                } catch (JsonProcessingException e) {
+                    System.out.println("Json 파싱 실패");
+                } catch (NumberFormatException e) {
+                    System.out.println("숙박업소 Id 오류");
+                }
+            } else {
+                System.out.println("유효한 응답이 없습니다.");
+            }
+            // try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
+        }
+
+        // 5. 수집된 숙소 정보 일괄 저장
+        if (!accommodationsToSave.isEmpty()) {
+            accommodationRepository.saveAll(accommodationsToSave); // saveAll로 성능 향상
+            System.out.println("저장 성공");
+        } else {
+            System.out.println("신규 저장 숙박업소가 없습니다.");
+        }
     }
 
-    public List getAllAccommodationData() throws IOException {
-        return accommodationParseService.parseXmlData();
+    private Accommodation mapDocumentToAccommodation(KakaoApiResponse.Document doc, Area area) {
+        Accommodation accommodation = new Accommodation();
+        try {
+            accommodation.setAccommodationId(Long.parseLong(doc.getId())); // String ID를 Long으로 변환
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("Invalid accommodation ID format: " + doc.getId(), e);
+        }
+        accommodation.setArea(area);
+        accommodation.setName(doc.getPlaceName());
+        accommodation.setAddress(doc.getRoadAddressName());
+        try {
+            accommodation.setLat(new BigDecimal(doc.getY())); // String 좌표를 BigDecimal로 변환
+            accommodation.setLon(new BigDecimal(doc.getX())); // String 좌표를 BigDecimal로 변환
+        } catch (NumberFormatException e) {
+            System.out.println("좌표 설정 실패");
+            accommodation.setLat(null);
+            accommodation.setLon(null);
+        }
+        accommodation.setPhone(doc.getPhone());
+
+        String[] parts = doc.getRoadAddressName().split(" ");
+        for (String part : parts) {
+            if (part.endsWith("구")) {
+                accommodation.setGu(part);
+            }
+        }
+
+        parts = doc.getCategoryName().split(" > ");
+        if (parts.length > 2) {
+            accommodation.setType(parts[2]);
+        }
+        else {
+            accommodation.setType(null);
+        }
+        accommodation.setKakaoUrl(doc.getPlaceUrl());
+
+        return accommodation;
+    }
+
+    public List showAccommodations() {
+        List accommodations = accommodationRepository.findAll();
+        List accommodationDtos = new ArrayList<>();
+        for (Accommodation accommodation : accommodations) {
+            AccommodationDto accommodationDto = AccommodationDto.fromEntity(accommodation);
+            accommodationDtos.add(accommodationDto);
+        }
+
+        return accommodationDtos;
     }
-}
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java
index fb1f491..dc33e3e 100644
--- a/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java
+++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantListService.java
@@ -31,4 +31,4 @@ public RestaurantDetailResponse getRestaurantDetail(Long id) {
                 .orElseThrow(() -> new IllegalArgumentException("해당 음식점이 존재하지 않습니다. id=" + id));
         return RestaurantDetailResponse.fromEntity(restaurant);
     }
-}
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java
index 4a71e18..c30a866 100644
--- a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java
+++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java
@@ -106,4 +106,4 @@ private double calculateDistance(double lat1, double lon1, double lat2, double l
 
         return EARTH_RADIUS * c * 1000; // meter 반환
     }
-}
+}
\ No newline at end of file
diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties
index 17afdc3..27952cf 100644
--- a/place-service/src/main/resources/application.properties
+++ b/place-service/src/main/resources/application.properties
@@ -1,10 +1,6 @@
 spring.application.name=place-service
 
-<<<<<<< HEAD
-
-=======
 spring.config.import=optional:file:.env[.properties]
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
 # postgreSQL ??
 spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db
 spring.datasource.username=root
@@ -13,11 +9,6 @@ spring.datasource.driver-class-name=org.postgresql.Driver
 
 spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
 spring.jpa.defer-datasource-initialization=true
-<<<<<<< HEAD
-spring.jpa.hibernate.ddl-auto=create
-spring.jpa.show-sql=true
-spring.sql.init.mode=always
-=======
 spring.jpa.hibernate.ddl-auto=update
 # spring.jpa.hibernate.ddl-auto=create
 spring.jpa.show-sql=true
@@ -28,5 +19,4 @@ spring.jpa.show-sql=true
 kakao.api.key=${KAKAO_API_KEY}
 
 # ?? ??
-logging.level.com.example.placeservice=INFO
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
+logging.level.com.example.placeservice=INFO
\ No newline at end of file
diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql
index 66e5ffe..6d15ed5 100644
--- a/place-service/src/main/resources/data.sql
+++ b/place-service/src/main/resources/data.sql
@@ -1,8 +1,4 @@
-<<<<<<< HEAD
-INSERT INTO area (seoul_id, name, name_eng, category, x, y) VALUES
-=======
 INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
 ('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705),
 ('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576),
 ('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864),
@@ -118,13 +114,5 @@ INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES
 ('POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264),
 ('POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616),
 ('POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821),
-<<<<<<< HEAD
-<<<<<<< HEAD
-('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286);
-=======
-('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286);
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
-=======
 ('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286)
-ON CONFLICT (seoul_id) DO NOTHING;
->>>>>>> 7d9e41f58fcc018b3cd8b94bb5aa79ab788ea9b5
+ON CONFLICT (seoul_id) DO NOTHING;
\ No newline at end of file

From d6362543c9926c23ef80f63e671e2de1ce6831e2 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 13:47:11 +0900
Subject: [PATCH 140/361] =?UTF-8?q?=EC=88=99=EB=B0=95=20kakao=20API=20?=
 =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EC=99=84=EB=A3=8C=20/=20cafe=20=EB=8D=B0?=
 =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A4=91=EB=B3=B5=20=EC=82=BD=EC=9E=85=20?=
 =?UTF-8?q?=ED=95=B4=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 external-info-service/build.gradle                       | 8 +-------
 .../example/placeservice/PlaceServiceApplication.java    | 9 ++-------
 2 files changed, 3 insertions(+), 14 deletions(-)

diff --git a/external-info-service/build.gradle b/external-info-service/build.gradle
index 69af807..2d6d5c0 100644
--- a/external-info-service/build.gradle
+++ b/external-info-service/build.gradle
@@ -24,21 +24,16 @@ repositories {
 }
 
 dependencies {
-<<<<<<< HEAD
-=======
     // JAXB (XML 파싱)
     implementation 'javax.xml.bind:jaxb-api:2.3.1'
     // JAXB Runtime (XML 파싱을 위해 필요)
     implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3'
 
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
     // JWT 처리
     implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
     runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
     runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
 
-<<<<<<< HEAD
-=======
 
     implementation 'com.fasterxml.jackson.core:jackson-databind'
     implementation 'org.springframework.boot:spring-boot-starter-validation'
@@ -52,7 +47,6 @@ dependencies {
     implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
 
     implementation 'org.postgresql:postgresql:42.7.2'
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
     implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
     implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
     implementation 'org.springframework.boot:spring-boot-starter-data-redis'
@@ -78,4 +72,4 @@ tasks.named('bootBuildImage') {
 
 tasks.named('test') {
     useJUnitPlatform()
-}
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
index a53bd16..f2689a8 100644
--- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
+++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
@@ -1,14 +1,7 @@
 package com.example.placeservice;
 
-import com.example.placeservice.repository.CafeRepository;
-import com.example.placeservice.repository.RestaurantRepository;
-import com.example.placeservice.repository.CafeRepository;
-import com.example.placeservice.service.RestaurantService;
-import com.example.placeservice.service.CafeService;
-import org.springframework.boot.CommandLineRunner;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.context.annotation.Bean;
 import org.springframework.scheduling.annotation.EnableAsync;
 
 @SpringBootApplication
@@ -23,6 +16,7 @@ public static void main(String[] args) {
         SpringApplication.run(PlaceServiceApplication.class, args);
     }
 
+    /*
     @Bean
 
     public CommandLineRunner initData(RestaurantService restaurantService, RestaurantRepository restaurantRepository, CafeService cafeService, CafeRepository cafeRepository) {
@@ -48,4 +42,5 @@ public CommandLineRunner initData(RestaurantService restaurantService, Restauran
 
         };
     }
+    */
 }
\ No newline at end of file

From 5dbc8c374cc0b46531ff61baac9d9ee4c2a5dd88 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 13:47:11 +0900
Subject: [PATCH 141/361] =?UTF-8?q?=EC=88=99=EB=B0=95=20kakao=20API=20?=
 =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EC=99=84=EB=A3=8C=20/=20cafe=20=EB=8D=B0?=
 =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A4=91=EB=B3=B5=20=EC=82=BD=EC=9E=85=20?=
 =?UTF-8?q?=ED=95=B4=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 external-info-service/build.gradle                       | 8 +-------
 .../example/placeservice/PlaceServiceApplication.java    | 9 ++-------
 2 files changed, 3 insertions(+), 14 deletions(-)

diff --git a/external-info-service/build.gradle b/external-info-service/build.gradle
index 69af807..2d6d5c0 100644
--- a/external-info-service/build.gradle
+++ b/external-info-service/build.gradle
@@ -24,21 +24,16 @@ repositories {
 }
 
 dependencies {
-<<<<<<< HEAD
-=======
     // JAXB (XML 파싱)
     implementation 'javax.xml.bind:jaxb-api:2.3.1'
     // JAXB Runtime (XML 파싱을 위해 필요)
     implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3'
 
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
     // JWT 처리
     implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
     runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
     runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
 
-<<<<<<< HEAD
-=======
 
     implementation 'com.fasterxml.jackson.core:jackson-databind'
     implementation 'org.springframework.boot:spring-boot-starter-validation'
@@ -52,7 +47,6 @@ dependencies {
     implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
 
     implementation 'org.postgresql:postgresql:42.7.2'
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
     implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
     implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
     implementation 'org.springframework.boot:spring-boot-starter-data-redis'
@@ -78,4 +72,4 @@ tasks.named('bootBuildImage') {
 
 tasks.named('test') {
     useJUnitPlatform()
-}
+}
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
index a53bd16..f2689a8 100644
--- a/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
+++ b/place-service/src/main/java/com/example/placeservice/PlaceServiceApplication.java
@@ -1,14 +1,7 @@
 package com.example.placeservice;
 
-import com.example.placeservice.repository.CafeRepository;
-import com.example.placeservice.repository.RestaurantRepository;
-import com.example.placeservice.repository.CafeRepository;
-import com.example.placeservice.service.RestaurantService;
-import com.example.placeservice.service.CafeService;
-import org.springframework.boot.CommandLineRunner;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.context.annotation.Bean;
 import org.springframework.scheduling.annotation.EnableAsync;
 
 @SpringBootApplication
@@ -23,6 +16,7 @@ public static void main(String[] args) {
         SpringApplication.run(PlaceServiceApplication.class, args);
     }
 
+    /*
     @Bean
 
     public CommandLineRunner initData(RestaurantService restaurantService, RestaurantRepository restaurantRepository, CafeService cafeService, CafeRepository cafeRepository) {
@@ -48,4 +42,5 @@ public CommandLineRunner initData(RestaurantService restaurantService, Restauran
 
         };
     }
+    */
 }
\ No newline at end of file

From b0a14e69c1e184e3ecbc497050ecf1cacd0f2ab8 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 14:01:08 +0900
Subject: [PATCH 142/361] =?UTF-8?q?=EC=88=99=EB=B0=95=20kakao=20api=20?=
 =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EC=99=84=EB=A3=8C=20/=20=EC=B9=B4?=
 =?UTF-8?q?=ED=8E=98=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A4=91=EB=B3=B5=20?=
 =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 congestion-service/build.gradle                              | 4 +---
 congestion-service/src/main/resources/application.properties | 5 +----
 2 files changed, 2 insertions(+), 7 deletions(-)

diff --git a/congestion-service/build.gradle b/congestion-service/build.gradle
index 0ec6de4..e202b46 100644
--- a/congestion-service/build.gradle
+++ b/congestion-service/build.gradle
@@ -33,9 +33,7 @@ dependencies {
     implementation 'javax.xml.bind:jaxb-api:2.3.1'
     // JAXB Runtime (XML 파싱을 위해 필요)
     implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3'
-
-
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
+    
     // JWT 처리
     implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
     runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
diff --git a/congestion-service/src/main/resources/application.properties b/congestion-service/src/main/resources/application.properties
index 40da7b1..815d0c3 100644
--- a/congestion-service/src/main/resources/application.properties
+++ b/congestion-service/src/main/resources/application.properties
@@ -1,6 +1,4 @@
 spring.application.name=congestion-service
-<<<<<<< HEAD
-=======
 
 # postgreSQL ??
 spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db
@@ -14,5 +12,4 @@ spring.jpa.hibernate.ddl-auto=update
 spring.jpa.defer-datasource-initialization=true
 # Hibernate SQL ?? ???
 spring.jpa.show-sql=true
-spring.sql.init.mode=always
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
+spring.sql.init.mode=always
\ No newline at end of file

From 2c486c534ca26e07257cf12e21cb03bbea018256 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 14:01:08 +0900
Subject: [PATCH 143/361] =?UTF-8?q?=EC=88=99=EB=B0=95=20kakao=20api=20?=
 =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EC=99=84=EB=A3=8C=20/=20=EC=B9=B4?=
 =?UTF-8?q?=ED=8E=98=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A4=91=EB=B3=B5=20?=
 =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 congestion-service/build.gradle                              | 4 +---
 congestion-service/src/main/resources/application.properties | 5 +----
 2 files changed, 2 insertions(+), 7 deletions(-)

diff --git a/congestion-service/build.gradle b/congestion-service/build.gradle
index 0ec6de4..e202b46 100644
--- a/congestion-service/build.gradle
+++ b/congestion-service/build.gradle
@@ -33,9 +33,7 @@ dependencies {
     implementation 'javax.xml.bind:jaxb-api:2.3.1'
     // JAXB Runtime (XML 파싱을 위해 필요)
     implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3'
-
-
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
+    
     // JWT 처리
     implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
     runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
diff --git a/congestion-service/src/main/resources/application.properties b/congestion-service/src/main/resources/application.properties
index 40da7b1..815d0c3 100644
--- a/congestion-service/src/main/resources/application.properties
+++ b/congestion-service/src/main/resources/application.properties
@@ -1,6 +1,4 @@
 spring.application.name=congestion-service
-<<<<<<< HEAD
-=======
 
 # postgreSQL ??
 spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db
@@ -14,5 +12,4 @@ spring.jpa.hibernate.ddl-auto=update
 spring.jpa.defer-datasource-initialization=true
 # Hibernate SQL ?? ???
 spring.jpa.show-sql=true
-spring.sql.init.mode=always
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
+spring.sql.init.mode=always
\ No newline at end of file

From 18144b2be2cd1a677a15c498410c237385ad90b6 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 14:07:58 +0900
Subject: [PATCH 144/361] =?UTF-8?q?congestion=20=EC=84=9C=EB=B9=84?=
 =?UTF-8?q?=EC=8A=A4=20gitignore=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 congestion-service/.gitignore | 1 +
 user-service/.env             | 1 +
 2 files changed, 2 insertions(+)
 create mode 100644 user-service/.env

diff --git a/congestion-service/.gitignore b/congestion-service/.gitignore
index c2065bc..55608a8 100644
--- a/congestion-service/.gitignore
+++ b/congestion-service/.gitignore
@@ -4,6 +4,7 @@ build/
 !gradle/wrapper/gradle-wrapper.jar
 !**/src/main/**/build/
 !**/src/test/**/build/
+.env
 
 ### STS ###
 .apt_generated
diff --git a/user-service/.env b/user-service/.env
new file mode 100644
index 0000000..e5ee787
--- /dev/null
+++ b/user-service/.env
@@ -0,0 +1 @@
+JWT_SECRET_KEY=mySuperSuperSuperSecureKey_ThisIsMoreThanSixtyFourBytesLong_1234567890abcdef

From a34862a6aff8225e6cb2d0def1a161142ece6f1d Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 14:07:58 +0900
Subject: [PATCH 145/361] =?UTF-8?q?congestion=20=EC=84=9C=EB=B9=84?=
 =?UTF-8?q?=EC=8A=A4=20gitignore=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 congestion-service/.gitignore | 1 +
 user-service/.env             | 1 +
 2 files changed, 2 insertions(+)
 create mode 100644 user-service/.env

diff --git a/congestion-service/.gitignore b/congestion-service/.gitignore
index c2065bc..55608a8 100644
--- a/congestion-service/.gitignore
+++ b/congestion-service/.gitignore
@@ -4,6 +4,7 @@ build/
 !gradle/wrapper/gradle-wrapper.jar
 !**/src/main/**/build/
 !**/src/test/**/build/
+.env
 
 ### STS ###
 .apt_generated
diff --git a/user-service/.env b/user-service/.env
new file mode 100644
index 0000000..e5ee787
--- /dev/null
+++ b/user-service/.env
@@ -0,0 +1 @@
+JWT_SECRET_KEY=mySuperSuperSuperSecureKey_ThisIsMoreThanSixtyFourBytesLong_1234567890abcdef

From 40654c622a55f70c6236151a4f4f18abceaccc5c Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 14:12:47 +0900
Subject: [PATCH 146/361] =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=84=9C=EB=B9=84?=
 =?UTF-8?q?=EC=8A=A4=EC=97=90=EC=84=9C=20.env=20=EC=B6=94=EC=A0=81=20?=
 =?UTF-8?q?=EC=A4=91=EB=8B=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 user-service/.env | 1 -
 1 file changed, 1 deletion(-)
 delete mode 100644 user-service/.env

diff --git a/user-service/.env b/user-service/.env
deleted file mode 100644
index e5ee787..0000000
--- a/user-service/.env
+++ /dev/null
@@ -1 +0,0 @@
-JWT_SECRET_KEY=mySuperSuperSuperSecureKey_ThisIsMoreThanSixtyFourBytesLong_1234567890abcdef

From 4913ea0fa1a526aa50c1eb9cc014f020f5341415 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 14:12:47 +0900
Subject: [PATCH 147/361] =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=84=9C=EB=B9=84?=
 =?UTF-8?q?=EC=8A=A4=EC=97=90=EC=84=9C=20.env=20=EC=B6=94=EC=A0=81=20?=
 =?UTF-8?q?=EC=A4=91=EB=8B=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 user-service/.env | 1 -
 1 file changed, 1 deletion(-)
 delete mode 100644 user-service/.env

diff --git a/user-service/.env b/user-service/.env
deleted file mode 100644
index e5ee787..0000000
--- a/user-service/.env
+++ /dev/null
@@ -1 +0,0 @@
-JWT_SECRET_KEY=mySuperSuperSuperSecureKey_ThisIsMoreThanSixtyFourBytesLong_1234567890abcdef

From f8e690881fab16b588e383fc617009c6eed6c97a Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 14:14:55 +0900
Subject: [PATCH 148/361] =?UTF-8?q?=EC=9E=A5=EC=86=8C=20=EC=84=9C=EB=B9=84?=
 =?UTF-8?q?=EC=8A=A4=20gitignore,=20docker-compose=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 place-service/.gitignore         | 8 --------
 place-service/docker-compose.yml | 4 +---
 2 files changed, 1 insertion(+), 11 deletions(-)

diff --git a/place-service/.gitignore b/place-service/.gitignore
index e5d1ef9..844c37d 100644
--- a/place-service/.gitignore
+++ b/place-service/.gitignore
@@ -4,11 +4,8 @@ build/
 !gradle/wrapper/gradle-wrapper.jar
 !**/src/main/**/build/
 !**/src/test/**/build/
-<<<<<<< HEAD
-=======
 .env
 .dockerignore
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
 
 ### STS ###
 .apt_generated
@@ -40,8 +37,3 @@ out/
 
 ### VS Code ###
 .vscode/
-<<<<<<< HEAD
-=======
-
-.env
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
diff --git a/place-service/docker-compose.yml b/place-service/docker-compose.yml
index 261a50d..b4dc6bd 100644
--- a/place-service/docker-compose.yml
+++ b/place-service/docker-compose.yml
@@ -18,15 +18,13 @@ services:
       SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/stars_db
       SPRING_DATASOURCE_USERNAME: root
       SPRING_DATASOURCE_PASSWORD: admin
-<<<<<<< HEAD
-=======
 
     env_file: # .env 파일 로드
       - .env
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
     ports:
       - "8080:8080"
     depends_on:
       - db
+      -
 volumes:
   pgdata:

From 61cd0901e4051f9dc0b2d7e0a66a7120d57fbb34 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 14:14:55 +0900
Subject: [PATCH 149/361] =?UTF-8?q?=EC=9E=A5=EC=86=8C=20=EC=84=9C=EB=B9=84?=
 =?UTF-8?q?=EC=8A=A4=20gitignore,=20docker-compose=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 place-service/.gitignore         | 8 --------
 place-service/docker-compose.yml | 4 +---
 2 files changed, 1 insertion(+), 11 deletions(-)

diff --git a/place-service/.gitignore b/place-service/.gitignore
index e5d1ef9..844c37d 100644
--- a/place-service/.gitignore
+++ b/place-service/.gitignore
@@ -4,11 +4,8 @@ build/
 !gradle/wrapper/gradle-wrapper.jar
 !**/src/main/**/build/
 !**/src/test/**/build/
-<<<<<<< HEAD
-=======
 .env
 .dockerignore
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
 
 ### STS ###
 .apt_generated
@@ -40,8 +37,3 @@ out/
 
 ### VS Code ###
 .vscode/
-<<<<<<< HEAD
-=======
-
-.env
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
diff --git a/place-service/docker-compose.yml b/place-service/docker-compose.yml
index 261a50d..b4dc6bd 100644
--- a/place-service/docker-compose.yml
+++ b/place-service/docker-compose.yml
@@ -18,15 +18,13 @@ services:
       SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/stars_db
       SPRING_DATASOURCE_USERNAME: root
       SPRING_DATASOURCE_PASSWORD: admin
-<<<<<<< HEAD
-=======
 
     env_file: # .env 파일 로드
       - .env
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
     ports:
       - "8080:8080"
     depends_on:
       - db
+      -
 volumes:
   pgdata:

From 716bbb30c1da61bbc82f8aff737fbf4ff1f5d1e5 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 15:52:59 +0900
Subject: [PATCH 150/361] =?UTF-8?q?=EC=88=99=EB=B0=95=20entity=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95=20/=20=EC=88=99=EB=B0=95=20uri=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 place-service/gradlew                         |  2 +-
 .../controller/AccommodationController.java   | 20 +++++++++++++-
 .../dto/accommodation/AccommodationDto.java   |  2 +-
 .../placeservice/entity/Accommodation.java    |  4 ++-
 .../repository/AccommodationRepository.java   |  6 +++++
 .../service/AccommodationService.java         | 27 +++++++++++++++++--
 user-service/.env                             |  1 +
 7 files changed, 56 insertions(+), 6 deletions(-)
 create mode 100644 user-service/.env

diff --git a/place-service/gradlew b/place-service/gradlew
index faf9300..48117b8 100644
--- a/place-service/gradlew
+++ b/place-service/gradlew
@@ -1,4 +1,4 @@
-#!/bin/sh
+ #!/bin/sh
 
 #
 # Copyright © 2015-2021 the original authors.
diff --git a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
index 82a562a..0cf13ea 100644
--- a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
+++ b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
@@ -3,6 +3,7 @@
 import com.example.placeservice.dto.accommodation.AccommodationDto;
 import com.example.placeservice.service.AccommodationService;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -13,14 +14,31 @@
 @RequestMapping("/main/accommodation")
 public class AccommodationController {
     private final AccommodationService accommodationParseService;
+    private final AccommodationService accommodationService;
 
     // 생성자 주입
-    public AccommodationController(AccommodationService accommodationParseService) {
+    public AccommodationController(AccommodationService accommodationParseService, AccommodationService accommodationService) {
         this.accommodationParseService = accommodationParseService;
+        this.accommodationService = accommodationService;
     }
 
     @GetMapping("/list")
     public List getAccommodations() throws IOException {
         return accommodationParseService.showAccommodations();
     }
+
+    @GetMapping("/{accommodation_id}")
+    public AccommodationDto getAccommodationById(@PathVariable Long accommodation_id) throws IOException {
+        return accommodationService.getAccommodationById(accommodation_id);
+    }
+
+    @GetMapping("gu/{gu}")
+    public List getAccommodationByGu(@PathVariable String gu) throws IOException {
+        return accommodationService.getAccommodationByGu(gu);
+    }
+
+    @GetMapping("/type/{type}")
+    public List getAccommodationByType(@PathVariable String type) throws IOException {
+        return accommodationService.getAccommodationByType(type);
+    }
 }
diff --git a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java
index 0c6291b..997e257 100644
--- a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java
+++ b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java
@@ -53,7 +53,7 @@ public static AccommodationDto fromEntity(Accommodation entity) {
                 entity.getPhone(),
                 entity.getGu(),
                 entity.getType(),
-                entity.getKakaoUrl()
+                entity.getKakaomapUrl()
         );
     }
 }
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java
index 4595176..79d051a 100644
--- a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java
+++ b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java
@@ -9,6 +9,7 @@
 @Entity
 public class Accommodation {
     @Id
+    @Column(name = "accommodation_id")
     private Long accommodationId;
 
     @ManyToOne(fetch = FetchType.EAGER)
@@ -24,5 +25,6 @@ public class Accommodation {
     private String phone;
     private String gu;
     private String type;
-    private String kakaoUrl;
+    @Column(name = "kakaomap_url")
+    private String kakaomapUrl;
 }
diff --git a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java
index 53efab6..eaedae0 100644
--- a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java
+++ b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java
@@ -3,6 +3,12 @@
 import com.example.placeservice.entity.Accommodation;
 import org.springframework.data.jpa.repository.JpaRepository;
 
+import java.util.List;
+
 public interface AccommodationRepository extends JpaRepository {
     boolean existsByAccommodationId(Long accommodationId);
+
+    List findByGu(String gu);
+
+    List findByType(String type);
 }
diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java
index e215972..e7067b5 100644
--- a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java
+++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java
@@ -164,9 +164,9 @@ private Accommodation mapDocumentToAccommodation(KakaoApiResponse.Document doc,
             accommodation.setType(parts[2]);
         }
         else {
-            accommodation.setType(null);
+            accommodation.setType("구분없음");
         }
-        accommodation.setKakaoUrl(doc.getPlaceUrl());
+        accommodation.setKakaomapUrl(doc.getPlaceUrl());
 
         return accommodation;
     }
@@ -178,7 +178,30 @@ public List showAccommodations() {
             AccommodationDto accommodationDto = AccommodationDto.fromEntity(accommodation);
             accommodationDtos.add(accommodationDto);
         }
+        return accommodationDtos;
+    }
 
+    public AccommodationDto getAccommodationById(Long id) {
+        return AccommodationDto.fromEntity(accommodationRepository.findById(id).get());
+    }
+
+    public List getAccommodationByGu(String gu) {
+        List accommodations = accommodationRepository.findByGu(gu);
+        List accommodationDtos = new ArrayList<>();
+        for (Accommodation accommodation : accommodations) {
+            AccommodationDto accommodationDto = AccommodationDto.fromEntity(accommodation);
+            accommodationDtos.add(accommodationDto);
+        }
+        return accommodationDtos;
+    }
+
+    public List getAccommodationByType(String type) {
+        List accommodations = accommodationRepository.findByType(type);
+        List accommodationDtos = new ArrayList<>();
+        for (Accommodation accommodation : accommodations) {
+            AccommodationDto accommodationDto = AccommodationDto.fromEntity(accommodation);
+            accommodationDtos.add(accommodationDto);
+        }
         return accommodationDtos;
     }
 }
\ No newline at end of file
diff --git a/user-service/.env b/user-service/.env
new file mode 100644
index 0000000..e5ee787
--- /dev/null
+++ b/user-service/.env
@@ -0,0 +1 @@
+JWT_SECRET_KEY=mySuperSuperSuperSecureKey_ThisIsMoreThanSixtyFourBytesLong_1234567890abcdef

From c65c5df2a3cbc255da794374044f7a084829514f Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 15:52:59 +0900
Subject: [PATCH 151/361] =?UTF-8?q?=EC=88=99=EB=B0=95=20entity=20=EC=88=98?=
 =?UTF-8?q?=EC=A0=95=20/=20=EC=88=99=EB=B0=95=20uri=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 place-service/gradlew                         |  2 +-
 .../controller/AccommodationController.java   | 20 +++++++++++++-
 .../dto/accommodation/AccommodationDto.java   |  2 +-
 .../placeservice/entity/Accommodation.java    |  4 ++-
 .../repository/AccommodationRepository.java   |  6 +++++
 .../service/AccommodationService.java         | 27 +++++++++++++++++--
 user-service/.env                             |  1 +
 7 files changed, 56 insertions(+), 6 deletions(-)
 create mode 100644 user-service/.env

diff --git a/place-service/gradlew b/place-service/gradlew
index faf9300..48117b8 100644
--- a/place-service/gradlew
+++ b/place-service/gradlew
@@ -1,4 +1,4 @@
-#!/bin/sh
+ #!/bin/sh
 
 #
 # Copyright © 2015-2021 the original authors.
diff --git a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
index 82a562a..0cf13ea 100644
--- a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
+++ b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
@@ -3,6 +3,7 @@
 import com.example.placeservice.dto.accommodation.AccommodationDto;
 import com.example.placeservice.service.AccommodationService;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -13,14 +14,31 @@
 @RequestMapping("/main/accommodation")
 public class AccommodationController {
     private final AccommodationService accommodationParseService;
+    private final AccommodationService accommodationService;
 
     // 생성자 주입
-    public AccommodationController(AccommodationService accommodationParseService) {
+    public AccommodationController(AccommodationService accommodationParseService, AccommodationService accommodationService) {
         this.accommodationParseService = accommodationParseService;
+        this.accommodationService = accommodationService;
     }
 
     @GetMapping("/list")
     public List getAccommodations() throws IOException {
         return accommodationParseService.showAccommodations();
     }
+
+    @GetMapping("/{accommodation_id}")
+    public AccommodationDto getAccommodationById(@PathVariable Long accommodation_id) throws IOException {
+        return accommodationService.getAccommodationById(accommodation_id);
+    }
+
+    @GetMapping("gu/{gu}")
+    public List getAccommodationByGu(@PathVariable String gu) throws IOException {
+        return accommodationService.getAccommodationByGu(gu);
+    }
+
+    @GetMapping("/type/{type}")
+    public List getAccommodationByType(@PathVariable String type) throws IOException {
+        return accommodationService.getAccommodationByType(type);
+    }
 }
diff --git a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java
index 0c6291b..997e257 100644
--- a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java
+++ b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java
@@ -53,7 +53,7 @@ public static AccommodationDto fromEntity(Accommodation entity) {
                 entity.getPhone(),
                 entity.getGu(),
                 entity.getType(),
-                entity.getKakaoUrl()
+                entity.getKakaomapUrl()
         );
     }
 }
\ No newline at end of file
diff --git a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java
index 4595176..79d051a 100644
--- a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java
+++ b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java
@@ -9,6 +9,7 @@
 @Entity
 public class Accommodation {
     @Id
+    @Column(name = "accommodation_id")
     private Long accommodationId;
 
     @ManyToOne(fetch = FetchType.EAGER)
@@ -24,5 +25,6 @@ public class Accommodation {
     private String phone;
     private String gu;
     private String type;
-    private String kakaoUrl;
+    @Column(name = "kakaomap_url")
+    private String kakaomapUrl;
 }
diff --git a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java
index 53efab6..eaedae0 100644
--- a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java
+++ b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java
@@ -3,6 +3,12 @@
 import com.example.placeservice.entity.Accommodation;
 import org.springframework.data.jpa.repository.JpaRepository;
 
+import java.util.List;
+
 public interface AccommodationRepository extends JpaRepository {
     boolean existsByAccommodationId(Long accommodationId);
+
+    List findByGu(String gu);
+
+    List findByType(String type);
 }
diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java
index e215972..e7067b5 100644
--- a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java
+++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java
@@ -164,9 +164,9 @@ private Accommodation mapDocumentToAccommodation(KakaoApiResponse.Document doc,
             accommodation.setType(parts[2]);
         }
         else {
-            accommodation.setType(null);
+            accommodation.setType("구분없음");
         }
-        accommodation.setKakaoUrl(doc.getPlaceUrl());
+        accommodation.setKakaomapUrl(doc.getPlaceUrl());
 
         return accommodation;
     }
@@ -178,7 +178,30 @@ public List showAccommodations() {
             AccommodationDto accommodationDto = AccommodationDto.fromEntity(accommodation);
             accommodationDtos.add(accommodationDto);
         }
+        return accommodationDtos;
+    }
 
+    public AccommodationDto getAccommodationById(Long id) {
+        return AccommodationDto.fromEntity(accommodationRepository.findById(id).get());
+    }
+
+    public List getAccommodationByGu(String gu) {
+        List accommodations = accommodationRepository.findByGu(gu);
+        List accommodationDtos = new ArrayList<>();
+        for (Accommodation accommodation : accommodations) {
+            AccommodationDto accommodationDto = AccommodationDto.fromEntity(accommodation);
+            accommodationDtos.add(accommodationDto);
+        }
+        return accommodationDtos;
+    }
+
+    public List getAccommodationByType(String type) {
+        List accommodations = accommodationRepository.findByType(type);
+        List accommodationDtos = new ArrayList<>();
+        for (Accommodation accommodation : accommodations) {
+            AccommodationDto accommodationDto = AccommodationDto.fromEntity(accommodation);
+            accommodationDtos.add(accommodationDto);
+        }
         return accommodationDtos;
     }
 }
\ No newline at end of file
diff --git a/user-service/.env b/user-service/.env
new file mode 100644
index 0000000..e5ee787
--- /dev/null
+++ b/user-service/.env
@@ -0,0 +1 @@
+JWT_SECRET_KEY=mySuperSuperSuperSecureKey_ThisIsMoreThanSixtyFourBytesLong_1234567890abcdef

From 87a7617cfae38d73cbd98930cd57c853a52e26d7 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 15:56:36 +0900
Subject: [PATCH 152/361] =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=84=9C=EB=B9=84?=
 =?UTF-8?q?=EC=8A=A4=20.env=20=EC=B6=94=EC=A0=81=20=EC=A4=91=EB=8B=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 user-service/.env       | 1 -
 user-service/.gitignore | 3 ++-
 2 files changed, 2 insertions(+), 2 deletions(-)
 delete mode 100644 user-service/.env

diff --git a/user-service/.env b/user-service/.env
deleted file mode 100644
index e5ee787..0000000
--- a/user-service/.env
+++ /dev/null
@@ -1 +0,0 @@
-JWT_SECRET_KEY=mySuperSuperSuperSecureKey_ThisIsMoreThanSixtyFourBytesLong_1234567890abcdef
diff --git a/user-service/.gitignore b/user-service/.gitignore
index c2065bc..adf2d96 100644
--- a/user-service/.gitignore
+++ b/user-service/.gitignore
@@ -3,7 +3,8 @@ HELP.md
 build/
 !gradle/wrapper/gradle-wrapper.jar
 !**/src/main/**/build/
-!**/src/test/**/build/
+!**/src/test/**/build
+.env
 
 ### STS ###
 .apt_generated

From 821ac896b54e6f8a8b33fd385fcac25196a54ef7 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 15:56:36 +0900
Subject: [PATCH 153/361] =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=84=9C=EB=B9=84?=
 =?UTF-8?q?=EC=8A=A4=20.env=20=EC=B6=94=EC=A0=81=20=EC=A4=91=EB=8B=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 user-service/.env       | 1 -
 user-service/.gitignore | 3 ++-
 2 files changed, 2 insertions(+), 2 deletions(-)
 delete mode 100644 user-service/.env

diff --git a/user-service/.env b/user-service/.env
deleted file mode 100644
index e5ee787..0000000
--- a/user-service/.env
+++ /dev/null
@@ -1 +0,0 @@
-JWT_SECRET_KEY=mySuperSuperSuperSecureKey_ThisIsMoreThanSixtyFourBytesLong_1234567890abcdef
diff --git a/user-service/.gitignore b/user-service/.gitignore
index c2065bc..adf2d96 100644
--- a/user-service/.gitignore
+++ b/user-service/.gitignore
@@ -3,7 +3,8 @@ HELP.md
 build/
 !gradle/wrapper/gradle-wrapper.jar
 !**/src/main/**/build/
-!**/src/test/**/build/
+!**/src/test/**/build
+.env
 
 ### STS ###
 .apt_generated

From d2a2eec2651216c6c6edbdc82d78b6f2b37fc58a Mon Sep 17 00:00:00 2001
From: ha-yun 
Date: Mon, 28 Apr 2025 16:25:49 +0900
Subject: [PATCH 154/361] =?UTF-8?q?Refactor:=20=ED=96=89=EC=82=AC=20?=
 =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=95=EC=86=8C(=EA=B3=BC?=
 =?UTF-8?q?=EA=B1=B0=20=ED=96=89=EC=82=AC=20=EC=A0=9C=EC=99=B8)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/CulturalEventController.java   | 10 ++++-
 .../dto/culturalevent/CulturalEventDto.java   | 39 +++++++++++++++++++
 .../external/CulturalEventClient.java         | 20 ----------
 .../service/CulturalEventService.java         | 28 ++++++++++---
 4 files changed, 70 insertions(+), 27 deletions(-)
 create mode 100644 place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventDto.java
 delete mode 100644 place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java

diff --git a/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java b/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java
index 76fd796..d149dc0 100644
--- a/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java
+++ b/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java
@@ -1,5 +1,6 @@
 package com.example.placeservice.controller;
 
+import com.example.placeservice.dto.culturalevent.CulturalEventDto;
 import com.example.placeservice.entity.CulturalEvent;
 import com.example.placeservice.repository.CulturalEventRepository;
 import lombok.RequiredArgsConstructor;
@@ -9,6 +10,7 @@
 import org.springframework.web.bind.annotation.RestController;
 
 import java.util.List;
+import java.util.stream.Collectors;
 
 @RestController
 @RequestMapping("/main")
@@ -20,7 +22,11 @@ public class CulturalEventController {
 
     // /main/events: 저장된 모든 이벤트 조회
     @GetMapping("/events")
-    public List getAllEvents() {
-        return culturalEventRepository.findAll();
+    public List getAllEvents() {
+        List events = culturalEventRepository.findAll();
+        return events.stream()
+                .map(CulturalEventDto::new)  // 생성자 이용해서 CulturalEvent -> DTO 변환
+                .collect(Collectors.toList());
+
     }
 }
diff --git a/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventDto.java b/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventDto.java
new file mode 100644
index 0000000..d1fcfb1
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventDto.java
@@ -0,0 +1,39 @@
+package com.example.placeservice.dto.culturalevent;
+
+import com.example.placeservice.entity.CulturalEvent;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+public class CulturalEventDto {
+
+    private Long event_id;
+    private String category;
+    private String title;
+    private String address;
+    private BigDecimal lat;
+    private BigDecimal lon;
+    private String target;
+    private String event_fee;
+    private String event_img;
+    private LocalDateTime start_date;
+    private LocalDateTime end_date;
+
+    public CulturalEventDto(CulturalEvent culturalEvent) {
+        this.event_id = culturalEvent.getEventId();
+        this.category = culturalEvent.getCategory();
+        this.title = culturalEvent.getTitle();
+        this.address = culturalEvent.getAddress();
+        this.lat = culturalEvent.getLat();
+        this.lon = culturalEvent.getLon();
+        this.target = culturalEvent.getTarget();
+        this.event_fee = culturalEvent.getEventFee();
+        this.event_img = culturalEvent.getEventImg();
+        this.start_date = culturalEvent.getStartDate();
+        this.end_date = culturalEvent.getEndDate();
+
+    }
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java b/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java
deleted file mode 100644
index 027dc4e..0000000
--- a/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.example.placeservice.external;
-
-import org.springframework.stereotype.Component;
-import org.springframework.web.client.RestTemplate;
-
-@Component
-public class CulturalEventClient {
-
-    private final RestTemplate restTemplate;
-
-    public CulturalEventClient(RestTemplate restTemplate) {
-        this.restTemplate = restTemplate;
-    }
-
-    // API에서 1000개씩 데이터를 받아오는 메서드
-    public String fetchEventData(int startIndex, int endIndex) {
-        String url = String.format("http://openapi.seoul.go.kr:8088/7669764c417069613736734567476c/json/culturalEventInfo/%d/%d/", startIndex, endIndex);
-        return restTemplate.getForObject(url, String.class);
-    }
-}
diff --git a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java
index 05fb404..e4f4d67 100644
--- a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java
+++ b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java
@@ -3,25 +3,28 @@
 import com.example.placeservice.dto.culturalevent.CulturalEventItem;
 import com.example.placeservice.entity.Area;
 import com.example.placeservice.entity.CulturalEvent;
-import com.example.placeservice.external.CulturalEventClient;
 import com.example.placeservice.repository.AreaRepository;
 import com.example.placeservice.repository.CulturalEventRepository;
 import com.example.placeservice.util.GeoUtils;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.RestTemplate;
 
+import java.time.LocalDate;
+import java.util.ArrayList;
 import java.util.List;
 
 @Service
 @RequiredArgsConstructor
 public class CulturalEventService {
 
-    private final CulturalEventClient culturalEventClient;
     private final EventParserService eventParserService;
     private final CulturalEventRepository culturalEventRepository;
     private final AreaRepository areaRepository; // 🔹 추가
 
+    private final RestTemplate restTemplate;
+
     @Transactional
     public void fetchAndSaveAllEvents() {
         int[] ranges = {1, 1001, 2001, 3001, 4001, 5001};
@@ -29,11 +32,22 @@ public void fetchAndSaveAllEvents() {
             int startIndex = ranges[i];
             int endIndex = startIndex + 999;
 
-            String jsonData = culturalEventClient.fetchEventData(startIndex, endIndex);
+            String jsonData = fetchEventData(startIndex, endIndex);
             List eventItems = eventParserService.parse(jsonData);
+            
+            // 행사 종료일이 오늘 이후 또는 오늘인 것만 추가
+            List filteredList = new ArrayList<>();
+            for (CulturalEventItem item : eventItems) {
+                String endDateStr = String.valueOf(item.getEndDate());
+                LocalDate endDate = LocalDate.parse(endDateStr.substring(0, 10));
+
+                if (!endDate.isBefore(LocalDate.now())) {
+                    filteredList.add(item);
+                }
+            }
 
             if (!eventItems.isEmpty()) {
-                saveEvents(eventItems);
+                saveEvents(filteredList);
             }
         }
     }
@@ -69,7 +83,11 @@ private Area findNearestArea(double lat, double lon, List areas) {
                 nearestArea = area;
             }
         }
-
         return nearestArea;
     }
+
+    private String fetchEventData(int startIndex, int endIndex) {
+        String url = String.format("http://openapi.seoul.go.kr:8088/7669764c417069613736734567476c/json/culturalEventInfo/%d/%d/", startIndex, endIndex);
+        return restTemplate.getForObject(url, String.class);
+    }
 }

From f9683f010153c54a8ee2702ea1431dd5060ef248 Mon Sep 17 00:00:00 2001
From: ha-yun 
Date: Mon, 28 Apr 2025 16:25:49 +0900
Subject: [PATCH 155/361] =?UTF-8?q?Refactor:=20=ED=96=89=EC=82=AC=20?=
 =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=95=EC=86=8C(=EA=B3=BC?=
 =?UTF-8?q?=EA=B1=B0=20=ED=96=89=EC=82=AC=20=EC=A0=9C=EC=99=B8)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../controller/CulturalEventController.java   | 10 ++++-
 .../dto/culturalevent/CulturalEventDto.java   | 39 +++++++++++++++++++
 .../external/CulturalEventClient.java         | 20 ----------
 .../service/CulturalEventService.java         | 28 ++++++++++---
 4 files changed, 70 insertions(+), 27 deletions(-)
 create mode 100644 place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventDto.java
 delete mode 100644 place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java

diff --git a/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java b/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java
index 76fd796..d149dc0 100644
--- a/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java
+++ b/place-service/src/main/java/com/example/placeservice/controller/CulturalEventController.java
@@ -1,5 +1,6 @@
 package com.example.placeservice.controller;
 
+import com.example.placeservice.dto.culturalevent.CulturalEventDto;
 import com.example.placeservice.entity.CulturalEvent;
 import com.example.placeservice.repository.CulturalEventRepository;
 import lombok.RequiredArgsConstructor;
@@ -9,6 +10,7 @@
 import org.springframework.web.bind.annotation.RestController;
 
 import java.util.List;
+import java.util.stream.Collectors;
 
 @RestController
 @RequestMapping("/main")
@@ -20,7 +22,11 @@ public class CulturalEventController {
 
     // /main/events: 저장된 모든 이벤트 조회
     @GetMapping("/events")
-    public List getAllEvents() {
-        return culturalEventRepository.findAll();
+    public List getAllEvents() {
+        List events = culturalEventRepository.findAll();
+        return events.stream()
+                .map(CulturalEventDto::new)  // 생성자 이용해서 CulturalEvent -> DTO 변환
+                .collect(Collectors.toList());
+
     }
 }
diff --git a/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventDto.java b/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventDto.java
new file mode 100644
index 0000000..d1fcfb1
--- /dev/null
+++ b/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventDto.java
@@ -0,0 +1,39 @@
+package com.example.placeservice.dto.culturalevent;
+
+import com.example.placeservice.entity.CulturalEvent;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+public class CulturalEventDto {
+
+    private Long event_id;
+    private String category;
+    private String title;
+    private String address;
+    private BigDecimal lat;
+    private BigDecimal lon;
+    private String target;
+    private String event_fee;
+    private String event_img;
+    private LocalDateTime start_date;
+    private LocalDateTime end_date;
+
+    public CulturalEventDto(CulturalEvent culturalEvent) {
+        this.event_id = culturalEvent.getEventId();
+        this.category = culturalEvent.getCategory();
+        this.title = culturalEvent.getTitle();
+        this.address = culturalEvent.getAddress();
+        this.lat = culturalEvent.getLat();
+        this.lon = culturalEvent.getLon();
+        this.target = culturalEvent.getTarget();
+        this.event_fee = culturalEvent.getEventFee();
+        this.event_img = culturalEvent.getEventImg();
+        this.start_date = culturalEvent.getStartDate();
+        this.end_date = culturalEvent.getEndDate();
+
+    }
+
+}
diff --git a/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java b/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java
deleted file mode 100644
index 027dc4e..0000000
--- a/place-service/src/main/java/com/example/placeservice/external/CulturalEventClient.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.example.placeservice.external;
-
-import org.springframework.stereotype.Component;
-import org.springframework.web.client.RestTemplate;
-
-@Component
-public class CulturalEventClient {
-
-    private final RestTemplate restTemplate;
-
-    public CulturalEventClient(RestTemplate restTemplate) {
-        this.restTemplate = restTemplate;
-    }
-
-    // API에서 1000개씩 데이터를 받아오는 메서드
-    public String fetchEventData(int startIndex, int endIndex) {
-        String url = String.format("http://openapi.seoul.go.kr:8088/7669764c417069613736734567476c/json/culturalEventInfo/%d/%d/", startIndex, endIndex);
-        return restTemplate.getForObject(url, String.class);
-    }
-}
diff --git a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java
index 05fb404..e4f4d67 100644
--- a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java
+++ b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java
@@ -3,25 +3,28 @@
 import com.example.placeservice.dto.culturalevent.CulturalEventItem;
 import com.example.placeservice.entity.Area;
 import com.example.placeservice.entity.CulturalEvent;
-import com.example.placeservice.external.CulturalEventClient;
 import com.example.placeservice.repository.AreaRepository;
 import com.example.placeservice.repository.CulturalEventRepository;
 import com.example.placeservice.util.GeoUtils;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.RestTemplate;
 
+import java.time.LocalDate;
+import java.util.ArrayList;
 import java.util.List;
 
 @Service
 @RequiredArgsConstructor
 public class CulturalEventService {
 
-    private final CulturalEventClient culturalEventClient;
     private final EventParserService eventParserService;
     private final CulturalEventRepository culturalEventRepository;
     private final AreaRepository areaRepository; // 🔹 추가
 
+    private final RestTemplate restTemplate;
+
     @Transactional
     public void fetchAndSaveAllEvents() {
         int[] ranges = {1, 1001, 2001, 3001, 4001, 5001};
@@ -29,11 +32,22 @@ public void fetchAndSaveAllEvents() {
             int startIndex = ranges[i];
             int endIndex = startIndex + 999;
 
-            String jsonData = culturalEventClient.fetchEventData(startIndex, endIndex);
+            String jsonData = fetchEventData(startIndex, endIndex);
             List eventItems = eventParserService.parse(jsonData);
+            
+            // 행사 종료일이 오늘 이후 또는 오늘인 것만 추가
+            List filteredList = new ArrayList<>();
+            for (CulturalEventItem item : eventItems) {
+                String endDateStr = String.valueOf(item.getEndDate());
+                LocalDate endDate = LocalDate.parse(endDateStr.substring(0, 10));
+
+                if (!endDate.isBefore(LocalDate.now())) {
+                    filteredList.add(item);
+                }
+            }
 
             if (!eventItems.isEmpty()) {
-                saveEvents(eventItems);
+                saveEvents(filteredList);
             }
         }
     }
@@ -69,7 +83,11 @@ private Area findNearestArea(double lat, double lon, List areas) {
                 nearestArea = area;
             }
         }
-
         return nearestArea;
     }
+
+    private String fetchEventData(int startIndex, int endIndex) {
+        String url = String.format("http://openapi.seoul.go.kr:8088/7669764c417069613736734567476c/json/culturalEventInfo/%d/%d/", startIndex, endIndex);
+        return restTemplate.getForObject(url, String.class);
+    }
 }

From c8b751ba4c9d0963ea8ec97344fb36c86ae07385 Mon Sep 17 00:00:00 2001
From: suminmmmmmmm 
Date: Mon, 28 Apr 2025 16:28:21 +0900
Subject: [PATCH 156/361] =?UTF-8?q?Refactor:=20redis=EC=97=90=20=EB=A6=AC?=
 =?UTF-8?q?=ED=94=84=EB=A0=88=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EC=A0=80?=
 =?UTF-8?q?=EC=9E=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Back-STARS                                    |   2 +-
 user-service/build.gradle                     |   2 +
 .../userservice/config/ApplicationConfig.java |  50 +++++++
 .../example/userservice/config/JpaConfig.java |  11 ++
 .../OAuth2AuthenticationSuccessHandler.java   |  38 ++++++
 .../userservice/config/RedisConfig.java       |  11 ++
 .../config/RedisRepositoryConfig.java         |  28 ++++
 .../userservice/config/SecurityConfig.java    |   8 +-
 .../controller/AuthController.java            |  51 +++++++
 .../com/example/userservice/dto/AuthDto.java  |  55 ++++++++
 .../example/userservice/dto/MemberDto.java    |  44 ++++++
 .../example/userservice/dto/MemberSign.java   |  21 +--
 .../example/userservice/entity/Member.java    |  29 ++--
 .../userservice/entity/RefreshToken.java      |  19 +++
 .../userservice/entity/TokenRedis.java        |  21 +++
 .../repository/MemberRepository.java          |  10 --
 .../repository/TokenRedisRepository.java      |  11 ++
 .../repository/jpa/MemberRepository.java      |  16 +++
 .../redis/RefreshTokenRepository.java         |  13 ++
 .../security/JwtAuthenticationFilter.java     | 103 ++++++++++++++
 .../example/userservice/security/JwtUtil.java | 126 ++++++++++++++++++
 .../userservice/service/AuthService.java      |  78 +++++++++++
 .../userservice/service/CookieUtil.java       |  21 +++
 .../userservice/service/MemberService.java    |  30 +++--
 .../userservice/service/TokenService.java     |  25 ++++
 .../src/main/resources/application.properties |  10 +-
 26 files changed, 778 insertions(+), 55 deletions(-)
 create mode 100644 user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
 create mode 100644 user-service/src/main/java/com/example/userservice/config/JpaConfig.java
 create mode 100644 user-service/src/main/java/com/example/userservice/config/OAuth2AuthenticationSuccessHandler.java
 create mode 100644 user-service/src/main/java/com/example/userservice/config/RedisConfig.java
 create mode 100644 user-service/src/main/java/com/example/userservice/config/RedisRepositoryConfig.java
 create mode 100644 user-service/src/main/java/com/example/userservice/controller/AuthController.java
 create mode 100644 user-service/src/main/java/com/example/userservice/dto/AuthDto.java
 create mode 100644 user-service/src/main/java/com/example/userservice/dto/MemberDto.java
 create mode 100644 user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
 create mode 100644 user-service/src/main/java/com/example/userservice/entity/TokenRedis.java
 delete mode 100644 user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
 create mode 100644 user-service/src/main/java/com/example/userservice/repository/TokenRedisRepository.java
 create mode 100644 user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java
 create mode 100644 user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java
 create mode 100644 user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
 create mode 100644 user-service/src/main/java/com/example/userservice/security/JwtUtil.java
 create mode 100644 user-service/src/main/java/com/example/userservice/service/AuthService.java
 create mode 100644 user-service/src/main/java/com/example/userservice/service/CookieUtil.java
 create mode 100644 user-service/src/main/java/com/example/userservice/service/TokenService.java

diff --git a/Back-STARS b/Back-STARS
index 1230fba..c42a641 160000
--- a/Back-STARS
+++ b/Back-STARS
@@ -1 +1 @@
-Subproject commit 1230fbad88e7b4ae62a7080d04646e647a3a6793
+Subproject commit c42a641af776e3112ad553dcd34f2ed7659c268b
diff --git a/user-service/build.gradle b/user-service/build.gradle
index 0f3fee3..0205bbb 100644
--- a/user-service/build.gradle
+++ b/user-service/build.gradle
@@ -37,6 +37,8 @@ dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
     implementation 'org.springframework.boot:spring-boot-starter-web'
     implementation 'org.apache.kafka:kafka-streams'
+    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+    implementation 'org.springframework.session:spring-session-data-redis'
     implementation 'org.springframework.kafka:spring-kafka'
     implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
     compileOnly 'org.projectlombok:lombok'
diff --git a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
new file mode 100644
index 0000000..5e16036
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
@@ -0,0 +1,50 @@
+package com.example.userservice.config;
+
+import com.example.userservice.repository.jpa.MemberRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@Configuration
+@RequiredArgsConstructor
+public class ApplicationConfig {
+
+    private final MemberRepository memberRepository;
+
+    @Bean
+    public UserDetailsService userDetailsService() {
+        return username -> memberRepository.findByUserId(username)
+                .map(member -> new org.springframework.security.core.userdetails.User(
+                        member.getUserId(),  // userId를 사용하도록 변경
+                        member.getPassword(),
+                        java.util.Collections.emptyList()
+                ))
+                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
+    }
+
+    @Bean
+    public AuthenticationProvider authenticationProvider() {
+        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
+        authProvider.setUserDetailsService(userDetailsService());
+        authProvider.setPasswordEncoder(passwordEncoder());
+        return authProvider;
+    }
+
+    @Bean
+    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
+        return config.getAuthenticationManager();
+    }
+
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/config/JpaConfig.java b/user-service/src/main/java/com/example/userservice/config/JpaConfig.java
new file mode 100644
index 0000000..d35f648
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/JpaConfig.java
@@ -0,0 +1,11 @@
+package com.example.userservice.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+@Configuration
+@EnableJpaRepositories(
+        basePackages = "com.example.userservice.repository.jpa"
+)
+public class JpaConfig {
+}
diff --git a/user-service/src/main/java/com/example/userservice/config/OAuth2AuthenticationSuccessHandler.java b/user-service/src/main/java/com/example/userservice/config/OAuth2AuthenticationSuccessHandler.java
new file mode 100644
index 0000000..d44b8a0
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/OAuth2AuthenticationSuccessHandler.java
@@ -0,0 +1,38 @@
+package com.example.userservice.security.oauth;
+
+import com.example.userservice.security.JwtUtil;
+import com.example.userservice.service.TokenService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.stereotype.Component;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {
+
+    private final JwtUtil jwtUtil;
+    private final TokenService tokenService;
+
+    @Override
+    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
+        // 1. 인증 성공했을 때 authentication 객체를 이용해서
+        // 2. accessToken, refreshToken 생성
+        String accessToken = jwtUtil.generateToken((UserDetails) authentication.getPrincipal());
+        String refreshToken = jwtUtil.generateRefreshToken((UserDetails) authentication.getPrincipal());
+
+        // 3. Redis에 Refresh Token 저장
+        String userId = authentication.getName();  // OAuth2 로그인 시 기본적으로 userId 같은 값
+        tokenService.saveRefreshToken(Long.parseLong(userId), refreshToken); // userId가 Long 타입이면
+
+        // 4. 응답에 AccessToken, RefreshToken 보내주기 (예시)
+        response.setContentType("application/json;charset=UTF-8");
+        response.getWriter().write("{\"accessToken\": \"" + accessToken + "\", \"refreshToken\": \"" + refreshToken + "\"}");
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/config/RedisConfig.java b/user-service/src/main/java/com/example/userservice/config/RedisConfig.java
new file mode 100644
index 0000000..b6e9eb7
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/RedisConfig.java
@@ -0,0 +1,11 @@
+package com.example.userservice.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
+
+@Configuration
+@EnableRedisRepositories(
+        basePackages = "com.example.userservice.repository.redis"
+)
+public class RedisConfig {
+}
diff --git a/user-service/src/main/java/com/example/userservice/config/RedisRepositoryConfig.java b/user-service/src/main/java/com/example/userservice/config/RedisRepositoryConfig.java
new file mode 100644
index 0000000..f952331
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/RedisRepositoryConfig.java
@@ -0,0 +1,28 @@
+package com.example.userservice.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+
+public class RedisRepositoryConfig {
+    @Value("${spring.data.redis.host}")
+    private String host;
+
+    @Value("${spring.data.redis.port}")
+    private int port;
+
+    @Value("${spring.data.redis.password}")
+    private String password;
+
+    @Bean
+    public RedisConnectionFactory redisConnectionFactory() {
+        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
+        redisStandaloneConfiguration.setHostName(host);
+        redisStandaloneConfiguration.setPassword(password);
+        redisStandaloneConfiguration.setPort(port);
+
+        return new LettuceConnectionFactory(redisStandaloneConfiguration);
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
index ff04de3..545a633 100644
--- a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
+++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
@@ -19,8 +19,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
     }
 
     //비밀번호 암호화 적용
-    @Bean
-    public BCryptPasswordEncoder passwordEncoder() {
-        return new BCryptPasswordEncoder();
-    }
+//    @Bean
+//    public BCryptPasswordEncoder passwordEncoder() {
+//        return new BCryptPasswordEncoder();
+//    }
 }
diff --git a/user-service/src/main/java/com/example/userservice/controller/AuthController.java b/user-service/src/main/java/com/example/userservice/controller/AuthController.java
new file mode 100644
index 0000000..9defdce
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/controller/AuthController.java
@@ -0,0 +1,51 @@
+package com.example.userservice.controller;
+
+import com.example.userservice.dto.AuthDto;
+import com.example.userservice.service.AuthService;
+import com.example.userservice.service.CookieUtil;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/auth")
+@RequiredArgsConstructor
+public class AuthController {
+
+    private final AuthService authService;
+    private final CookieUtil cookieUtil;
+
+    @PostMapping("/login")
+    public ResponseEntity login(
+            @RequestBody AuthDto.LoginRequest request,
+            HttpServletResponse response) {
+
+        // 디버깅 로그 출력
+        System.out.println("로그인 컨트롤러 호출: " + request.getUser_id());
+
+        AuthDto.LoginResponse loginResponse = authService.login(request);
+
+        // 쿠키에 토큰 저장
+        cookieUtil.addCookie(response, "access_token", loginResponse.getAccessToken(), cookieUtil.getDaysInSeconds(1));
+        cookieUtil.addCookie(response, "refresh_token", loginResponse.getRefreshToken(), cookieUtil.getDaysInSeconds(30));
+
+        return ResponseEntity.ok(loginResponse);
+    }
+
+    @PostMapping("/logout")
+    public ResponseEntity logout(
+            @RequestBody AuthDto.LogoutRequest request,
+            HttpServletResponse response) {
+
+        // 쿠키 삭제
+        cookieUtil.addCookie(response, "access_token", "", 0);
+        cookieUtil.addCookie(response, "refresh_token", "", 0);
+
+        // authService의 logout 메서드 호출
+        return ResponseEntity.ok(authService.logout(request));
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/AuthDto.java b/user-service/src/main/java/com/example/userservice/dto/AuthDto.java
new file mode 100644
index 0000000..9a04608
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/AuthDto.java
@@ -0,0 +1,55 @@
+package com.example.userservice.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+public class AuthDto {
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LoginRequest {
+        private String user_id;  // 로그인 시 사용자가 입력하는 ID
+        private String password;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LoginResponse {
+        private String accessToken;
+        private String refreshToken;
+        private String member_id;  // 내부 관리용 ID
+        private String user_id;    // 로그인용 ID
+        private String nickname;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LogoutRequest {
+        private String accessToken;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LogoutResponse {
+        private boolean success;
+        private String message;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class RefreshTokenRequest {
+        private String refreshToken;
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java
new file mode 100644
index 0000000..3648c46
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java
@@ -0,0 +1,44 @@
+package com.example.userservice.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+public class MemberDto {
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class MemberResponse {
+        private String user_id;
+        private String nickname;
+        private Short birth_year;
+        private LocalDateTime created_at;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class UpdateRequest {
+        private String nickname;
+        private Short birth_year;
+        private String current_password;
+        private String new_password;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class RegisterRequest {
+        private String user_id;
+        private String password;
+        private String nickname;
+        private Short birth_year;
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
index a648540..058318e 100644
--- a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
@@ -2,25 +2,18 @@
 
 import lombok.Getter;
 import lombok.Setter;
+import com.fasterxml.jackson.annotation.JsonProperty;
 
 @Getter
 @Setter
-
 public class MemberSign {
-    //private Long memberId;
+    @JsonProperty("user_id")  // JSON 필드 이름 매핑
+    private String userId;
+
     private String password;
     private String nickname;
     private String createTime;
 
-//    public Long getMemberId() {
-//        return memberId;
-//    }
-
-    public String getPassword() {
-        return password;
-    }
-
-    public String getNickname() {
-        return nickname;
-    }
-}
+    @JsonProperty("birth_year")  // JSON 필드 이름 매핑
+    private Short birthYear;
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/Member.java b/user-service/src/main/java/com/example/userservice/entity/Member.java
index a6e4b46..bf3a84e 100644
--- a/user-service/src/main/java/com/example/userservice/entity/Member.java
+++ b/user-service/src/main/java/com/example/userservice/entity/Member.java
@@ -2,31 +2,36 @@
 
 import jakarta.persistence.*;
 import lombok.AllArgsConstructor;
-import lombok.Getter;
+import lombok.Builder;
+import lombok.Data;
 import lombok.NoArgsConstructor;
-import lombok.Setter;
+
+import java.time.LocalDateTime;
 
 @Entity
-@Getter
-@Setter
-@NoArgsConstructor // JPA용 기본 생성자
-@AllArgsConstructor // 모든 필드로 객체를 편하게 생성
 @Table(name = "member")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
 public class Member {
-
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
-    @Column(name = "member_id",nullable = false)
+    @Column(name = "member_id", nullable = false)
     private Long memberId;
 
+    @Column(name = "user_id", nullable = true, unique = true)
+    private String userId;
+
     @Column(nullable = false)
     private String password;
 
     @Column
     private String nickname;
 
-    @Column(name = "create_at")
-    private String createAt;
-
+    @Column(name = "birth_year")
+    private Short birthYear;
 
-}
+    @Column(name = "created_at")
+    private LocalDateTime createdAt;
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
new file mode 100644
index 0000000..dc67ee4
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
@@ -0,0 +1,19 @@
+package com.example.userservice.entity;
+
+import lombok.*;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+
+@Getter
+@Setter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@RedisHash(value = "refresh_token", timeToLive = 60 * 60 * 24 * 7) // 7일 동안 유지
+public class RefreshToken {
+
+    @Id
+    private String id;  // Redis의 key (보통 memberId나 userId를 넣는다)
+
+    private String refreshToken;
+}
diff --git a/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java b/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java
new file mode 100644
index 0000000..19affd3
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java
@@ -0,0 +1,21 @@
+package com.example.userservice.entity;
+
+import jakarta.persistence.Id;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.data.redis.core.RedisHash;
+import org.springframework.data.redis.core.index.Indexed;
+
+@Getter
+@AllArgsConstructor
+@RedisHash(value = "token", timeToLive = 60 * 60 * 24 * 7) // 리프레시토큰과 expiretime 일치
+public class TokenRedis {
+
+    @Id
+    private String id;
+
+    @Indexed
+    private String accessToken;
+
+    private String refreshToken;
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java b/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
deleted file mode 100644
index 14955d4..0000000
--- a/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.example.userservice.repository;
-
-import com.example.userservice.entity.Member;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface MemberRepository extends JpaRepository {
-    boolean existsByMemberId(Long memberId);
-    boolean existsByNickname(String nickname);
-}
-
diff --git a/user-service/src/main/java/com/example/userservice/repository/TokenRedisRepository.java b/user-service/src/main/java/com/example/userservice/repository/TokenRedisRepository.java
new file mode 100644
index 0000000..30e8fe1
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/TokenRedisRepository.java
@@ -0,0 +1,11 @@
+package com.example.userservice.repository;
+
+import com.example.userservice.entity.TokenRedis;
+import org.springframework.data.repository.CrudRepository;
+
+import java.util.Optional;
+
+public interface TokenRedisRepository extends CrudRepository {
+
+    Optional findByAccessToken(String accessToken); // AccessToken으로 찾아내기
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java b/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java
new file mode 100644
index 0000000..cea05f5
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java
@@ -0,0 +1,16 @@
+package com.example.userservice.repository.jpa;
+
+import com.example.userservice.entity.Member;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface MemberRepository extends JpaRepository {
+    boolean existsByMemberId(Long memberId);
+    boolean existsByUserId(String userId);
+    boolean existsByNickname(String nickname);
+    Optional findByNickname(String nickname);
+    Optional findByUserId(String userId);
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java b/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java
new file mode 100644
index 0000000..f871368
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java
@@ -0,0 +1,13 @@
+package com.example.userservice.repository.redis;
+
+import com.example.userservice.entity.RefreshToken;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface RefreshTokenRepository extends JpaRepository {
+    Optional findByUserId(Long userId);
+    Optional findByRefreshToken(String refreshToken);
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..b917cea
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
@@ -0,0 +1,103 @@
+package com.example.userservice.security;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+    private final JwtUtil jwtUtil;
+    private final UserDetailsService userDetailsService;
+
+    // 필터링하지 않을 URL 패턴 목록
+    private final List excludedPaths = Arrays.asList("/auth/signup", "/auth/login", "/auth/logout");
+
+    @Override
+    protected void doFilterInternal(
+            HttpServletRequest request,
+            HttpServletResponse response,
+            FilterChain filterChain
+    ) throws ServletException, IOException {
+
+        // 인증이 필요없는 경로는 필터를 적용하지 않음
+        String requestPath = request.getServletPath();
+        if (shouldNotFilter(request)) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        // Authorization 헤더에서 토큰 추출 시도
+        final String authHeader = request.getHeader("Authorization");
+        String jwt = null;
+
+        // Authorization 헤더가 있으면 해당 토큰 사용
+        if (authHeader != null && authHeader.startsWith("Bearer ")) {
+            jwt = authHeader.substring(7);
+        }
+        // 아니면 쿠키에서 토큰 찾기
+        else {
+            Cookie[] cookies = request.getCookies();
+            if (cookies != null) {
+                for (Cookie cookie : cookies) {
+                    if ("access_token".equals(cookie.getName())) {
+                        jwt = cookie.getValue();
+                        break;
+                    }
+                }
+            }
+        }
+
+        // 토큰이 없으면 다음 필터로 이동
+        if (jwt == null) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        try {
+            final String username = jwtUtil.extractUsername(jwt);
+
+            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
+
+                if (jwtUtil.validateToken(jwt, userDetails)) {
+                    UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
+                            userDetails,
+                            null,
+                            userDetails.getAuthorities()
+                    );
+                    authToken.setDetails(
+                            new WebAuthenticationDetailsSource().buildDetails(request)
+                    );
+                    SecurityContextHolder.getContext().setAuthentication(authToken);
+                }
+            }
+        } catch (Exception e) {
+            // 토큰 검증 과정에서 오류가 발생하면 로그만 남기고 계속 진행
+            logger.error("JWT 토큰 검증 실패: " + e.getMessage());
+        }
+
+        filterChain.doFilter(request, response);
+    }
+
+    @Override
+    protected boolean shouldNotFilter(HttpServletRequest request) {
+        String path = request.getServletPath();
+        return excludedPaths.stream().anyMatch(path::startsWith);
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/security/JwtUtil.java b/user-service/src/main/java/com/example/userservice/security/JwtUtil.java
new file mode 100644
index 0000000..c582096
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/security/JwtUtil.java
@@ -0,0 +1,126 @@
+package com.example.userservice.security;
+
+import io.github.cdimascio.dotenv.Dotenv;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.security.Keys;
+import jakarta.annotation.PostConstruct;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+@Component
+public class JwtUtil {
+
+    private SecretKey secretKey;
+    private Long accessTokenExpiration; // 액세스 토큰 만료 시간 (ms)
+    private Long refreshTokenExpiration; // 리프레시 토큰 만료 시간 (ms)
+
+    @PostConstruct
+    public void init() {
+        Dotenv dotenv = Dotenv.configure()
+                .ignoreIfMalformed()
+                .ignoreIfMissing()
+                .load();
+
+        String jwtSecret = dotenv.get("JWT_SECRET_KEY");
+        if (jwtSecret == null || jwtSecret.isEmpty()) {
+            throw new IllegalStateException("JWT_SECRET_KEY 환경변수가 설정되지 않았습니다!");
+        }
+
+        this.secretKey = Keys.hmacShaKeyFor(jwtSecret.getBytes());
+        this.accessTokenExpiration = Long.parseLong(dotenv.get("JWT_EXPIRATION", "1800000")); // 기본값 30분
+        this.refreshTokenExpiration = Long.parseLong(dotenv.get("JWT_REFRESH_EXPIRATION", "604800000")); // 기본값 7일
+    }
+
+    // 사용자 이름 추출
+    public String extractUsername(String token) {
+        return extractClaim(token, Claims::getSubject);
+    }
+
+    // 만료 시간 추출
+    public Date extractExpiration(String token) {
+        return extractClaim(token, Claims::getExpiration);
+    }
+
+    // 클레임 추출
+    public  T extractClaim(String token, Function claimsResolver) {
+        final Claims claims = extractAllClaims(token);
+        return claimsResolver.apply(claims);
+    }
+
+    // 모든 클레임 추출
+    private Claims extractAllClaims(String token) {
+        return Jwts
+                .parser()
+                .verifyWith(secretKey)
+                .build()
+                .parseSignedClaims(token)
+                .getPayload();
+    }
+
+    // 토큰 만료 확인
+    private Boolean isTokenExpired(String token) {
+        return extractExpiration(token).before(new Date());
+    }
+
+    // 액세스 토큰 생성
+    public String generateToken(UserDetails userDetails) {
+        Map claims = new HashMap<>();
+        return createToken(claims, userDetails.getUsername(), accessTokenExpiration);
+    }
+
+    // 액세스 토큰 생성 (nickname 직접 전달)
+    public String generateAccessToken(String nickname) {
+        Map claims = new HashMap<>();
+        return createToken(claims, nickname, accessTokenExpiration);
+    }
+
+    // 리프레시 토큰 생성
+    public String generateRefreshToken(UserDetails userDetails) {
+        Map claims = new HashMap<>();
+        return createToken(claims, userDetails.getUsername(), refreshTokenExpiration);
+    }
+
+    // 리프레시 토큰 생성 (nickname 직접 전달)
+    public String generateRefreshToken(String nickname) {
+        Map claims = new HashMap<>();
+        return createToken(claims, nickname, refreshTokenExpiration);
+    }
+
+    // 토큰 생성 메소드
+    private String createToken(Map claims, String subject, Long expirationTime) {
+        return Jwts.builder()
+                .claims(claims)
+                .subject(subject)
+                .issuedAt(new Date(System.currentTimeMillis()))
+                .expiration(new Date(System.currentTimeMillis() + expirationTime))
+                .signWith(secretKey, SignatureAlgorithm.HS512)
+                .compact();
+    }
+
+    // 토큰 유효성 검증 (UserDetails와 비교)
+    public Boolean validateToken(String token, UserDetails userDetails) {
+        final String username = extractUsername(token);
+        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
+    }
+
+    // 토큰 유효성만 검사 (사용자 정보 없이)
+    public Boolean validateToken(String token) {
+        try {
+            Jwts.parser()
+                    .verifyWith(secretKey)
+                    .build()
+                    .parseSignedClaims(token);
+            return !isTokenExpired(token);
+        } catch (Exception e) {
+            return false;
+        }
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/AuthService.java b/user-service/src/main/java/com/example/userservice/service/AuthService.java
new file mode 100644
index 0000000..cd4028f
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/AuthService.java
@@ -0,0 +1,78 @@
+package com.example.userservice.service;
+
+import com.example.userservice.dto.AuthDto;
+import com.example.userservice.entity.Member;
+import com.example.userservice.repository.jpa.MemberRepository;
+import com.example.userservice.security.JwtUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class AuthService {
+
+    private final MemberRepository memberRepository;
+    private final JwtUtil jwtUtil;
+    private final AuthenticationManager authenticationManager;
+    private final TokenService tokenService;
+
+    public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
+        // 디버깅 로그
+        System.out.println("로그인 시도: " + request.getUser_id());
+
+        // 1. 사용자 조회 (userId로 조회)
+        Optional memberOptional = memberRepository.findByUserId(request.getUser_id());
+        if (memberOptional.isEmpty()) {
+            System.out.println("사용자를 찾을 수 없음: " + request.getUser_id());
+            throw new RuntimeException("User not found");
+        }
+        Member member = memberOptional.get();
+        System.out.println("사용자 찾음: ID=" + member.getMemberId() + ", UserID=" + member.getUserId());
+
+        // 2. 인증 시도
+        try {
+            authenticationManager.authenticate(
+                    new UsernamePasswordAuthenticationToken(
+                            request.getUser_id(),
+                            request.getPassword()
+                    )
+            );
+            System.out.println("인증 성공");
+        } catch (Exception e) {
+            System.out.println("인증 실패: " + e.getMessage());
+            throw e;
+        }
+
+        // 3. 토큰 생성
+        UserDetails userDetails = new User(member.getUserId(), member.getPassword(), Collections.emptyList());
+        String accessToken = jwtUtil.generateToken(userDetails);
+        String refreshToken = jwtUtil.generateRefreshToken(userDetails);
+
+        // 4. Refresh Token을 Redis에 저장
+        tokenService.saveRefreshToken(member.getMemberId(), refreshToken);
+
+        // 5. 결과 리턴
+        return AuthDto.LoginResponse.builder()
+                .accessToken(accessToken)
+                .refreshToken(refreshToken)
+                .member_id(String.valueOf(member.getMemberId()))
+                .user_id(member.getUserId())
+                .nickname(member.getNickname())
+                .build();
+    }
+
+    public AuthDto.LogoutResponse logout(AuthDto.LogoutRequest request) {
+        // 로그아웃 로직도 나중에 redis에서 refresh token 삭제하는 걸 추가할 수 있음
+        return AuthDto.LogoutResponse.builder()
+                .success(true)
+                .message("Logout successful")
+                .build();
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/service/CookieUtil.java b/user-service/src/main/java/com/example/userservice/service/CookieUtil.java
new file mode 100644
index 0000000..2a1ab1f
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/CookieUtil.java
@@ -0,0 +1,21 @@
+package com.example.userservice.service;
+
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Service;
+
+@Service
+public class CookieUtil {
+
+    public void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
+        Cookie cookie = new Cookie(name, value);
+        cookie.setPath("/");
+        cookie.setHttpOnly(true);
+        cookie.setMaxAge(maxAge);
+        response.addCookie(cookie);
+    }
+
+    public int getDaysInSeconds(int days) {
+        return days * 24 * 60 * 60;
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java
index 6f2f58f..cba58f6 100644
--- a/user-service/src/main/java/com/example/userservice/service/MemberService.java
+++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java
@@ -2,9 +2,9 @@
 
 import com.example.userservice.dto.MemberSign;
 import com.example.userservice.entity.Member;
-import com.example.userservice.repository.MemberRepository;
+import com.example.userservice.repository.jpa.MemberRepository;
 import lombok.RequiredArgsConstructor;
-import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
@@ -13,22 +13,26 @@
 @RequiredArgsConstructor
 public class MemberService {
     private final MemberRepository memberRepository;
-    private final BCryptPasswordEncoder passwordEncoder;
+    private final PasswordEncoder passwordEncoder;
 
     public Member registerAdmin(MemberSign dto) {
-//        if (memberRepository.existsByMemberId(dto.getMemberId())) {
-//            throw new IllegalStateException("이미 존재하는 ID입니다.");
-//        }
+        // userId 중복 체크
+        if (memberRepository.existsByUserId(dto.getUserId())) {
+            throw new IllegalStateException("이미 존재하는 사용자 ID입니다.");
+        }
+
         if (memberRepository.existsByNickname(dto.getNickname())) {
             throw new IllegalStateException("이미 존재하는 닉네임입니다.");
         }
 
-        Member member = new Member();
-        member.setPassword(passwordEncoder.encode(dto.getPassword()));
-        member.setNickname(dto.getNickname());
-        member.setCreateAt(String.valueOf(LocalDateTime.now()));
+        Member member = Member.builder()
+                .userId(dto.getUserId())  // userId 설정
+                .password(passwordEncoder.encode(dto.getPassword()))
+                .nickname(dto.getNickname())
+                .birthYear(dto.getBirthYear())
+                .createdAt(LocalDateTime.now())
+                .build();
 
-        Member savedMember = memberRepository.save(member);
-        return savedMember; // 저장된 객체 리턴
+        return memberRepository.save(member);
     }
-}
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/TokenService.java b/user-service/src/main/java/com/example/userservice/service/TokenService.java
new file mode 100644
index 0000000..648e9b6
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/TokenService.java
@@ -0,0 +1,25 @@
+package com.example.userservice.service;
+
+import com.example.userservice.entity.RefreshToken;
+import com.example.userservice.repository.redis.RefreshTokenRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class TokenService {
+
+    private final RefreshTokenRepository refreshTokenRepository;
+
+    public void saveRefreshToken(Long memberId, String refreshToken) {
+        RefreshToken token = RefreshToken.builder()
+                .id(String.valueOf(memberId))  // 여기!! id로 memberId를 문자열로 저장
+                .refreshToken(refreshToken)
+                .build();
+        refreshTokenRepository.save(token);
+    }
+
+    public void deleteRefreshToken(Long memberId) {
+        refreshTokenRepository.deleteById((memberId));
+    }
+}
diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties
index a2c035e..d99579a 100644
--- a/user-service/src/main/resources/application.properties
+++ b/user-service/src/main/resources/application.properties
@@ -1,11 +1,19 @@
 spring.application.name=user-service
 
+server.port=8082
+
 spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db
 spring.datasource.username=root
 spring.datasource.password=admin
 spring.datasource.driver-class-name=org.postgresql.Driver
 spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
-spring.jpa.hibernate.ddl-auto=create
+spring.jpa.hibernate.ddl-auto=update
+
+spring.data.redis.host=localhost
+spring.data.redis.port=6379
+spring.data.redis.password=1234
 
 
 jwt.secret=${JWT_SECRET_KEY}
+jwt.expiration=86400000
+jwt.refresh-expiration=2592000000
\ No newline at end of file

From 31a3ef86d52a581791cfbe319e2ae82fcc8e03d1 Mon Sep 17 00:00:00 2001
From: suminmmmmmmm 
Date: Mon, 28 Apr 2025 16:28:21 +0900
Subject: [PATCH 157/361] =?UTF-8?q?Refactor:=20redis=EC=97=90=20=EB=A6=AC?=
 =?UTF-8?q?=ED=94=84=EB=A0=88=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EC=A0=80?=
 =?UTF-8?q?=EC=9E=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Back-STARS                                    |   2 +-
 user-service/build.gradle                     |   2 +
 .../userservice/config/ApplicationConfig.java |  50 +++++++
 .../example/userservice/config/JpaConfig.java |  11 ++
 .../OAuth2AuthenticationSuccessHandler.java   |  38 ++++++
 .../userservice/config/RedisConfig.java       |  11 ++
 .../config/RedisRepositoryConfig.java         |  28 ++++
 .../userservice/config/SecurityConfig.java    |   8 +-
 .../controller/AuthController.java            |  51 +++++++
 .../com/example/userservice/dto/AuthDto.java  |  55 ++++++++
 .../example/userservice/dto/MemberDto.java    |  44 ++++++
 .../example/userservice/dto/MemberSign.java   |  21 +--
 .../example/userservice/entity/Member.java    |  29 ++--
 .../userservice/entity/RefreshToken.java      |  19 +++
 .../userservice/entity/TokenRedis.java        |  21 +++
 .../repository/MemberRepository.java          |  10 --
 .../repository/TokenRedisRepository.java      |  11 ++
 .../repository/jpa/MemberRepository.java      |  16 +++
 .../redis/RefreshTokenRepository.java         |  13 ++
 .../security/JwtAuthenticationFilter.java     | 103 ++++++++++++++
 .../example/userservice/security/JwtUtil.java | 126 ++++++++++++++++++
 .../userservice/service/AuthService.java      |  78 +++++++++++
 .../userservice/service/CookieUtil.java       |  21 +++
 .../userservice/service/MemberService.java    |  30 +++--
 .../userservice/service/TokenService.java     |  25 ++++
 .../src/main/resources/application.properties |  10 +-
 26 files changed, 778 insertions(+), 55 deletions(-)
 create mode 100644 user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
 create mode 100644 user-service/src/main/java/com/example/userservice/config/JpaConfig.java
 create mode 100644 user-service/src/main/java/com/example/userservice/config/OAuth2AuthenticationSuccessHandler.java
 create mode 100644 user-service/src/main/java/com/example/userservice/config/RedisConfig.java
 create mode 100644 user-service/src/main/java/com/example/userservice/config/RedisRepositoryConfig.java
 create mode 100644 user-service/src/main/java/com/example/userservice/controller/AuthController.java
 create mode 100644 user-service/src/main/java/com/example/userservice/dto/AuthDto.java
 create mode 100644 user-service/src/main/java/com/example/userservice/dto/MemberDto.java
 create mode 100644 user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
 create mode 100644 user-service/src/main/java/com/example/userservice/entity/TokenRedis.java
 delete mode 100644 user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
 create mode 100644 user-service/src/main/java/com/example/userservice/repository/TokenRedisRepository.java
 create mode 100644 user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java
 create mode 100644 user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java
 create mode 100644 user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
 create mode 100644 user-service/src/main/java/com/example/userservice/security/JwtUtil.java
 create mode 100644 user-service/src/main/java/com/example/userservice/service/AuthService.java
 create mode 100644 user-service/src/main/java/com/example/userservice/service/CookieUtil.java
 create mode 100644 user-service/src/main/java/com/example/userservice/service/TokenService.java

diff --git a/Back-STARS b/Back-STARS
index 1230fba..c42a641 160000
--- a/Back-STARS
+++ b/Back-STARS
@@ -1 +1 @@
-Subproject commit 1230fbad88e7b4ae62a7080d04646e647a3a6793
+Subproject commit c42a641af776e3112ad553dcd34f2ed7659c268b
diff --git a/user-service/build.gradle b/user-service/build.gradle
index 0f3fee3..0205bbb 100644
--- a/user-service/build.gradle
+++ b/user-service/build.gradle
@@ -37,6 +37,8 @@ dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
     implementation 'org.springframework.boot:spring-boot-starter-web'
     implementation 'org.apache.kafka:kafka-streams'
+    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+    implementation 'org.springframework.session:spring-session-data-redis'
     implementation 'org.springframework.kafka:spring-kafka'
     implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
     compileOnly 'org.projectlombok:lombok'
diff --git a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
new file mode 100644
index 0000000..5e16036
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
@@ -0,0 +1,50 @@
+package com.example.userservice.config;
+
+import com.example.userservice.repository.jpa.MemberRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@Configuration
+@RequiredArgsConstructor
+public class ApplicationConfig {
+
+    private final MemberRepository memberRepository;
+
+    @Bean
+    public UserDetailsService userDetailsService() {
+        return username -> memberRepository.findByUserId(username)
+                .map(member -> new org.springframework.security.core.userdetails.User(
+                        member.getUserId(),  // userId를 사용하도록 변경
+                        member.getPassword(),
+                        java.util.Collections.emptyList()
+                ))
+                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
+    }
+
+    @Bean
+    public AuthenticationProvider authenticationProvider() {
+        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
+        authProvider.setUserDetailsService(userDetailsService());
+        authProvider.setPasswordEncoder(passwordEncoder());
+        return authProvider;
+    }
+
+    @Bean
+    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
+        return config.getAuthenticationManager();
+    }
+
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/config/JpaConfig.java b/user-service/src/main/java/com/example/userservice/config/JpaConfig.java
new file mode 100644
index 0000000..d35f648
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/JpaConfig.java
@@ -0,0 +1,11 @@
+package com.example.userservice.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+@Configuration
+@EnableJpaRepositories(
+        basePackages = "com.example.userservice.repository.jpa"
+)
+public class JpaConfig {
+}
diff --git a/user-service/src/main/java/com/example/userservice/config/OAuth2AuthenticationSuccessHandler.java b/user-service/src/main/java/com/example/userservice/config/OAuth2AuthenticationSuccessHandler.java
new file mode 100644
index 0000000..d44b8a0
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/OAuth2AuthenticationSuccessHandler.java
@@ -0,0 +1,38 @@
+package com.example.userservice.security.oauth;
+
+import com.example.userservice.security.JwtUtil;
+import com.example.userservice.service.TokenService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.stereotype.Component;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {
+
+    private final JwtUtil jwtUtil;
+    private final TokenService tokenService;
+
+    @Override
+    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
+        // 1. 인증 성공했을 때 authentication 객체를 이용해서
+        // 2. accessToken, refreshToken 생성
+        String accessToken = jwtUtil.generateToken((UserDetails) authentication.getPrincipal());
+        String refreshToken = jwtUtil.generateRefreshToken((UserDetails) authentication.getPrincipal());
+
+        // 3. Redis에 Refresh Token 저장
+        String userId = authentication.getName();  // OAuth2 로그인 시 기본적으로 userId 같은 값
+        tokenService.saveRefreshToken(Long.parseLong(userId), refreshToken); // userId가 Long 타입이면
+
+        // 4. 응답에 AccessToken, RefreshToken 보내주기 (예시)
+        response.setContentType("application/json;charset=UTF-8");
+        response.getWriter().write("{\"accessToken\": \"" + accessToken + "\", \"refreshToken\": \"" + refreshToken + "\"}");
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/config/RedisConfig.java b/user-service/src/main/java/com/example/userservice/config/RedisConfig.java
new file mode 100644
index 0000000..b6e9eb7
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/RedisConfig.java
@@ -0,0 +1,11 @@
+package com.example.userservice.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
+
+@Configuration
+@EnableRedisRepositories(
+        basePackages = "com.example.userservice.repository.redis"
+)
+public class RedisConfig {
+}
diff --git a/user-service/src/main/java/com/example/userservice/config/RedisRepositoryConfig.java b/user-service/src/main/java/com/example/userservice/config/RedisRepositoryConfig.java
new file mode 100644
index 0000000..f952331
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/config/RedisRepositoryConfig.java
@@ -0,0 +1,28 @@
+package com.example.userservice.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+
+public class RedisRepositoryConfig {
+    @Value("${spring.data.redis.host}")
+    private String host;
+
+    @Value("${spring.data.redis.port}")
+    private int port;
+
+    @Value("${spring.data.redis.password}")
+    private String password;
+
+    @Bean
+    public RedisConnectionFactory redisConnectionFactory() {
+        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
+        redisStandaloneConfiguration.setHostName(host);
+        redisStandaloneConfiguration.setPassword(password);
+        redisStandaloneConfiguration.setPort(port);
+
+        return new LettuceConnectionFactory(redisStandaloneConfiguration);
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
index ff04de3..545a633 100644
--- a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
+++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
@@ -19,8 +19,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
     }
 
     //비밀번호 암호화 적용
-    @Bean
-    public BCryptPasswordEncoder passwordEncoder() {
-        return new BCryptPasswordEncoder();
-    }
+//    @Bean
+//    public BCryptPasswordEncoder passwordEncoder() {
+//        return new BCryptPasswordEncoder();
+//    }
 }
diff --git a/user-service/src/main/java/com/example/userservice/controller/AuthController.java b/user-service/src/main/java/com/example/userservice/controller/AuthController.java
new file mode 100644
index 0000000..9defdce
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/controller/AuthController.java
@@ -0,0 +1,51 @@
+package com.example.userservice.controller;
+
+import com.example.userservice.dto.AuthDto;
+import com.example.userservice.service.AuthService;
+import com.example.userservice.service.CookieUtil;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/auth")
+@RequiredArgsConstructor
+public class AuthController {
+
+    private final AuthService authService;
+    private final CookieUtil cookieUtil;
+
+    @PostMapping("/login")
+    public ResponseEntity login(
+            @RequestBody AuthDto.LoginRequest request,
+            HttpServletResponse response) {
+
+        // 디버깅 로그 출력
+        System.out.println("로그인 컨트롤러 호출: " + request.getUser_id());
+
+        AuthDto.LoginResponse loginResponse = authService.login(request);
+
+        // 쿠키에 토큰 저장
+        cookieUtil.addCookie(response, "access_token", loginResponse.getAccessToken(), cookieUtil.getDaysInSeconds(1));
+        cookieUtil.addCookie(response, "refresh_token", loginResponse.getRefreshToken(), cookieUtil.getDaysInSeconds(30));
+
+        return ResponseEntity.ok(loginResponse);
+    }
+
+    @PostMapping("/logout")
+    public ResponseEntity logout(
+            @RequestBody AuthDto.LogoutRequest request,
+            HttpServletResponse response) {
+
+        // 쿠키 삭제
+        cookieUtil.addCookie(response, "access_token", "", 0);
+        cookieUtil.addCookie(response, "refresh_token", "", 0);
+
+        // authService의 logout 메서드 호출
+        return ResponseEntity.ok(authService.logout(request));
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/AuthDto.java b/user-service/src/main/java/com/example/userservice/dto/AuthDto.java
new file mode 100644
index 0000000..9a04608
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/AuthDto.java
@@ -0,0 +1,55 @@
+package com.example.userservice.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+public class AuthDto {
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LoginRequest {
+        private String user_id;  // 로그인 시 사용자가 입력하는 ID
+        private String password;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LoginResponse {
+        private String accessToken;
+        private String refreshToken;
+        private String member_id;  // 내부 관리용 ID
+        private String user_id;    // 로그인용 ID
+        private String nickname;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LogoutRequest {
+        private String accessToken;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LogoutResponse {
+        private boolean success;
+        private String message;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class RefreshTokenRequest {
+        private String refreshToken;
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java
new file mode 100644
index 0000000..3648c46
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java
@@ -0,0 +1,44 @@
+package com.example.userservice.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+public class MemberDto {
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class MemberResponse {
+        private String user_id;
+        private String nickname;
+        private Short birth_year;
+        private LocalDateTime created_at;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class UpdateRequest {
+        private String nickname;
+        private Short birth_year;
+        private String current_password;
+        private String new_password;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class RegisterRequest {
+        private String user_id;
+        private String password;
+        private String nickname;
+        private Short birth_year;
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
index a648540..058318e 100644
--- a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
@@ -2,25 +2,18 @@
 
 import lombok.Getter;
 import lombok.Setter;
+import com.fasterxml.jackson.annotation.JsonProperty;
 
 @Getter
 @Setter
-
 public class MemberSign {
-    //private Long memberId;
+    @JsonProperty("user_id")  // JSON 필드 이름 매핑
+    private String userId;
+
     private String password;
     private String nickname;
     private String createTime;
 
-//    public Long getMemberId() {
-//        return memberId;
-//    }
-
-    public String getPassword() {
-        return password;
-    }
-
-    public String getNickname() {
-        return nickname;
-    }
-}
+    @JsonProperty("birth_year")  // JSON 필드 이름 매핑
+    private Short birthYear;
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/Member.java b/user-service/src/main/java/com/example/userservice/entity/Member.java
index a6e4b46..bf3a84e 100644
--- a/user-service/src/main/java/com/example/userservice/entity/Member.java
+++ b/user-service/src/main/java/com/example/userservice/entity/Member.java
@@ -2,31 +2,36 @@
 
 import jakarta.persistence.*;
 import lombok.AllArgsConstructor;
-import lombok.Getter;
+import lombok.Builder;
+import lombok.Data;
 import lombok.NoArgsConstructor;
-import lombok.Setter;
+
+import java.time.LocalDateTime;
 
 @Entity
-@Getter
-@Setter
-@NoArgsConstructor // JPA용 기본 생성자
-@AllArgsConstructor // 모든 필드로 객체를 편하게 생성
 @Table(name = "member")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
 public class Member {
-
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
-    @Column(name = "member_id",nullable = false)
+    @Column(name = "member_id", nullable = false)
     private Long memberId;
 
+    @Column(name = "user_id", nullable = true, unique = true)
+    private String userId;
+
     @Column(nullable = false)
     private String password;
 
     @Column
     private String nickname;
 
-    @Column(name = "create_at")
-    private String createAt;
-
+    @Column(name = "birth_year")
+    private Short birthYear;
 
-}
+    @Column(name = "created_at")
+    private LocalDateTime createdAt;
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
new file mode 100644
index 0000000..dc67ee4
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
@@ -0,0 +1,19 @@
+package com.example.userservice.entity;
+
+import lombok.*;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+
+@Getter
+@Setter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@RedisHash(value = "refresh_token", timeToLive = 60 * 60 * 24 * 7) // 7일 동안 유지
+public class RefreshToken {
+
+    @Id
+    private String id;  // Redis의 key (보통 memberId나 userId를 넣는다)
+
+    private String refreshToken;
+}
diff --git a/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java b/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java
new file mode 100644
index 0000000..19affd3
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java
@@ -0,0 +1,21 @@
+package com.example.userservice.entity;
+
+import jakarta.persistence.Id;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.data.redis.core.RedisHash;
+import org.springframework.data.redis.core.index.Indexed;
+
+@Getter
+@AllArgsConstructor
+@RedisHash(value = "token", timeToLive = 60 * 60 * 24 * 7) // 리프레시토큰과 expiretime 일치
+public class TokenRedis {
+
+    @Id
+    private String id;
+
+    @Indexed
+    private String accessToken;
+
+    private String refreshToken;
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java b/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
deleted file mode 100644
index 14955d4..0000000
--- a/user-service/src/main/java/com/example/userservice/repository/MemberRepository.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.example.userservice.repository;
-
-import com.example.userservice.entity.Member;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface MemberRepository extends JpaRepository {
-    boolean existsByMemberId(Long memberId);
-    boolean existsByNickname(String nickname);
-}
-
diff --git a/user-service/src/main/java/com/example/userservice/repository/TokenRedisRepository.java b/user-service/src/main/java/com/example/userservice/repository/TokenRedisRepository.java
new file mode 100644
index 0000000..30e8fe1
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/TokenRedisRepository.java
@@ -0,0 +1,11 @@
+package com.example.userservice.repository;
+
+import com.example.userservice.entity.TokenRedis;
+import org.springframework.data.repository.CrudRepository;
+
+import java.util.Optional;
+
+public interface TokenRedisRepository extends CrudRepository {
+
+    Optional findByAccessToken(String accessToken); // AccessToken으로 찾아내기
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java b/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java
new file mode 100644
index 0000000..cea05f5
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java
@@ -0,0 +1,16 @@
+package com.example.userservice.repository.jpa;
+
+import com.example.userservice.entity.Member;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface MemberRepository extends JpaRepository {
+    boolean existsByMemberId(Long memberId);
+    boolean existsByUserId(String userId);
+    boolean existsByNickname(String nickname);
+    Optional findByNickname(String nickname);
+    Optional findByUserId(String userId);
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java b/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java
new file mode 100644
index 0000000..f871368
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java
@@ -0,0 +1,13 @@
+package com.example.userservice.repository.redis;
+
+import com.example.userservice.entity.RefreshToken;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface RefreshTokenRepository extends JpaRepository {
+    Optional findByUserId(Long userId);
+    Optional findByRefreshToken(String refreshToken);
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..b917cea
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
@@ -0,0 +1,103 @@
+package com.example.userservice.security;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+    private final JwtUtil jwtUtil;
+    private final UserDetailsService userDetailsService;
+
+    // 필터링하지 않을 URL 패턴 목록
+    private final List excludedPaths = Arrays.asList("/auth/signup", "/auth/login", "/auth/logout");
+
+    @Override
+    protected void doFilterInternal(
+            HttpServletRequest request,
+            HttpServletResponse response,
+            FilterChain filterChain
+    ) throws ServletException, IOException {
+
+        // 인증이 필요없는 경로는 필터를 적용하지 않음
+        String requestPath = request.getServletPath();
+        if (shouldNotFilter(request)) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        // Authorization 헤더에서 토큰 추출 시도
+        final String authHeader = request.getHeader("Authorization");
+        String jwt = null;
+
+        // Authorization 헤더가 있으면 해당 토큰 사용
+        if (authHeader != null && authHeader.startsWith("Bearer ")) {
+            jwt = authHeader.substring(7);
+        }
+        // 아니면 쿠키에서 토큰 찾기
+        else {
+            Cookie[] cookies = request.getCookies();
+            if (cookies != null) {
+                for (Cookie cookie : cookies) {
+                    if ("access_token".equals(cookie.getName())) {
+                        jwt = cookie.getValue();
+                        break;
+                    }
+                }
+            }
+        }
+
+        // 토큰이 없으면 다음 필터로 이동
+        if (jwt == null) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        try {
+            final String username = jwtUtil.extractUsername(jwt);
+
+            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
+
+                if (jwtUtil.validateToken(jwt, userDetails)) {
+                    UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
+                            userDetails,
+                            null,
+                            userDetails.getAuthorities()
+                    );
+                    authToken.setDetails(
+                            new WebAuthenticationDetailsSource().buildDetails(request)
+                    );
+                    SecurityContextHolder.getContext().setAuthentication(authToken);
+                }
+            }
+        } catch (Exception e) {
+            // 토큰 검증 과정에서 오류가 발생하면 로그만 남기고 계속 진행
+            logger.error("JWT 토큰 검증 실패: " + e.getMessage());
+        }
+
+        filterChain.doFilter(request, response);
+    }
+
+    @Override
+    protected boolean shouldNotFilter(HttpServletRequest request) {
+        String path = request.getServletPath();
+        return excludedPaths.stream().anyMatch(path::startsWith);
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/security/JwtUtil.java b/user-service/src/main/java/com/example/userservice/security/JwtUtil.java
new file mode 100644
index 0000000..c582096
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/security/JwtUtil.java
@@ -0,0 +1,126 @@
+package com.example.userservice.security;
+
+import io.github.cdimascio.dotenv.Dotenv;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.security.Keys;
+import jakarta.annotation.PostConstruct;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+@Component
+public class JwtUtil {
+
+    private SecretKey secretKey;
+    private Long accessTokenExpiration; // 액세스 토큰 만료 시간 (ms)
+    private Long refreshTokenExpiration; // 리프레시 토큰 만료 시간 (ms)
+
+    @PostConstruct
+    public void init() {
+        Dotenv dotenv = Dotenv.configure()
+                .ignoreIfMalformed()
+                .ignoreIfMissing()
+                .load();
+
+        String jwtSecret = dotenv.get("JWT_SECRET_KEY");
+        if (jwtSecret == null || jwtSecret.isEmpty()) {
+            throw new IllegalStateException("JWT_SECRET_KEY 환경변수가 설정되지 않았습니다!");
+        }
+
+        this.secretKey = Keys.hmacShaKeyFor(jwtSecret.getBytes());
+        this.accessTokenExpiration = Long.parseLong(dotenv.get("JWT_EXPIRATION", "1800000")); // 기본값 30분
+        this.refreshTokenExpiration = Long.parseLong(dotenv.get("JWT_REFRESH_EXPIRATION", "604800000")); // 기본값 7일
+    }
+
+    // 사용자 이름 추출
+    public String extractUsername(String token) {
+        return extractClaim(token, Claims::getSubject);
+    }
+
+    // 만료 시간 추출
+    public Date extractExpiration(String token) {
+        return extractClaim(token, Claims::getExpiration);
+    }
+
+    // 클레임 추출
+    public  T extractClaim(String token, Function claimsResolver) {
+        final Claims claims = extractAllClaims(token);
+        return claimsResolver.apply(claims);
+    }
+
+    // 모든 클레임 추출
+    private Claims extractAllClaims(String token) {
+        return Jwts
+                .parser()
+                .verifyWith(secretKey)
+                .build()
+                .parseSignedClaims(token)
+                .getPayload();
+    }
+
+    // 토큰 만료 확인
+    private Boolean isTokenExpired(String token) {
+        return extractExpiration(token).before(new Date());
+    }
+
+    // 액세스 토큰 생성
+    public String generateToken(UserDetails userDetails) {
+        Map claims = new HashMap<>();
+        return createToken(claims, userDetails.getUsername(), accessTokenExpiration);
+    }
+
+    // 액세스 토큰 생성 (nickname 직접 전달)
+    public String generateAccessToken(String nickname) {
+        Map claims = new HashMap<>();
+        return createToken(claims, nickname, accessTokenExpiration);
+    }
+
+    // 리프레시 토큰 생성
+    public String generateRefreshToken(UserDetails userDetails) {
+        Map claims = new HashMap<>();
+        return createToken(claims, userDetails.getUsername(), refreshTokenExpiration);
+    }
+
+    // 리프레시 토큰 생성 (nickname 직접 전달)
+    public String generateRefreshToken(String nickname) {
+        Map claims = new HashMap<>();
+        return createToken(claims, nickname, refreshTokenExpiration);
+    }
+
+    // 토큰 생성 메소드
+    private String createToken(Map claims, String subject, Long expirationTime) {
+        return Jwts.builder()
+                .claims(claims)
+                .subject(subject)
+                .issuedAt(new Date(System.currentTimeMillis()))
+                .expiration(new Date(System.currentTimeMillis() + expirationTime))
+                .signWith(secretKey, SignatureAlgorithm.HS512)
+                .compact();
+    }
+
+    // 토큰 유효성 검증 (UserDetails와 비교)
+    public Boolean validateToken(String token, UserDetails userDetails) {
+        final String username = extractUsername(token);
+        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
+    }
+
+    // 토큰 유효성만 검사 (사용자 정보 없이)
+    public Boolean validateToken(String token) {
+        try {
+            Jwts.parser()
+                    .verifyWith(secretKey)
+                    .build()
+                    .parseSignedClaims(token);
+            return !isTokenExpired(token);
+        } catch (Exception e) {
+            return false;
+        }
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/AuthService.java b/user-service/src/main/java/com/example/userservice/service/AuthService.java
new file mode 100644
index 0000000..cd4028f
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/AuthService.java
@@ -0,0 +1,78 @@
+package com.example.userservice.service;
+
+import com.example.userservice.dto.AuthDto;
+import com.example.userservice.entity.Member;
+import com.example.userservice.repository.jpa.MemberRepository;
+import com.example.userservice.security.JwtUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class AuthService {
+
+    private final MemberRepository memberRepository;
+    private final JwtUtil jwtUtil;
+    private final AuthenticationManager authenticationManager;
+    private final TokenService tokenService;
+
+    public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
+        // 디버깅 로그
+        System.out.println("로그인 시도: " + request.getUser_id());
+
+        // 1. 사용자 조회 (userId로 조회)
+        Optional memberOptional = memberRepository.findByUserId(request.getUser_id());
+        if (memberOptional.isEmpty()) {
+            System.out.println("사용자를 찾을 수 없음: " + request.getUser_id());
+            throw new RuntimeException("User not found");
+        }
+        Member member = memberOptional.get();
+        System.out.println("사용자 찾음: ID=" + member.getMemberId() + ", UserID=" + member.getUserId());
+
+        // 2. 인증 시도
+        try {
+            authenticationManager.authenticate(
+                    new UsernamePasswordAuthenticationToken(
+                            request.getUser_id(),
+                            request.getPassword()
+                    )
+            );
+            System.out.println("인증 성공");
+        } catch (Exception e) {
+            System.out.println("인증 실패: " + e.getMessage());
+            throw e;
+        }
+
+        // 3. 토큰 생성
+        UserDetails userDetails = new User(member.getUserId(), member.getPassword(), Collections.emptyList());
+        String accessToken = jwtUtil.generateToken(userDetails);
+        String refreshToken = jwtUtil.generateRefreshToken(userDetails);
+
+        // 4. Refresh Token을 Redis에 저장
+        tokenService.saveRefreshToken(member.getMemberId(), refreshToken);
+
+        // 5. 결과 리턴
+        return AuthDto.LoginResponse.builder()
+                .accessToken(accessToken)
+                .refreshToken(refreshToken)
+                .member_id(String.valueOf(member.getMemberId()))
+                .user_id(member.getUserId())
+                .nickname(member.getNickname())
+                .build();
+    }
+
+    public AuthDto.LogoutResponse logout(AuthDto.LogoutRequest request) {
+        // 로그아웃 로직도 나중에 redis에서 refresh token 삭제하는 걸 추가할 수 있음
+        return AuthDto.LogoutResponse.builder()
+                .success(true)
+                .message("Logout successful")
+                .build();
+    }
+}
diff --git a/user-service/src/main/java/com/example/userservice/service/CookieUtil.java b/user-service/src/main/java/com/example/userservice/service/CookieUtil.java
new file mode 100644
index 0000000..2a1ab1f
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/CookieUtil.java
@@ -0,0 +1,21 @@
+package com.example.userservice.service;
+
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Service;
+
+@Service
+public class CookieUtil {
+
+    public void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
+        Cookie cookie = new Cookie(name, value);
+        cookie.setPath("/");
+        cookie.setHttpOnly(true);
+        cookie.setMaxAge(maxAge);
+        response.addCookie(cookie);
+    }
+
+    public int getDaysInSeconds(int days) {
+        return days * 24 * 60 * 60;
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java
index 6f2f58f..cba58f6 100644
--- a/user-service/src/main/java/com/example/userservice/service/MemberService.java
+++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java
@@ -2,9 +2,9 @@
 
 import com.example.userservice.dto.MemberSign;
 import com.example.userservice.entity.Member;
-import com.example.userservice.repository.MemberRepository;
+import com.example.userservice.repository.jpa.MemberRepository;
 import lombok.RequiredArgsConstructor;
-import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
@@ -13,22 +13,26 @@
 @RequiredArgsConstructor
 public class MemberService {
     private final MemberRepository memberRepository;
-    private final BCryptPasswordEncoder passwordEncoder;
+    private final PasswordEncoder passwordEncoder;
 
     public Member registerAdmin(MemberSign dto) {
-//        if (memberRepository.existsByMemberId(dto.getMemberId())) {
-//            throw new IllegalStateException("이미 존재하는 ID입니다.");
-//        }
+        // userId 중복 체크
+        if (memberRepository.existsByUserId(dto.getUserId())) {
+            throw new IllegalStateException("이미 존재하는 사용자 ID입니다.");
+        }
+
         if (memberRepository.existsByNickname(dto.getNickname())) {
             throw new IllegalStateException("이미 존재하는 닉네임입니다.");
         }
 
-        Member member = new Member();
-        member.setPassword(passwordEncoder.encode(dto.getPassword()));
-        member.setNickname(dto.getNickname());
-        member.setCreateAt(String.valueOf(LocalDateTime.now()));
+        Member member = Member.builder()
+                .userId(dto.getUserId())  // userId 설정
+                .password(passwordEncoder.encode(dto.getPassword()))
+                .nickname(dto.getNickname())
+                .birthYear(dto.getBirthYear())
+                .createdAt(LocalDateTime.now())
+                .build();
 
-        Member savedMember = memberRepository.save(member);
-        return savedMember; // 저장된 객체 리턴
+        return memberRepository.save(member);
     }
-}
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/TokenService.java b/user-service/src/main/java/com/example/userservice/service/TokenService.java
new file mode 100644
index 0000000..648e9b6
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/service/TokenService.java
@@ -0,0 +1,25 @@
+package com.example.userservice.service;
+
+import com.example.userservice.entity.RefreshToken;
+import com.example.userservice.repository.redis.RefreshTokenRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class TokenService {
+
+    private final RefreshTokenRepository refreshTokenRepository;
+
+    public void saveRefreshToken(Long memberId, String refreshToken) {
+        RefreshToken token = RefreshToken.builder()
+                .id(String.valueOf(memberId))  // 여기!! id로 memberId를 문자열로 저장
+                .refreshToken(refreshToken)
+                .build();
+        refreshTokenRepository.save(token);
+    }
+
+    public void deleteRefreshToken(Long memberId) {
+        refreshTokenRepository.deleteById((memberId));
+    }
+}
diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties
index a2c035e..d99579a 100644
--- a/user-service/src/main/resources/application.properties
+++ b/user-service/src/main/resources/application.properties
@@ -1,11 +1,19 @@
 spring.application.name=user-service
 
+server.port=8082
+
 spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db
 spring.datasource.username=root
 spring.datasource.password=admin
 spring.datasource.driver-class-name=org.postgresql.Driver
 spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
-spring.jpa.hibernate.ddl-auto=create
+spring.jpa.hibernate.ddl-auto=update
+
+spring.data.redis.host=localhost
+spring.data.redis.port=6379
+spring.data.redis.password=1234
 
 
 jwt.secret=${JWT_SECRET_KEY}
+jwt.expiration=86400000
+jwt.refresh-expiration=2592000000
\ No newline at end of file

From a791736f5c659ac136ce3089237b2dad58e64285 Mon Sep 17 00:00:00 2001
From: suminmmmmmmm 
Date: Mon, 28 Apr 2025 16:28:42 +0900
Subject: [PATCH 158/361] =?UTF-8?q?Refactor:=20redis=EC=97=90=20=EB=A6=AC?=
 =?UTF-8?q?=ED=94=84=EB=A0=88=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EC=A0=80?=
 =?UTF-8?q?=EC=9E=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md               | 6 +++++-
 user-service/.gitignore | 3 +++
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index c7a05ff..de6e3f7 100644
--- a/README.md
+++ b/README.md
@@ -33,4 +33,8 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다.
 5. 접속한 후 \dt 로 테이블 존재 확인 가능, select 문으로 데이터 확인 가능
 + select * from area; 로 결과를 본 후, q 를 눌러야 다시 명령창으로 돌아갈 수 있음
 
-
\ No newline at end of file
+
+
+토큰 redis에 저장
+- 
+
diff --git a/user-service/.gitignore b/user-service/.gitignore
index c2065bc..eb9988c 100644
--- a/user-service/.gitignore
+++ b/user-service/.gitignore
@@ -35,3 +35,6 @@ out/
 
 ### VS Code ###
 .vscode/
+
+.env
+

From 7f2519a7593a0c3fe0671aae4c65e6ffface96b5 Mon Sep 17 00:00:00 2001
From: suminmmmmmmm 
Date: Mon, 28 Apr 2025 16:28:42 +0900
Subject: [PATCH 159/361] =?UTF-8?q?Refactor:=20redis=EC=97=90=20=EB=A6=AC?=
 =?UTF-8?q?=ED=94=84=EB=A0=88=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EC=A0=80?=
 =?UTF-8?q?=EC=9E=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md               | 6 +++++-
 user-service/.gitignore | 3 +++
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index c7a05ff..de6e3f7 100644
--- a/README.md
+++ b/README.md
@@ -33,4 +33,8 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다.
 5. 접속한 후 \dt 로 테이블 존재 확인 가능, select 문으로 데이터 확인 가능
 + select * from area; 로 결과를 본 후, q 를 눌러야 다시 명령창으로 돌아갈 수 있음
 
-
\ No newline at end of file
+
+
+토큰 redis에 저장
+- 
+
diff --git a/user-service/.gitignore b/user-service/.gitignore
index c2065bc..eb9988c 100644
--- a/user-service/.gitignore
+++ b/user-service/.gitignore
@@ -35,3 +35,6 @@ out/
 
 ### VS Code ###
 .vscode/
+
+.env
+

From ac81a1cec3fa9866a90106be98f5d1b0986b5ce5 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 16:50:04 +0900
Subject: [PATCH 160/361] =?UTF-8?q?area.sql=20=EC=82=AD=EC=A0=9C=20?=
 =?UTF-8?q?=EB=B0=8F=20=EC=B6=94=EC=A0=81=20=EC=A4=91=EB=8B=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../placeservice/controller/AccommodationController.java        | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
index 0cf13ea..84594e4 100644
--- a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
+++ b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
@@ -32,7 +32,7 @@ public AccommodationDto getAccommodationById(@PathVariable Long accommodation_id
         return accommodationService.getAccommodationById(accommodation_id);
     }
 
-    @GetMapping("gu/{gu}")
+    @GetMapping("/gu/{gu}")
     public List getAccommodationByGu(@PathVariable String gu) throws IOException {
         return accommodationService.getAccommodationByGu(gu);
     }

From b66078334231b5cf33fedabcdc99807a91e0d16b Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 16:50:04 +0900
Subject: [PATCH 161/361] =?UTF-8?q?area.sql=20=EC=82=AD=EC=A0=9C=20?=
 =?UTF-8?q?=EB=B0=8F=20=EC=B6=94=EC=A0=81=20=EC=A4=91=EB=8B=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../placeservice/controller/AccommodationController.java        | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
index 0cf13ea..84594e4 100644
--- a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
+++ b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java
@@ -32,7 +32,7 @@ public AccommodationDto getAccommodationById(@PathVariable Long accommodation_id
         return accommodationService.getAccommodationById(accommodation_id);
     }
 
-    @GetMapping("gu/{gu}")
+    @GetMapping("/gu/{gu}")
     public List getAccommodationByGu(@PathVariable String gu) throws IOException {
         return accommodationService.getAccommodationByGu(gu);
     }

From ac3f2de60f487ec784ad09194628d4f041a08abf Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 16:51:45 +0900
Subject: [PATCH 162/361] =?UTF-8?q?area.sql=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 area.sql | 116 -------------------------------------------------------
 1 file changed, 116 deletions(-)
 delete mode 100644 area.sql

diff --git a/area.sql b/area.sql
deleted file mode 100644
index d2119f5..0000000
--- a/area.sql
+++ /dev/null
@@ -1,116 +0,0 @@
-INSERT INTO area (area_id, name, name_eng, category, x, y) VALUES
-('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705),
-('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576),
-('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864),
-('POI004', '이태원 관광특구', 'Itaewon Special Tourist Zone', '관광특구', 37.53475718815058, 126.99591956660342),
-('POI005', '잠실 관광특구', 'Jamsil Special Tourist Zone', '관광특구', 37.51868923685722, 127.11694379878114),
-('POI006', '종로·청계 관광특구', 'Jongno Cheonggye Special Toruist Zone', '관광특구', 37.56984229024498, 126.99866678943675),
-('POI007', '홍대 관광특구', 'HongDae Culture & Arts Special Tourist Zone', '관광특구', 37.55506170187393, 126.92159115672915),
-('POI008', '경복궁', 'Gyeongbokgung Palace', '고궁·문화유산', 37.5809150911806, 126.97764726625209),
-('POI009', '광화문·덕수궁', 'Gwanghwamun & Deoksugung Palace', '고궁·문화유산', 37.56847081447724, 126.97620594802834),
-('POI010', '보신각', 'Bosingak', '고궁·문화유산', 37.57089797616037, 126.98337607511172),
-('POI011', '서울 암사동 유적', 'Amsa Prehistoric Settlement Site', '고궁·문화유산', 37.56102809827398, 127.13078491934499),
-('POI012', '창덕궁·종묘', 'Changdeokgung Palace & Jongmyo Shrine', '고궁·문화유산', 37.5795304952381, 126.99416802285715),
-('POI013', '가산디지털단지역', 'Gasan Digital Complex station', '인구밀집지역', 37.48267268376068, 126.88014526210826),
-('POI014', '강남역', 'Gangnam station', '인구밀집지역', 37.50019794543511, 127.02822145618981),
-('POI015', '건대입구역', 'Konkuk University station', '인구밀집지역', 37.540005312302846, 127.06718185804415),
-('POI016', '고덕역', 'Godeok station', '인구밀집지역', 37.553377141111774, 127.15503341723486),
-('POI017', '고속터미널역', 'Express Bus Terminal station', '인구밀집지역', 37.50472316731823, 127.00542276570697),
-('POI018', '교대역', 'Seoul National University of Education station', '인구밀집지역', 37.49185444557083, 127.01456630812736),
-('POI019', '구로디지털단지역', 'Guro Digital Complex station', '인구밀집지역', 37.48349491938897, 126.89557915984915),
-('POI020', '구로역', 'Guro station', '인구밀집지역', 37.501517014484925, 126.88285531403136),
-('POI021', '군자역', 'Gunja station', '인구밀집지역', 37.555916439038526, 127.08008933356267),
-('POI022', '남구로역', 'Namguro station', '인구밀집지역', 37.484184300878496, 126.88539683418863),
-('POI023', '대림역', 'Daerim station', '인구밀집지역', 37.492818237753816, 126.89567307890546),
-('POI024', '동대문역', 'Dongdaemun station', '인구밀집지역', 37.57127393968462, 127.0096854515538),
-('POI025', '뚝섬역', 'Ttukseom station', '인구밀집지역', 37.548249670448335, 127.04603079475764),
-('POI026', '미아사거리역', 'Miasageori station', '인구밀집지역', 37.61244898963818, 127.0305835226811),
-('POI027', '발산역', 'Balsan station', '인구밀집지역', 37.55796811527854, 126.84179723439551),
-('POI028', '북한산우이역', 'Bukhansan Ui station', '인구밀집지역', 37.66281154490492, 127.01070580250341),
-('POI029', '사당역', 'Sadang station', '인구밀집지역', 37.47635049189678, 126.98129519152415),
-('POI030', '삼각지역', 'Samgakji station', '인구밀집지역', 37.535323919468816, 126.97392611189508),
-('POI031', '서울대입구역', 'Seoul National University station', '인구밀집지역', 37.48060991955129, 126.95360407634334),
-('POI032', '서울식물원·마곡나루역', 'Seoul Botanic Park·Magongnaru station', '인구밀집지역', 37.56745047572558, 126.82986126533689),
-('POI033', '서울역', 'Seoul station', '인구밀집지역', 37.55662336923077, 126.97216878769233),
-('POI034', '선릉역', 'Seolleung station', '인구밀집지역', 37.50584166753116, 127.0489100692203),
-('POI035', '성신여대입구역', 'Sungshin Women''s University station', '인구밀집지역', 37.59262801654558, 127.01658994286346),
-('POI036', '수유역', 'Suyu station', '인구밀집지역', 37.641215514705884, 127.02565393627451),
-('POI037', '신논현역·논현역', 'Sinnonhyeon·Nonhyeon station', '인구밀집지역', 37.50848054554439, 127.02291493850254),
-('POI038', '신도림역', 'Sindorim station', '인구밀집지역', 37.50941493666961, 126.89096014067083),
-('POI039', '신림역', 'Sillim station', '인구밀집지역', 37.48396968904173, 126.92987028995233),
-('POI040', '신촌·이대역', 'Sinchon·Ewha Womans University station', '인구밀집지역', 37.55564146783625, 126.93761215497075),
-('POI041', '양재역', 'Yangjae station', '인구밀집지역', 37.48451422353994, 127.03551115644439),
-('POI042', '역삼역', 'Yeoksam station', '인구밀집지역', 37.50011177778084, 127.03721339389585),
-('POI043', '연신내역', 'Yeonsinnae station', '인구밀집지역', 37.61903547457627, 126.92180184322034),
-('POI044', '오목교역·목동운동장', 'Omokgyo·Mok-dong Stadium station', '인구밀집지역', 37.53174788866626, 126.87924768246015),
-('POI045', '왕십리역', 'Wangsimni station', '인구밀집지역', 37.561636745463275, 127.03857508984515),
-('POI046', '용산역', 'Yongsan station', '인구밀집지역', 37.53059674809017, 126.95953798376128),
-('POI047', '이태원역', 'Itaewon station', '인구밀집지역', 37.534847968613946, 126.99261390234051),
-('POI048', '장지역', 'Jangji station', '인구밀집지역', 37.47861018962738, 127.12424263037641),
-('POI049', '장한평역', 'Janghanpyeong station', '인구밀집지역', 37.562849061361526, 127.06235115915185),
-('POI050', '천호역', 'Cheonho station', '인구밀집지역', 37.53921065146112, 127.12470374198884),
-('POI051', '총신대입구(이수)역', 'Chongshin University(Isu) station', '인구밀집지역', 37.48677821828824, 126.9807539648491),
-('POI052', '충정로역', 'Chungjeongno station', '인구밀집지역', 37.559524839900476, 126.9642059673645),
-('POI053', '합정역', 'Hapjeong station', '인구밀집지역', 37.54857307399022, 126.91084543497917),
-('POI054', '혜화역', 'Hyehwa station', '인구밀집지역', 37.58237426286851, 127.0014404389915),
-('POI055', '홍대입구역(2호선)', 'Hongik University station(Line 2)', '인구밀집지역', 37.5560688250626, 126.92329766229786),
-('POI056', '회기역', 'Hoegi station', '인구밀집지역', 37.590538258503685, 127.05700039395965),
-('POI057', '4·19 카페거리', '4·19 Cafe street', '발달상권', 37.64481185893745, 127.00739701908337),
-('POI058', '가락시장', 'Garak Market', '발달상권', 37.49445476721721, 127.11054976163454),
-('POI059', '가로수길', 'Garosu-gil', '발달상권', 37.522662622516556, 127.02543305960263),
-('POI060', '광장(전통)시장', 'Gwangjang(Traditional) Market', '발달상권', 37.569817036518465, 127.00014893169192),
-('POI061', '김포공항', 'Gimpo Airport', '발달상권', 37.56228914604604, 126.80289965159135),
-('POI062', '낙산공원·이화마을', 'Naksan Park·Ihwa Village', '발달상권', 37.580295002146926, 127.00692469978675),
-('POI063', '노량진', 'Noryangjin', '발달상권', 37.513690699957316, 126.94907070192423),
-('POI064', '덕수궁길·정동길', 'Deoksugung-gil·Jeongdong-gil', '발달상권', 37.5659744042236, 126.97244482634493),
-('POI065', '방배역 먹자골목', 'Bangbae food alley', '발달상권', 37.48234102283986, 126.99597969571558),
-('POI066', '북촌한옥마을', 'Bukchon Hanok Village', '발달상권', 37.58425649416171, 126.98386049085175),
-('POI067', '서촌', 'Seochon', '발달상권', 37.58018833392947, 126.9666234325548),
-('POI068', '성수카페거리', 'Seongsu Cafe Street', '발달상권', 37.54227657813654, 127.05560237183015),
-('POI069', '수유리 먹자골목', 'Suyuri food alley', '발달상권', 37.64270823057644, 127.02733855833522),
-('POI070', '쌍문동 맛집거리', 'Ssangmun-dong restaurant street', '발달상권', 37.6477285443038, 127.03242528481013),
-('POI071', '압구정로데오거리', 'Apgujeong Rodeo Street', '발달상권', 37.52560441860466, 127.04040171511626),
-('POI072', '여의도', 'Yeouido', '발달상권', 37.52549260943663, 126.92338937129487),
-('POI073', '연남동', 'Yeonnam-dong', '발달상권', 37.561426426204264, 126.92406273242905),
-('POI074', '영등포 타임스퀘어', 'Yeongdeungpo Time square', '발달상권', 37.51653179760669, 126.90523931614634),
-('POI075', '외대앞', 'Hankuk University of Foreign Studies', '발달상권', 37.59442412843283, 127.06153833102397),
-('POI076', '용리단길', 'Yongnidan-gil', '발달상권', 37.5311056439386, 126.97147359358237),
-('POI077', '이태원 앤틱가구거리', 'Itaewon Antiques street', '발달상권', 37.53187270743738, 126.99343376513602),
-('POI078', '인사동·익선동', 'Insa-dong·Ikseon-dong', '발달상권', 37.5741577489609, 126.98736374162822),
-('POI079', '창동 신경제 중심지', 'Changdong New Economic Center', '발달상권', 37.656167961335676, 127.05328808787348),
-('POI080', '청담동 명품거리', 'Cheongdam-dong Luxury Fashion street', '발달상권', 37.52645537152884, 127.04354058968364),
-('POI081', '청량리 제기동 일대 전통시장', 'Traditional market in Cheongnyangni Jegi-dong', '발달상권', 37.579977738703214, 127.0382790126348),
-('POI082', '해방촌·경리단길', 'Haebangchon·Gyeongnidan-gil', '발달상권', 37.54143829714048, 126.98836800969178),
-('POI083', 'DDP(동대문디자인플라자)', 'DDP(Dongdaemun Design Plaza)', '발달상권', 37.56654201564661, 127.00993045294622),
-('POI084', 'DMC(디지털미디어시티)', 'DMC(Digital Media City)', '발달상권', 37.577407110655734, 126.8936691147541),
-('POI085', '강서한강공원', 'Gangseo Hangang Park', '공원', 37.5858708295553, 126.82002478578644),
-('POI086', '고척돔', 'Gocheok Dome', '공원', 37.49776790791694, 126.86713336195083),
-('POI087', '광나루한강공원', 'Gwangnaru Hangang Park', '공원', 37.550866089267345, 127.12370647922764),
-('POI088', '광화문광장', 'Gwanghwamun Square', '공원', 37.57505062349029, 126.97680679071502),
-('POI089', '국립중앙박물관·용산가족공원', 'The National Museum of Korea·Yongsan Family Park', '공원', 37.52202119313341, 126.9824679850178),
-('POI090', '난지한강공원', 'Nanji Hangang Park', '공원', 37.566316740172454, 126.87770001597329),
-('POI091', '남산공원', 'Namsan Park', '공원', 37.555070650369004, 126.99365012730628),
-('POI092', '노들섬', 'Nodeul island', '공원', 37.517556872131706, 126.95871397855296),
-('POI093', '뚝섬한강공원', 'Ttukseom Hangang Park', '공원', 37.52882503002481, 127.07460742502332),
-('POI094', '망원한강공원', 'Mangwon Hangang Park', '공원', 37.55480087656761, 126.89823749216892),
-('POI095', '반포한강공원', 'Banpo Hangang Park', '공원', 37.51065462735107, 126.99457550649858),
-('POI096', '북서울꿈의숲', 'Dream Forest', '공원', 37.62244979881656, 127.04069568441815),
-('POI097', '불광천', 'Bulgwangcheon River', '공원', 37.58940646530798, 126.91194122899687),
-('POI098', '서리풀공원·몽마르뜨공원', 'Seoripul Park·Montmartre Park', '공원', 37.49022054658995, 127.00173655775917),
-('POI100', '서울대공원', 'Seoul Grand Park', '공원', 37.42839607329216, 127.01863790236284),
-('POI101', '서울숲공원', 'Seoul Forest', '공원', 37.54402680167043, 127.03673083133076),
-('POI099', '서울광장', 'Seoul Plaza', '공원', 37.5655010296151, 126.97798702124584),
-('POI102', '아차산', 'Achasan', '공원', 37.56307021845223, 127.10047918048643),
-('POI103', '양화한강공원', 'Yanghwa Hangang Park', '공원', 37.54029653900909, 126.89963558394624),
-('POI104', '어린이대공원', 'Children''s Grand Park', '공원', 37.54957100980681, 127.0824313180458),
-('POI105', '여의도한강공원', 'Yeouido Hangang Park', '공원', 37.52868909088513, 126.92627587984478),
-('POI106', '월드컵공원', 'World Cup Park', '공원', 37.5660391150387, 126.88740201899091),
-('POI107', '응봉산', 'Eungbongsan', '공원', 37.54797346368485, 127.02904643771303),
-('POI108', '이촌한강공원', 'Ichon Hangang Park', '공원', 37.52139035897837, 126.96356631928725),
-('POI109', '잠실종합운동장', 'Jamsil (Seoul) Sports Complex', '공원', 37.51610665876664, 127.07230325047283),
-('POI110', '잠실한강공원', 'Jamsil Hangang Park', '공원', 37.51981181577704, 127.08507449607517),
-('POI111', '잠원한강공원', 'Jamwon Hangang Park', '공원', 37.52336245003122, 127.01397131593632),
-('POI112', '청계산', 'Cheonggyesan', '공원', 37.443365424959026, 127.0469697170313),
-('POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264),
-('POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616),
-('POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821);
\ No newline at end of file

From 3937f139916c020bd1ad959373391a803342353b Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 16:51:45 +0900
Subject: [PATCH 163/361] =?UTF-8?q?area.sql=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 area.sql | 116 -------------------------------------------------------
 1 file changed, 116 deletions(-)
 delete mode 100644 area.sql

diff --git a/area.sql b/area.sql
deleted file mode 100644
index d2119f5..0000000
--- a/area.sql
+++ /dev/null
@@ -1,116 +0,0 @@
-INSERT INTO area (area_id, name, name_eng, category, x, y) VALUES
-('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705),
-('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576),
-('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864),
-('POI004', '이태원 관광특구', 'Itaewon Special Tourist Zone', '관광특구', 37.53475718815058, 126.99591956660342),
-('POI005', '잠실 관광특구', 'Jamsil Special Tourist Zone', '관광특구', 37.51868923685722, 127.11694379878114),
-('POI006', '종로·청계 관광특구', 'Jongno Cheonggye Special Toruist Zone', '관광특구', 37.56984229024498, 126.99866678943675),
-('POI007', '홍대 관광특구', 'HongDae Culture & Arts Special Tourist Zone', '관광특구', 37.55506170187393, 126.92159115672915),
-('POI008', '경복궁', 'Gyeongbokgung Palace', '고궁·문화유산', 37.5809150911806, 126.97764726625209),
-('POI009', '광화문·덕수궁', 'Gwanghwamun & Deoksugung Palace', '고궁·문화유산', 37.56847081447724, 126.97620594802834),
-('POI010', '보신각', 'Bosingak', '고궁·문화유산', 37.57089797616037, 126.98337607511172),
-('POI011', '서울 암사동 유적', 'Amsa Prehistoric Settlement Site', '고궁·문화유산', 37.56102809827398, 127.13078491934499),
-('POI012', '창덕궁·종묘', 'Changdeokgung Palace & Jongmyo Shrine', '고궁·문화유산', 37.5795304952381, 126.99416802285715),
-('POI013', '가산디지털단지역', 'Gasan Digital Complex station', '인구밀집지역', 37.48267268376068, 126.88014526210826),
-('POI014', '강남역', 'Gangnam station', '인구밀집지역', 37.50019794543511, 127.02822145618981),
-('POI015', '건대입구역', 'Konkuk University station', '인구밀집지역', 37.540005312302846, 127.06718185804415),
-('POI016', '고덕역', 'Godeok station', '인구밀집지역', 37.553377141111774, 127.15503341723486),
-('POI017', '고속터미널역', 'Express Bus Terminal station', '인구밀집지역', 37.50472316731823, 127.00542276570697),
-('POI018', '교대역', 'Seoul National University of Education station', '인구밀집지역', 37.49185444557083, 127.01456630812736),
-('POI019', '구로디지털단지역', 'Guro Digital Complex station', '인구밀집지역', 37.48349491938897, 126.89557915984915),
-('POI020', '구로역', 'Guro station', '인구밀집지역', 37.501517014484925, 126.88285531403136),
-('POI021', '군자역', 'Gunja station', '인구밀집지역', 37.555916439038526, 127.08008933356267),
-('POI022', '남구로역', 'Namguro station', '인구밀집지역', 37.484184300878496, 126.88539683418863),
-('POI023', '대림역', 'Daerim station', '인구밀집지역', 37.492818237753816, 126.89567307890546),
-('POI024', '동대문역', 'Dongdaemun station', '인구밀집지역', 37.57127393968462, 127.0096854515538),
-('POI025', '뚝섬역', 'Ttukseom station', '인구밀집지역', 37.548249670448335, 127.04603079475764),
-('POI026', '미아사거리역', 'Miasageori station', '인구밀집지역', 37.61244898963818, 127.0305835226811),
-('POI027', '발산역', 'Balsan station', '인구밀집지역', 37.55796811527854, 126.84179723439551),
-('POI028', '북한산우이역', 'Bukhansan Ui station', '인구밀집지역', 37.66281154490492, 127.01070580250341),
-('POI029', '사당역', 'Sadang station', '인구밀집지역', 37.47635049189678, 126.98129519152415),
-('POI030', '삼각지역', 'Samgakji station', '인구밀집지역', 37.535323919468816, 126.97392611189508),
-('POI031', '서울대입구역', 'Seoul National University station', '인구밀집지역', 37.48060991955129, 126.95360407634334),
-('POI032', '서울식물원·마곡나루역', 'Seoul Botanic Park·Magongnaru station', '인구밀집지역', 37.56745047572558, 126.82986126533689),
-('POI033', '서울역', 'Seoul station', '인구밀집지역', 37.55662336923077, 126.97216878769233),
-('POI034', '선릉역', 'Seolleung station', '인구밀집지역', 37.50584166753116, 127.0489100692203),
-('POI035', '성신여대입구역', 'Sungshin Women''s University station', '인구밀집지역', 37.59262801654558, 127.01658994286346),
-('POI036', '수유역', 'Suyu station', '인구밀집지역', 37.641215514705884, 127.02565393627451),
-('POI037', '신논현역·논현역', 'Sinnonhyeon·Nonhyeon station', '인구밀집지역', 37.50848054554439, 127.02291493850254),
-('POI038', '신도림역', 'Sindorim station', '인구밀집지역', 37.50941493666961, 126.89096014067083),
-('POI039', '신림역', 'Sillim station', '인구밀집지역', 37.48396968904173, 126.92987028995233),
-('POI040', '신촌·이대역', 'Sinchon·Ewha Womans University station', '인구밀집지역', 37.55564146783625, 126.93761215497075),
-('POI041', '양재역', 'Yangjae station', '인구밀집지역', 37.48451422353994, 127.03551115644439),
-('POI042', '역삼역', 'Yeoksam station', '인구밀집지역', 37.50011177778084, 127.03721339389585),
-('POI043', '연신내역', 'Yeonsinnae station', '인구밀집지역', 37.61903547457627, 126.92180184322034),
-('POI044', '오목교역·목동운동장', 'Omokgyo·Mok-dong Stadium station', '인구밀집지역', 37.53174788866626, 126.87924768246015),
-('POI045', '왕십리역', 'Wangsimni station', '인구밀집지역', 37.561636745463275, 127.03857508984515),
-('POI046', '용산역', 'Yongsan station', '인구밀집지역', 37.53059674809017, 126.95953798376128),
-('POI047', '이태원역', 'Itaewon station', '인구밀집지역', 37.534847968613946, 126.99261390234051),
-('POI048', '장지역', 'Jangji station', '인구밀집지역', 37.47861018962738, 127.12424263037641),
-('POI049', '장한평역', 'Janghanpyeong station', '인구밀집지역', 37.562849061361526, 127.06235115915185),
-('POI050', '천호역', 'Cheonho station', '인구밀집지역', 37.53921065146112, 127.12470374198884),
-('POI051', '총신대입구(이수)역', 'Chongshin University(Isu) station', '인구밀집지역', 37.48677821828824, 126.9807539648491),
-('POI052', '충정로역', 'Chungjeongno station', '인구밀집지역', 37.559524839900476, 126.9642059673645),
-('POI053', '합정역', 'Hapjeong station', '인구밀집지역', 37.54857307399022, 126.91084543497917),
-('POI054', '혜화역', 'Hyehwa station', '인구밀집지역', 37.58237426286851, 127.0014404389915),
-('POI055', '홍대입구역(2호선)', 'Hongik University station(Line 2)', '인구밀집지역', 37.5560688250626, 126.92329766229786),
-('POI056', '회기역', 'Hoegi station', '인구밀집지역', 37.590538258503685, 127.05700039395965),
-('POI057', '4·19 카페거리', '4·19 Cafe street', '발달상권', 37.64481185893745, 127.00739701908337),
-('POI058', '가락시장', 'Garak Market', '발달상권', 37.49445476721721, 127.11054976163454),
-('POI059', '가로수길', 'Garosu-gil', '발달상권', 37.522662622516556, 127.02543305960263),
-('POI060', '광장(전통)시장', 'Gwangjang(Traditional) Market', '발달상권', 37.569817036518465, 127.00014893169192),
-('POI061', '김포공항', 'Gimpo Airport', '발달상권', 37.56228914604604, 126.80289965159135),
-('POI062', '낙산공원·이화마을', 'Naksan Park·Ihwa Village', '발달상권', 37.580295002146926, 127.00692469978675),
-('POI063', '노량진', 'Noryangjin', '발달상권', 37.513690699957316, 126.94907070192423),
-('POI064', '덕수궁길·정동길', 'Deoksugung-gil·Jeongdong-gil', '발달상권', 37.5659744042236, 126.97244482634493),
-('POI065', '방배역 먹자골목', 'Bangbae food alley', '발달상권', 37.48234102283986, 126.99597969571558),
-('POI066', '북촌한옥마을', 'Bukchon Hanok Village', '발달상권', 37.58425649416171, 126.98386049085175),
-('POI067', '서촌', 'Seochon', '발달상권', 37.58018833392947, 126.9666234325548),
-('POI068', '성수카페거리', 'Seongsu Cafe Street', '발달상권', 37.54227657813654, 127.05560237183015),
-('POI069', '수유리 먹자골목', 'Suyuri food alley', '발달상권', 37.64270823057644, 127.02733855833522),
-('POI070', '쌍문동 맛집거리', 'Ssangmun-dong restaurant street', '발달상권', 37.6477285443038, 127.03242528481013),
-('POI071', '압구정로데오거리', 'Apgujeong Rodeo Street', '발달상권', 37.52560441860466, 127.04040171511626),
-('POI072', '여의도', 'Yeouido', '발달상권', 37.52549260943663, 126.92338937129487),
-('POI073', '연남동', 'Yeonnam-dong', '발달상권', 37.561426426204264, 126.92406273242905),
-('POI074', '영등포 타임스퀘어', 'Yeongdeungpo Time square', '발달상권', 37.51653179760669, 126.90523931614634),
-('POI075', '외대앞', 'Hankuk University of Foreign Studies', '발달상권', 37.59442412843283, 127.06153833102397),
-('POI076', '용리단길', 'Yongnidan-gil', '발달상권', 37.5311056439386, 126.97147359358237),
-('POI077', '이태원 앤틱가구거리', 'Itaewon Antiques street', '발달상권', 37.53187270743738, 126.99343376513602),
-('POI078', '인사동·익선동', 'Insa-dong·Ikseon-dong', '발달상권', 37.5741577489609, 126.98736374162822),
-('POI079', '창동 신경제 중심지', 'Changdong New Economic Center', '발달상권', 37.656167961335676, 127.05328808787348),
-('POI080', '청담동 명품거리', 'Cheongdam-dong Luxury Fashion street', '발달상권', 37.52645537152884, 127.04354058968364),
-('POI081', '청량리 제기동 일대 전통시장', 'Traditional market in Cheongnyangni Jegi-dong', '발달상권', 37.579977738703214, 127.0382790126348),
-('POI082', '해방촌·경리단길', 'Haebangchon·Gyeongnidan-gil', '발달상권', 37.54143829714048, 126.98836800969178),
-('POI083', 'DDP(동대문디자인플라자)', 'DDP(Dongdaemun Design Plaza)', '발달상권', 37.56654201564661, 127.00993045294622),
-('POI084', 'DMC(디지털미디어시티)', 'DMC(Digital Media City)', '발달상권', 37.577407110655734, 126.8936691147541),
-('POI085', '강서한강공원', 'Gangseo Hangang Park', '공원', 37.5858708295553, 126.82002478578644),
-('POI086', '고척돔', 'Gocheok Dome', '공원', 37.49776790791694, 126.86713336195083),
-('POI087', '광나루한강공원', 'Gwangnaru Hangang Park', '공원', 37.550866089267345, 127.12370647922764),
-('POI088', '광화문광장', 'Gwanghwamun Square', '공원', 37.57505062349029, 126.97680679071502),
-('POI089', '국립중앙박물관·용산가족공원', 'The National Museum of Korea·Yongsan Family Park', '공원', 37.52202119313341, 126.9824679850178),
-('POI090', '난지한강공원', 'Nanji Hangang Park', '공원', 37.566316740172454, 126.87770001597329),
-('POI091', '남산공원', 'Namsan Park', '공원', 37.555070650369004, 126.99365012730628),
-('POI092', '노들섬', 'Nodeul island', '공원', 37.517556872131706, 126.95871397855296),
-('POI093', '뚝섬한강공원', 'Ttukseom Hangang Park', '공원', 37.52882503002481, 127.07460742502332),
-('POI094', '망원한강공원', 'Mangwon Hangang Park', '공원', 37.55480087656761, 126.89823749216892),
-('POI095', '반포한강공원', 'Banpo Hangang Park', '공원', 37.51065462735107, 126.99457550649858),
-('POI096', '북서울꿈의숲', 'Dream Forest', '공원', 37.62244979881656, 127.04069568441815),
-('POI097', '불광천', 'Bulgwangcheon River', '공원', 37.58940646530798, 126.91194122899687),
-('POI098', '서리풀공원·몽마르뜨공원', 'Seoripul Park·Montmartre Park', '공원', 37.49022054658995, 127.00173655775917),
-('POI100', '서울대공원', 'Seoul Grand Park', '공원', 37.42839607329216, 127.01863790236284),
-('POI101', '서울숲공원', 'Seoul Forest', '공원', 37.54402680167043, 127.03673083133076),
-('POI099', '서울광장', 'Seoul Plaza', '공원', 37.5655010296151, 126.97798702124584),
-('POI102', '아차산', 'Achasan', '공원', 37.56307021845223, 127.10047918048643),
-('POI103', '양화한강공원', 'Yanghwa Hangang Park', '공원', 37.54029653900909, 126.89963558394624),
-('POI104', '어린이대공원', 'Children''s Grand Park', '공원', 37.54957100980681, 127.0824313180458),
-('POI105', '여의도한강공원', 'Yeouido Hangang Park', '공원', 37.52868909088513, 126.92627587984478),
-('POI106', '월드컵공원', 'World Cup Park', '공원', 37.5660391150387, 126.88740201899091),
-('POI107', '응봉산', 'Eungbongsan', '공원', 37.54797346368485, 127.02904643771303),
-('POI108', '이촌한강공원', 'Ichon Hangang Park', '공원', 37.52139035897837, 126.96356631928725),
-('POI109', '잠실종합운동장', 'Jamsil (Seoul) Sports Complex', '공원', 37.51610665876664, 127.07230325047283),
-('POI110', '잠실한강공원', 'Jamsil Hangang Park', '공원', 37.51981181577704, 127.08507449607517),
-('POI111', '잠원한강공원', 'Jamwon Hangang Park', '공원', 37.52336245003122, 127.01397131593632),
-('POI112', '청계산', 'Cheonggyesan', '공원', 37.443365424959026, 127.0469697170313),
-('POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264),
-('POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616),
-('POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821);
\ No newline at end of file

From 6ce48f4cce4df8680a955be88c4a0549b507ff10 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 16:56:37 +0900
Subject: [PATCH 164/361] =?UTF-8?q?auth=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?=
 =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 area.sql                                      | 116 --------
 auth-service/.gitattributes                   |   3 -
 auth-service/.gitignore                       |  37 ---
 .../.gradle/8.13/checksums/checksums.lock     | Bin 17 -> 0 bytes
 .../.gradle/8.13/checksums/md5-checksums.bin  | Bin 21097 -> 0 bytes
 .../.gradle/8.13/checksums/sha1-checksums.bin | Bin 27407 -> 0 bytes
 .../executionHistory/executionHistory.lock    | Bin 17 -> 0 bytes
 .../.gradle/8.13/fileChanges/last-build.bin   | Bin 1 -> 0 bytes
 .../.gradle/8.13/fileHashes/fileHashes.lock   | Bin 17 -> 0 bytes
 auth-service/.gradle/8.13/gc.properties       |   0
 .../buildOutputCleanup.lock                   | Bin 17 -> 0 bytes
 .../buildOutputCleanup/cache.properties       |   2 -
 auth-service/.gradle/vcs-1/gc.properties      |   0
 auth-service/bin/main/application.properties  |   1 -
 .../authservice/AuthServiceApplication.class  | Bin 738 -> 0 bytes
 .../AuthServiceApplicationTests.class         | Bin 566 -> 0 bytes
 auth-service/build.gradle                     |  58 ----
 .../gradle/wrapper/gradle-wrapper.jar         | Bin 43705 -> 0 bytes
 .../gradle/wrapper/gradle-wrapper.properties  |   7 -
 auth-service/gradlew                          | 251 ------------------
 auth-service/gradlew.bat                      |  94 -------
 auth-service/settings.gradle                  |   1 -
 .../authservice/AuthServiceApplication.java   |  13 -
 .../src/main/resources/application.properties |   1 -
 .../AuthServiceApplicationTests.java          |  13 -
 25 files changed, 597 deletions(-)
 delete mode 100644 area.sql
 delete mode 100644 auth-service/.gitattributes
 delete mode 100644 auth-service/.gitignore
 delete mode 100644 auth-service/.gradle/8.13/checksums/checksums.lock
 delete mode 100644 auth-service/.gradle/8.13/checksums/md5-checksums.bin
 delete mode 100644 auth-service/.gradle/8.13/checksums/sha1-checksums.bin
 delete mode 100644 auth-service/.gradle/8.13/executionHistory/executionHistory.lock
 delete mode 100644 auth-service/.gradle/8.13/fileChanges/last-build.bin
 delete mode 100644 auth-service/.gradle/8.13/fileHashes/fileHashes.lock
 delete mode 100644 auth-service/.gradle/8.13/gc.properties
 delete mode 100644 auth-service/.gradle/buildOutputCleanup/buildOutputCleanup.lock
 delete mode 100644 auth-service/.gradle/buildOutputCleanup/cache.properties
 delete mode 100644 auth-service/.gradle/vcs-1/gc.properties
 delete mode 100644 auth-service/bin/main/application.properties
 delete mode 100644 auth-service/bin/main/com/example/authservice/AuthServiceApplication.class
 delete mode 100644 auth-service/bin/test/com/example/authservice/AuthServiceApplicationTests.class
 delete mode 100644 auth-service/build.gradle
 delete mode 100644 auth-service/gradle/wrapper/gradle-wrapper.jar
 delete mode 100644 auth-service/gradle/wrapper/gradle-wrapper.properties
 delete mode 100644 auth-service/gradlew
 delete mode 100644 auth-service/gradlew.bat
 delete mode 100644 auth-service/settings.gradle
 delete mode 100644 auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java
 delete mode 100644 auth-service/src/main/resources/application.properties
 delete mode 100644 auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java

diff --git a/area.sql b/area.sql
deleted file mode 100644
index d2119f5..0000000
--- a/area.sql
+++ /dev/null
@@ -1,116 +0,0 @@
-INSERT INTO area (area_id, name, name_eng, category, x, y) VALUES
-('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705),
-('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576),
-('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864),
-('POI004', '이태원 관광특구', 'Itaewon Special Tourist Zone', '관광특구', 37.53475718815058, 126.99591956660342),
-('POI005', '잠실 관광특구', 'Jamsil Special Tourist Zone', '관광특구', 37.51868923685722, 127.11694379878114),
-('POI006', '종로·청계 관광특구', 'Jongno Cheonggye Special Toruist Zone', '관광특구', 37.56984229024498, 126.99866678943675),
-('POI007', '홍대 관광특구', 'HongDae Culture & Arts Special Tourist Zone', '관광특구', 37.55506170187393, 126.92159115672915),
-('POI008', '경복궁', 'Gyeongbokgung Palace', '고궁·문화유산', 37.5809150911806, 126.97764726625209),
-('POI009', '광화문·덕수궁', 'Gwanghwamun & Deoksugung Palace', '고궁·문화유산', 37.56847081447724, 126.97620594802834),
-('POI010', '보신각', 'Bosingak', '고궁·문화유산', 37.57089797616037, 126.98337607511172),
-('POI011', '서울 암사동 유적', 'Amsa Prehistoric Settlement Site', '고궁·문화유산', 37.56102809827398, 127.13078491934499),
-('POI012', '창덕궁·종묘', 'Changdeokgung Palace & Jongmyo Shrine', '고궁·문화유산', 37.5795304952381, 126.99416802285715),
-('POI013', '가산디지털단지역', 'Gasan Digital Complex station', '인구밀집지역', 37.48267268376068, 126.88014526210826),
-('POI014', '강남역', 'Gangnam station', '인구밀집지역', 37.50019794543511, 127.02822145618981),
-('POI015', '건대입구역', 'Konkuk University station', '인구밀집지역', 37.540005312302846, 127.06718185804415),
-('POI016', '고덕역', 'Godeok station', '인구밀집지역', 37.553377141111774, 127.15503341723486),
-('POI017', '고속터미널역', 'Express Bus Terminal station', '인구밀집지역', 37.50472316731823, 127.00542276570697),
-('POI018', '교대역', 'Seoul National University of Education station', '인구밀집지역', 37.49185444557083, 127.01456630812736),
-('POI019', '구로디지털단지역', 'Guro Digital Complex station', '인구밀집지역', 37.48349491938897, 126.89557915984915),
-('POI020', '구로역', 'Guro station', '인구밀집지역', 37.501517014484925, 126.88285531403136),
-('POI021', '군자역', 'Gunja station', '인구밀집지역', 37.555916439038526, 127.08008933356267),
-('POI022', '남구로역', 'Namguro station', '인구밀집지역', 37.484184300878496, 126.88539683418863),
-('POI023', '대림역', 'Daerim station', '인구밀집지역', 37.492818237753816, 126.89567307890546),
-('POI024', '동대문역', 'Dongdaemun station', '인구밀집지역', 37.57127393968462, 127.0096854515538),
-('POI025', '뚝섬역', 'Ttukseom station', '인구밀집지역', 37.548249670448335, 127.04603079475764),
-('POI026', '미아사거리역', 'Miasageori station', '인구밀집지역', 37.61244898963818, 127.0305835226811),
-('POI027', '발산역', 'Balsan station', '인구밀집지역', 37.55796811527854, 126.84179723439551),
-('POI028', '북한산우이역', 'Bukhansan Ui station', '인구밀집지역', 37.66281154490492, 127.01070580250341),
-('POI029', '사당역', 'Sadang station', '인구밀집지역', 37.47635049189678, 126.98129519152415),
-('POI030', '삼각지역', 'Samgakji station', '인구밀집지역', 37.535323919468816, 126.97392611189508),
-('POI031', '서울대입구역', 'Seoul National University station', '인구밀집지역', 37.48060991955129, 126.95360407634334),
-('POI032', '서울식물원·마곡나루역', 'Seoul Botanic Park·Magongnaru station', '인구밀집지역', 37.56745047572558, 126.82986126533689),
-('POI033', '서울역', 'Seoul station', '인구밀집지역', 37.55662336923077, 126.97216878769233),
-('POI034', '선릉역', 'Seolleung station', '인구밀집지역', 37.50584166753116, 127.0489100692203),
-('POI035', '성신여대입구역', 'Sungshin Women''s University station', '인구밀집지역', 37.59262801654558, 127.01658994286346),
-('POI036', '수유역', 'Suyu station', '인구밀집지역', 37.641215514705884, 127.02565393627451),
-('POI037', '신논현역·논현역', 'Sinnonhyeon·Nonhyeon station', '인구밀집지역', 37.50848054554439, 127.02291493850254),
-('POI038', '신도림역', 'Sindorim station', '인구밀집지역', 37.50941493666961, 126.89096014067083),
-('POI039', '신림역', 'Sillim station', '인구밀집지역', 37.48396968904173, 126.92987028995233),
-('POI040', '신촌·이대역', 'Sinchon·Ewha Womans University station', '인구밀집지역', 37.55564146783625, 126.93761215497075),
-('POI041', '양재역', 'Yangjae station', '인구밀집지역', 37.48451422353994, 127.03551115644439),
-('POI042', '역삼역', 'Yeoksam station', '인구밀집지역', 37.50011177778084, 127.03721339389585),
-('POI043', '연신내역', 'Yeonsinnae station', '인구밀집지역', 37.61903547457627, 126.92180184322034),
-('POI044', '오목교역·목동운동장', 'Omokgyo·Mok-dong Stadium station', '인구밀집지역', 37.53174788866626, 126.87924768246015),
-('POI045', '왕십리역', 'Wangsimni station', '인구밀집지역', 37.561636745463275, 127.03857508984515),
-('POI046', '용산역', 'Yongsan station', '인구밀집지역', 37.53059674809017, 126.95953798376128),
-('POI047', '이태원역', 'Itaewon station', '인구밀집지역', 37.534847968613946, 126.99261390234051),
-('POI048', '장지역', 'Jangji station', '인구밀집지역', 37.47861018962738, 127.12424263037641),
-('POI049', '장한평역', 'Janghanpyeong station', '인구밀집지역', 37.562849061361526, 127.06235115915185),
-('POI050', '천호역', 'Cheonho station', '인구밀집지역', 37.53921065146112, 127.12470374198884),
-('POI051', '총신대입구(이수)역', 'Chongshin University(Isu) station', '인구밀집지역', 37.48677821828824, 126.9807539648491),
-('POI052', '충정로역', 'Chungjeongno station', '인구밀집지역', 37.559524839900476, 126.9642059673645),
-('POI053', '합정역', 'Hapjeong station', '인구밀집지역', 37.54857307399022, 126.91084543497917),
-('POI054', '혜화역', 'Hyehwa station', '인구밀집지역', 37.58237426286851, 127.0014404389915),
-('POI055', '홍대입구역(2호선)', 'Hongik University station(Line 2)', '인구밀집지역', 37.5560688250626, 126.92329766229786),
-('POI056', '회기역', 'Hoegi station', '인구밀집지역', 37.590538258503685, 127.05700039395965),
-('POI057', '4·19 카페거리', '4·19 Cafe street', '발달상권', 37.64481185893745, 127.00739701908337),
-('POI058', '가락시장', 'Garak Market', '발달상권', 37.49445476721721, 127.11054976163454),
-('POI059', '가로수길', 'Garosu-gil', '발달상권', 37.522662622516556, 127.02543305960263),
-('POI060', '광장(전통)시장', 'Gwangjang(Traditional) Market', '발달상권', 37.569817036518465, 127.00014893169192),
-('POI061', '김포공항', 'Gimpo Airport', '발달상권', 37.56228914604604, 126.80289965159135),
-('POI062', '낙산공원·이화마을', 'Naksan Park·Ihwa Village', '발달상권', 37.580295002146926, 127.00692469978675),
-('POI063', '노량진', 'Noryangjin', '발달상권', 37.513690699957316, 126.94907070192423),
-('POI064', '덕수궁길·정동길', 'Deoksugung-gil·Jeongdong-gil', '발달상권', 37.5659744042236, 126.97244482634493),
-('POI065', '방배역 먹자골목', 'Bangbae food alley', '발달상권', 37.48234102283986, 126.99597969571558),
-('POI066', '북촌한옥마을', 'Bukchon Hanok Village', '발달상권', 37.58425649416171, 126.98386049085175),
-('POI067', '서촌', 'Seochon', '발달상권', 37.58018833392947, 126.9666234325548),
-('POI068', '성수카페거리', 'Seongsu Cafe Street', '발달상권', 37.54227657813654, 127.05560237183015),
-('POI069', '수유리 먹자골목', 'Suyuri food alley', '발달상권', 37.64270823057644, 127.02733855833522),
-('POI070', '쌍문동 맛집거리', 'Ssangmun-dong restaurant street', '발달상권', 37.6477285443038, 127.03242528481013),
-('POI071', '압구정로데오거리', 'Apgujeong Rodeo Street', '발달상권', 37.52560441860466, 127.04040171511626),
-('POI072', '여의도', 'Yeouido', '발달상권', 37.52549260943663, 126.92338937129487),
-('POI073', '연남동', 'Yeonnam-dong', '발달상권', 37.561426426204264, 126.92406273242905),
-('POI074', '영등포 타임스퀘어', 'Yeongdeungpo Time square', '발달상권', 37.51653179760669, 126.90523931614634),
-('POI075', '외대앞', 'Hankuk University of Foreign Studies', '발달상권', 37.59442412843283, 127.06153833102397),
-('POI076', '용리단길', 'Yongnidan-gil', '발달상권', 37.5311056439386, 126.97147359358237),
-('POI077', '이태원 앤틱가구거리', 'Itaewon Antiques street', '발달상권', 37.53187270743738, 126.99343376513602),
-('POI078', '인사동·익선동', 'Insa-dong·Ikseon-dong', '발달상권', 37.5741577489609, 126.98736374162822),
-('POI079', '창동 신경제 중심지', 'Changdong New Economic Center', '발달상권', 37.656167961335676, 127.05328808787348),
-('POI080', '청담동 명품거리', 'Cheongdam-dong Luxury Fashion street', '발달상권', 37.52645537152884, 127.04354058968364),
-('POI081', '청량리 제기동 일대 전통시장', 'Traditional market in Cheongnyangni Jegi-dong', '발달상권', 37.579977738703214, 127.0382790126348),
-('POI082', '해방촌·경리단길', 'Haebangchon·Gyeongnidan-gil', '발달상권', 37.54143829714048, 126.98836800969178),
-('POI083', 'DDP(동대문디자인플라자)', 'DDP(Dongdaemun Design Plaza)', '발달상권', 37.56654201564661, 127.00993045294622),
-('POI084', 'DMC(디지털미디어시티)', 'DMC(Digital Media City)', '발달상권', 37.577407110655734, 126.8936691147541),
-('POI085', '강서한강공원', 'Gangseo Hangang Park', '공원', 37.5858708295553, 126.82002478578644),
-('POI086', '고척돔', 'Gocheok Dome', '공원', 37.49776790791694, 126.86713336195083),
-('POI087', '광나루한강공원', 'Gwangnaru Hangang Park', '공원', 37.550866089267345, 127.12370647922764),
-('POI088', '광화문광장', 'Gwanghwamun Square', '공원', 37.57505062349029, 126.97680679071502),
-('POI089', '국립중앙박물관·용산가족공원', 'The National Museum of Korea·Yongsan Family Park', '공원', 37.52202119313341, 126.9824679850178),
-('POI090', '난지한강공원', 'Nanji Hangang Park', '공원', 37.566316740172454, 126.87770001597329),
-('POI091', '남산공원', 'Namsan Park', '공원', 37.555070650369004, 126.99365012730628),
-('POI092', '노들섬', 'Nodeul island', '공원', 37.517556872131706, 126.95871397855296),
-('POI093', '뚝섬한강공원', 'Ttukseom Hangang Park', '공원', 37.52882503002481, 127.07460742502332),
-('POI094', '망원한강공원', 'Mangwon Hangang Park', '공원', 37.55480087656761, 126.89823749216892),
-('POI095', '반포한강공원', 'Banpo Hangang Park', '공원', 37.51065462735107, 126.99457550649858),
-('POI096', '북서울꿈의숲', 'Dream Forest', '공원', 37.62244979881656, 127.04069568441815),
-('POI097', '불광천', 'Bulgwangcheon River', '공원', 37.58940646530798, 126.91194122899687),
-('POI098', '서리풀공원·몽마르뜨공원', 'Seoripul Park·Montmartre Park', '공원', 37.49022054658995, 127.00173655775917),
-('POI100', '서울대공원', 'Seoul Grand Park', '공원', 37.42839607329216, 127.01863790236284),
-('POI101', '서울숲공원', 'Seoul Forest', '공원', 37.54402680167043, 127.03673083133076),
-('POI099', '서울광장', 'Seoul Plaza', '공원', 37.5655010296151, 126.97798702124584),
-('POI102', '아차산', 'Achasan', '공원', 37.56307021845223, 127.10047918048643),
-('POI103', '양화한강공원', 'Yanghwa Hangang Park', '공원', 37.54029653900909, 126.89963558394624),
-('POI104', '어린이대공원', 'Children''s Grand Park', '공원', 37.54957100980681, 127.0824313180458),
-('POI105', '여의도한강공원', 'Yeouido Hangang Park', '공원', 37.52868909088513, 126.92627587984478),
-('POI106', '월드컵공원', 'World Cup Park', '공원', 37.5660391150387, 126.88740201899091),
-('POI107', '응봉산', 'Eungbongsan', '공원', 37.54797346368485, 127.02904643771303),
-('POI108', '이촌한강공원', 'Ichon Hangang Park', '공원', 37.52139035897837, 126.96356631928725),
-('POI109', '잠실종합운동장', 'Jamsil (Seoul) Sports Complex', '공원', 37.51610665876664, 127.07230325047283),
-('POI110', '잠실한강공원', 'Jamsil Hangang Park', '공원', 37.51981181577704, 127.08507449607517),
-('POI111', '잠원한강공원', 'Jamwon Hangang Park', '공원', 37.52336245003122, 127.01397131593632),
-('POI112', '청계산', 'Cheonggyesan', '공원', 37.443365424959026, 127.0469697170313),
-('POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264),
-('POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616),
-('POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821);
\ No newline at end of file
diff --git a/auth-service/.gitattributes b/auth-service/.gitattributes
deleted file mode 100644
index 8af972c..0000000
--- a/auth-service/.gitattributes
+++ /dev/null
@@ -1,3 +0,0 @@
-/gradlew text eol=lf
-*.bat text eol=crlf
-*.jar binary
diff --git a/auth-service/.gitignore b/auth-service/.gitignore
deleted file mode 100644
index c2065bc..0000000
--- a/auth-service/.gitignore
+++ /dev/null
@@ -1,37 +0,0 @@
-HELP.md
-.gradle
-build/
-!gradle/wrapper/gradle-wrapper.jar
-!**/src/main/**/build/
-!**/src/test/**/build/
-
-### STS ###
-.apt_generated
-.classpath
-.factorypath
-.project
-.settings
-.springBeans
-.sts4-cache
-bin/
-!**/src/main/**/bin/
-!**/src/test/**/bin/
-
-### IntelliJ IDEA ###
-.idea
-*.iws
-*.iml
-*.ipr
-out/
-!**/src/main/**/out/
-!**/src/test/**/out/
-
-### NetBeans ###
-/nbproject/private/
-/nbbuild/
-/dist/
-/nbdist/
-/.nb-gradle/
-
-### VS Code ###
-.vscode/
diff --git a/auth-service/.gradle/8.13/checksums/checksums.lock b/auth-service/.gradle/8.13/checksums/checksums.lock
deleted file mode 100644
index b1d2a8d2f692684808fb4cfe80a007e413c235fe..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 17
UcmZQp++t@_wroch0|eX$04u`;Z~y=R

diff --git a/auth-service/.gradle/8.13/checksums/md5-checksums.bin b/auth-service/.gradle/8.13/checksums/md5-checksums.bin
deleted file mode 100644
index 01c17bdefd353cfae0fc456139e95b61f09ba191..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 21097
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
a=~s7#v0USHY6diF@m31n!X?4B)PDhIMUcq=

diff --git a/auth-service/.gradle/8.13/executionHistory/executionHistory.lock b/auth-service/.gradle/8.13/executionHistory/executionHistory.lock
deleted file mode 100644
index d22c4ab2276dbc5c29d737790cbaacddc10d9753..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 17
TcmZQBmdv@|c}#RO0~7!NFfjx2

diff --git a/auth-service/.gradle/8.13/fileChanges/last-build.bin b/auth-service/.gradle/8.13/fileChanges/last-build.bin
deleted file mode 100644
index f76dd238ade08917e6712764a16a22005a50573d..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1
IcmZPo000310RR91

diff --git a/auth-service/.gradle/8.13/fileHashes/fileHashes.lock b/auth-service/.gradle/8.13/fileHashes/fileHashes.lock
deleted file mode 100644
index 0c961dfdc2280d13c3ca8b5f49adba48fba78245..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 17
TcmZQRy{|fQ?$^0l3{U_7KNJM|

diff --git a/auth-service/.gradle/8.13/gc.properties b/auth-service/.gradle/8.13/gc.properties
deleted file mode 100644
index e69de29..0000000
diff --git a/auth-service/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/auth-service/.gradle/buildOutputCleanup/buildOutputCleanup.lock
deleted file mode 100644
index 8cb4cde28e862ced8d3109ea4ffcbcd881d35864..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 17
UcmZQx&nb4BcPFW@5%EQt1wH2<{}0PG|TnDl!!L
zfP7RYtFve9;u@M=?OW;X+g<(r_x1towy=&3hMiDp
zd0k|Yv^OG3B||4Pu}CMvDjzSvy1Q37!{_jQs)kfC6G}65`@h1ay0NoLC&SZ8fL-i0
z&|w%DoA7+<@jELr`DE-PkBo7AT!JgTwy=+n)R8TSIO?zPj#825!>USQ6@6^9lh2Ni
zjJ{JzVSU3ZbqMxp--NIUwZB)v%n*!CVPpAC`RN^3ujkD7&d~c&Xs0qcQMvN9v^K6>
zACX^FN-QTp>o=se)F>D8xT4XlXdU7cXe9K3fEt}>xnvs@aq^7(`rsAKmogoY^=Z(B
zEwZio1@hYzZQ=uV2=R}V64viP??$)@wHcsJ68)>bJS6*&iHnskXE8MjvIPVl|FrmV}Npeql07fCw6`pw`0s
zGauF(<*@v{3t!qoUU*=j)6;|-(yg@jvDx&fV^trtZt27?4Tkn729qrItVh@PMwG5$
z+oXHSPM??iHZ!cVP~gYact-CwV`}~Q+R}PPNRy+T-geK+>fHrijpllon_F4N{@b-}
z1M0=a!VbVmJM8Xk@NRv)m&aRYN}FSJ{LS;}2ArQ5baSjfy40l@T5)1r-^0fAU6f_}
zzScst%$Nd-^ElV~H0TetQhMc%S{}Q4lssln=|;LG?Ulo}*mhg8YvBAUY7YFdXs~vv
zv~{duzVw%C#GxkBwX=TYp1Dh*Uaum2?RmsvPaLlzO^fIJ`L?&OV?Y&kKj~^kWC`Ly
zfL-}J^4a0Ojuz9O{jUbIS;^JatJ5+YNNHe}6nG9Yd6P-lJiK2ms)A^xq^H2fKrTF)
zp!6=`Ece~57>^9(RA4OB9;f1FAhV%zVss%#rDq$9ZW3N2cXC7dMz;|UcRFecBm`DA
z1pCO!#6zKp#@mx{2>Qcme8y$Qg_gnA%(`Vtg3ccwgb~D(&@y8#Jg8nNYW*-P{_M#E
zZ|wCsQoO1(iIKd-2B9xzI}?l#Q@G5d$m1Lfh0q;iS5FDQ&9_2X-H)VDKA*fa{b(sV
zL--krNCXibi1+*C2;4qVjb0KWUVGjjRT{A}Q*!cFmj0tRip2ra>WYJ>ZK4C|V~RYs
z6;~+*)5F^x^aQqk9tjh)L;DOLlD8j+0<>kHc8MN|68PxQV`tJFbgxSfq-}b(_h`luA0&;Vk<@51i0
z_cu6{_*=vlvYbKjDawLw+t^H?OV00_73Cn3goU5?})UYFuoSX6Xqw;TKcrsc|r#
z$sMWYl@cs#SVopO$hpHZ)cdU-+Ui%z&Sa#lMI~zWW@vE%QDh@bTe0&V9nL>4Et9`N
zGT8(X{l@A~loDx}BDz`m6@tLv@$mTlVJ;4MGuj!;9Y=%;;_kj#o8n5tX%@M)2I@}u
z_{I!^7N1BxW9`g&Z+K#lZ@7_dXdsqp{W9_`)zgZ=sD~%WS5s$`7z#XR!Lfy(4se(m
zR@a3twgMs19!-c4jh`PfpJOSU;vShBKD|I0@rmv_x|+ogqslnLLOepJpPMOxhRb*i
zGHkwf#?ylQ@k9QJL?!}MY4i7joSzMcEhrDKJH&?2v{-tgCqJe+Y0njl7HYff
z{&~M;JUXVR$qM1FPucIEY(IBAuCHC@^~QG6O!dAjzQBxDOR~lJEr4KS9R*idQ^p{D
zS#%NQADGbAH~6wAt}(1=Uff-1O#ITe)31zCL$e9~{w)gx)g>?zFE{Bc9nJT6xR!i8
z)l)~9&~zSZTHk{?iQL^MQo$wLi}`B*qnvUy+Y*jEraZMnEhuj`Fu+>b5xD1_Tp
z)8|wedv42#3AZUL7x&G@p@&zcUvPkvg=YJS6?1B7ZEXr4b>M+9Gli$gK-Sgh{O@>q7TUg+H
zNJj`6q#O@>4HpPJEHvNij`sYW&u%#=215HKNg;C!0#hH1vlO5+dFq9&
zS)8{5_%hz?#D#wn&nm@aB?1_|@kpA@{%jYcs{K%$a4W{k@F
zPyTav?jb;F(|GaZhm6&M#g|`ckO+|mCtAU)5_(hn&Ogd
z9Ku}orOMu@K^Ac>eRh3+0-y^F`j^noa*OkS3p^tLV`TY$F$cPXZJ48!xz1d7%vfA(
zUx2+sDPqHfiD-_wJDb38K^LtpN2B0w=$A10z%F9f_P2aDX63w7zDG5CekVQJGy18I
zB!tI`6rZr7TK10L(8bpiaQ>S@b7r_u@lh^vakd0e6USWw7W%d_Ob%M!a`K>#I3r-w
zo2^+9Y)Sb?P9)x0iA#^ns+Kp{JFF|$09jb6ZS2}_<-=$?^#IUo5;g`4ICZknr!_aJ
zd73%QP^e-$%Xjt|28xM}ftD|V@76V_qvNu#?Mt*A-OV{E4_zC4Ymo|(cb+w^`Wv==
z>)c%_U0w`d$^`lZQp@midD89ta_qTJW~5lRrIVwjRG_9aRiQGug%f3p@;*%Y@J5uQ|#dJ+P{Omc`d2VR)DXM*=ukjVqIpkb<9gn9{*+&#p)Ek
zN=4zwNWHF~=GqcLkd!q0p(S2_K=Q`$whZ}r@ec_cb9hhg9a
z6CE=1n8Q;hC?;ujo0numJBSYY6)GTq^=kB~`-qE*h%*V6-ip=c4+Yqs*7C@@b4YAi
zuLjsmD!5M7r7d5ZPe>4$;iv|zq=9=;B$lI|xuAJwi~j~^Wuv!Qj2iEPWjh9Z&#+G>lZQpZ@(xfBrhc{rlLwOC;optJZDj4Xfu3$u6rt_=YY0~lxoy~fq=*L_&RmD7dZWBUmY&12S;(Ui^y
zBpHR0?Gk|`U&CooNm_(kkO~pK+cC%uVh^cnNn)MZjF@l{_bvn4`Jc}8QwC5_)k$zs
zM2qW1Zda%bIgY^3NcfL)9ug`05r5c%8ck)J6{fluBQhVE>h+IA&Kb}~$55m-^c1S3
zJMXGlOk+01qTQUFlh5Jc3xq|7McY$nCs$5=`8Y;|il#Ypb{O9}GJZD8!kYh{TKqs@
z-mQn1K4q$yGeyMcryHQgD6Ra<6^5V(>6_qg`3uxbl|T&cJVA*M_+OC#>w(xL`RoPQ
zf1ZCI3G%;o-x>RzO!mc}K!XX{1rih0$~9XeczHgHdPfL}4IPi~5EV#ZcT9
zdgkB3+NPbybS-d;{8%bZW^U+x@Ak+uw;a5JrZH!WbNvl!b~r4*vs#he^bqz`W93PkZna2oYO9dBrKh2QCWt{dGOw)%Su%1bIjtp4dKjZ^
zWfhb$M0MQiDa4)9rkip9DaH0_tv=XxNm>6MKeWv>`KNk@QVkp$Lhq_~>M6S$oliq2
zU6i7bK;TY)m>-}X7hDTie>cc$J|`*}t=MAMfWIALRh2=O{L57{#fA_9LMnrV(HrN6
zG0K_P5^#$eKt{J|#l~U0WN_3)p^LLY(XEqes0OvI?3)GTNY&S13X+9`6PLVFRf8K)
z9x@c|2T72+-KOm|kZ@j4EDDec>03FdgQlJ!&FbUQQH+nU^=U3Jyrgu97&#-W4C*;_
z(WacjhBDp@&Yon<9(BWPb;Q?Kc0gR5ZH~aRNkPAWbDY!FiYVSu!~Ss^9067|JCrZk
z-{Rn2KEBR|Wti_iy)
zXnh2wiU5Yz2L!W{{_#LwNWXeNPHkF=jjXmHC@n*oiz
zIoM~Wvo^T@@t!QQW?Ujql-GBOlnB|HjN@x~K8z)c(X}%%5Zcux09vC8=@tvgY>czq
z3D(U&FiETaN9aP}FDP3ZSIXIffq>M3{~eTB{uauL07oYiM=~K(XA{SN!rJLyXeC+Y
zOdeebgHOc2aCIgC=8>-Q>zfuXV*=a&gp{l#E@K|{qft@YtO>xaF>O7sZz%8);e86?
z+jJlFB{0fu6%8ew^_<+v>>%6eB8|t*_v7gb{x=vLLQYJKo;p7^o9!9A1)fZZ8i#ZU
z<|E?bZakjkEV8xGi?n+{Xh3EgFKdM^;4D;5fHmc04PI>6oU>>WuLy6jgpPhf8$K4M
zjJo*MbN0rZbZ!5DmoC^@hbqXiP^1l7I5;Wtp2i9Jkh+KtDJoXP0O8qmN;Sp(+%upX
zAxXs*qlr(ck+-QG_mMx?hQNXVV~LT{$Q$ShX+&x?Q7v
z@8t|UDylH6@RZ?WsMVd3B0z5zf50BP6U<&X_}+y3uJ0c5OD}+J&2T8}A%2Hu#Nt_4
zoOoTI$A!hQ<2pk5wfZDv+7Z{yo+Etqry=$!*pvYyS+kA4xnJ~3b~TBmA8Qd){w_bE
zqDaLIjnU8m$wG#&T!}{e0qmHHipA{$j`%KN{&#_Kmjd&#X-hQN+ju$5Ms$iHj4r?)
z&5m8tI}L$ih&95AjQ9EDfPKSmMj-@j?Q+h~C3<|Lg2zVtfKz=ft{YaQ1i6Om&EMll
zzov%MsjSg=u^%EfnO+W}@)O6u0LwoX709h3Cxdc2Rwgjd%LLTChQvHZ+y<1q6kbJXj3_pq1&MBE{8
zd;aFotyW>4WHB{JSD8Z9M@jBitC1RF;!B8;Rf-B4nOiVbGlh9w51(8WjL&e{_iXN(
zAvuMDIm_>L?rJPxc>S`bqC|W$njA0MKWa?V$u6mN@PLKYqak!bR!b%c^ze(M`ec(x
zv500337YCT4gO3+9>oVIJLv$pkf`01S(DUM+4u!HQob|IFHJHm#>eb#eB1X5;bMc|
z>QA4Zv}$S?fWg~31?Lr(C>MKhZg>gplRm`2WZ--iw%&&YlneQYY|PXl;_4*>vkp;I
z$VYTZq|B*(3(y17#@ud@o)XUZPYN*rStQg5U1Sm2gM}7hf_G<>*T%6ebK*tF(kbJc
zNPH4*xMnJNgw!ff{YXrhL&V$6`ylY={qT_xg9znQWw9>PlG~IbhnpsG_94Kk_(V-o&v7#F
znra%uD-}KOX2dkak**hJnZZQyp#ERyyV^lNe!Qrg=VHiyr7*%j#PMvZMuYNE8o;JM
zGrnDWmGGy)(UX{rLzJ*QEBd(VwMBXnJ@>*F8eOFy|FK*Vi0tYDw;#E
zu#6eS;%Nm2KY+7dHGT3m{TM7sl=z8|V0e!DzEkY-RG8vTWDdSQFE|?+&FYA146@|y
zV(JP>LWL;TSL6rao@W5fWqM1-xr$gRci#RQV2DX-x4@`w{uEUgoH4G|`J%H!N?*Qn
zy~rjzuf(E7E!A9R2bSF|{{U(zO+;e29K_dGmC^p7MCP!=Bzq@}&AdF5=rtCwka
zTT1A?5o}i*sXCsRXBt)`?nOL$zxuP3i*rm3Gmbmr6}9HCLvL*45d|(zP;q&(v%}S5yBmRVdYQQ24zh
z6qL2<2>StU$_Ft29IyF!6=!@;tW=o8vNzVy*hh}XhZhUbxa&;9~woye<_YmkUZ)S?PW{7t;
zmr%({tBlRLx=ffLd60`e{PQR3NUniWN2W^~7Sy~MPJ>A#!6PLnlw7O0(`=PgA}JLZ
ztqhiNcKvobCcBel2
z-N82?4-()eGOisnWcQ9Wp23|ybG?*g!2j#>m3~0__IX1o%dG4b;VF@^B+mRgKx|ij
zWr5G4jiRy}5n*(qu!W`y54Y*t8g`$YrjSunUmOsqykYB4-D(*(A~?QpuFWh;)A;5=
zPl|=x+-w&H9B7EZGjUMqXT}MkcSfF}bHeRFLttu!vHD{Aq)3HVhvtZY^&-lxYb2%`
zDXk7>V#WzPfJs6u{?ZhXpsMdm3kZscOc<^P&e&684Rc1-d=+=VOB)NR;{?0NjTl~D
z1MXak$#X4{VNJyD$b;U~Q@;zlGoPc@ny!u7Pe;N2l4;i8Q=8>R3H{>HU(z
z%hV2?rSinAg6&wuv1DmXok`5@a3@H0BrqsF~L$pRYHNEXXuRIWom0l
zR9hrZpn1LoYc+G@q@VsFyMDNX;>_Vf%4>6$Y@j;KSK#g)TZRmjJxB!_NmUMTY(cAV
zmewn7H{z`M3^Z&
z2O$pWlDuZHAQJ{xjA}B;fuojAj8WxhO}_9>qd0|p0nBXS6IIRMX|8Qa!YDD{9NYYK
z%JZrk2!Ss(Ra@NRW<7U#%8SZdWMFDU@;q<}%F{|6n#Y|?FaBgV$7!@|=NSVoxlJI4G-G(rn}bh|?mKkaBF$-Yr
zA;t0r?^5Nz;u6gwxURapQ0$(-su(S+24Ffmx-aP(@8d>GhMtC5x*iEXIKthE*mk$`
zOj!Uri|EAb4>03C1xaC#(q_I<;t}U7;1JqISVHz3tO{)
zD(Yu@=>I9FDmDtUiWt81;BeaU{_=es^#QI7>uYl@e$$lGeZ~Q(f$?^3>$<<{n`Bn$
zn8bamZlL@6r^RZHV_c5WV7m2(G6X|OI!+04eAnNA5=0v1Z3lxml2#p~Zo57ri;4>;#16sSXXEK#QlH>=b$inEH0`G#<_
zvp;{+iY)BgX$R!`HmB{S&1TrS=V;*5SB$7*&%4rf_2wQS2ed2E%Wtz@y$4ecq4w<)
z-?1vz_&u>s?BMrCQG6t9;t&gvYz;@K@$k!Zi=`tgpw*v-#U1Pxy%S9%52`uf$XMv~
zU}7FR5L4F<#9i%$P=t29nX9VBVv)-y7S$ZW;gmMVBvT$BT8d}B#XV^@;wXErJ-W2A
zA=JftQRL>vNO(!n4mcd3O27bHYZD!a0kI)6b4hzzL9)l-OqWn)a~{VP;=Uo|D~?AY
z#8grAAASNOkFMbRDdlqVUfB;GIS-B-_YXNlT_8~a|LvRMVXf!<^uy;)d$^OR(u)!)
zHHH=FqJF-*BXif9uP~`SXlt0pYx|W&7jQnCbjy|8b-i>NWb@!6bx;1L&$v&+!%9BZ
z0nN-l`&}xvv|wwxmC-ZmoFT_B#BzgQZxtm|4N+|;+(YW&Jtj^g!)iqPG++Z%x0LmqnF875%Ry&2QcCamx!T@FgE@H
zN39P6e#I5y6Yl&K4eUP{^biV`u9{&CiCG#U6xgGRQr)zew;Z%x+
z-gC>y%gvx|dM=OrO`N@P+h2klPtbYvjS!mNnk4yE0+I&YrSRi?F^plh}hIp_+OKd#o7ID;b;%*c0ES
z!J))9D&YufGIvNVwT|qsGWiZAwFODugFQ$VsNS%gMi8OJ#i${a4!E3<-4Jj<9SdSY
z&xe|D0V1c`dZv+$8>(}RE|zL{E3
z-$5Anhp#7}oO(xm#}tF+W=KE*3(xxKxhBt-uuJP}`_K#0A<
zE%rhMg?=b$ot^i@BhE3&)bNBpt1V*O`g?8hhcsV-n#=|9wGCOYt8`^#T&H7{U`yt2
z{l9Xl5CVsE=`)w4A^%PbIR6uG_5Ww9k`=q<@t9Bu662;o{8PTjDBzzbY#tL;$wrpjONqZ{^Ds4oanFm~uyPm#y1Ll3(H57YDWk9TlC
zq;kebC!e=`FU&q2ojmz~GeLxaJHfs0#F%c(i+~gg$#$XOHIi@1mA72g2pFEdZSvp}m0zgQb5u2?tSRp#oo!bp`FP}<
zaK4iuMpH+Jg{bb7n9N6eR*NZfgL7QiLxI
zk6{uKr>xxJ42sR%bJ%m8QgrL|fzo9@?9eQiMW8O`j3teoO_R8cXPe_XiLnlYkE3U4
zN!^F)Z4ZWcA8gekEPLtFqX-Q~)te`LZnJK_pgdKs)Dp50
zdUq)JjlJeELskKg^6KY!sIou-HUnSFRsqG^lsHuRs`Z{f(Ti9eyd3cwu*Kxp?Ws7l
z3cN>hGPXTnQK@qBgqz(n*qdJ2wbafELi?b90fK~+#XIkFGU4+HihnWq;{{)1J
zv*Txl@GlnIMOjzjA1z%g?GsB2(6Zb-8fooT*8b0KF2CdsIw}~Hir$d3TdVHRx1m3c
z4C3#h@1Xi@{t4zge-#B6jo*ChO%s-R%+9%-E|y<*4;L>$766RiygaLR?X%izyqMXA
zb|N=Z-0PSFeH;W6aQ3(5VZWVC>5Ibgi&cj*c%_3=o#VyUJv*
zM&bjyFOzlaFq;ZW(q?|yyi|_zS%oIuH^T*MZ6NNXBj;&yM3eQ7!CqXY?`7+*+GN47
zNR#%*ZH<^x{(0@hS8l{seisY~IE*)BD+R6^OJX}<2HRzo^fC$n>#yTOAZbk4%=Bei=JEe=o$jm`or0YDw*G?d>
z=i$eEL7^}_?UI^9$;1Tn9b>$KOM@NAnvWrcru)r`?LodV%lz55O3y(%FqN;cKgj7t
zlJ7BmLTQ*NDX#uelGbCY>k+&H*iSK?x-{w;f5G%%!^e4QT9z<_0vHbXW^MLR}
zeC*jezrU|{*_F`I0mi)9=sUj^G03i@MjXx@ePv@(Udt2CCXVOJhRh4yp~fpn>ssHZ
z?k(C>2uOMWKW5FVsBo#Nk!oqYbL`?#i~#!{3w^qmCto05uS|hKkT+iPrC-}hU_nbL
zO622#mJupB21nChpime}&M1+whF2XM?prT-Vv)|EjWYK(yGYwJLRRMCkx;nMSpu?0
zNwa*{0n+Yg6=SR3-S&;vq=-lRqN`s9~#)OOaIcy3GZ&~l4g@2h|
zThAN#=dh{3UN7Xil;nb8@%)wx5t!l
z0RSe_yJQ+_y#qEYy$B)m2yDlul^|m9V2Ia$1CKi6Q19~GTbzqk*{y4;ew=_B4V8zw
zScDH&QedBl&M*-S+bH}@IZUSkUfleyM45G>CnYY{hx8J9q}ME?Iv%XK`#DJRNmAYt
zk2uY?A*uyBA=nlYjkcNPMGi*552=*Q>%l?gDK_XYh*Rya_c)ve{=ps`QYE0n!n!)_$TrGi_}J|>1v}(VE7I~aP-wns#?>Y
zu+O7`5kq32zM4mAQpJ50vJsUDT_^s&^k-llQMy9!@wRnxw@~kXV6{;z_wLu3i=F3m
z&eVsJmuauY)8(<=pNUM5!!fQ4uA6hBkJoElL1asWNkYE#qaP?a+biwWw~vB48PRS7
zY;DSHvgbIB$)!uJU)xA!yLE*kP0owzYo`v@wfdux#~f!dv#uNc_$SF@Qq9#3q5R
zfuQnPPN_(z;#X#nRHTV>TWL_Q%}5N-a=PhkQ^GL+$=QYfoDr2JO-zo#j;mCsZVUQ)
zJ96e^OqdLW6b-T@CW@eQg)EgIS9*k`xr$1yDa1NWqQ|gF^2pn#dP}3NjfRYx$pTrb
zwGrf8=bQAjXx*8?du*?rlH2x~^pXjiEmj^XwQo{`NMonBN=Q@Y21!H)D(
zA~%|VhiTjaRQ%|#Q9d*K4j~JDXOa4wmHb0L)hn*;Eq#*GI}@#ux4}bt+olS(M4$>c
z=v8x74V_5~xH$sP+LZCTrMxi)VC%(Dg!2)KvW|Wwj@pwmH6%8zd*x0rUUe$e(Z%AW
z@Q{4LL9#(A-9QaY2*+q8Yq2P`pbk3!V3mJkh3uH~uN)+p?67d(r|Vo0CebgR#u}i?
zBxa^w%U|7QytN%L9bKaeYhwdg7(z=AoMeP0)M3XZA)NnyqL%D_x-(jXp&tp*`%Qsx
z6}=lGr;^m1<{;e=QQZ!FNxvLcvJVGPkJ63at5%*`W?46!6|5FHYV0qhizSMT>Zoe8
zsJ48kb2@=*txGRe;?~KhZgr-ZZ&c0rNV7eK+h$I-UvQ=552@psVrvj#Ys@EU4p8`3
zsNqJu-o=#@9N!Pq`}<=|((u)>^r0k^*%r<{YTMm+mOPL>EoSREuQc-e2~C#ZQ&Xve
zZ}OUzmE4{N-7cqhJiUoO_V#(nHX11fdfVZJT>|6CJGX5RQ+Ng$Nq9xs-C86-)~`>p
zW--X53J`O~vS{WWjsAuGq{K#8f#2iz`
zzSSNIf6;?5sXrHig%X(}0q^Y=eYwvh{TWK-fT>($8Ex>!vo_oGFw#ncr{vmERi^m7lRi%8Imph})ZopLoIWt*eFWSPuBK
zu>;Pu2B#+e_W|IZ0_Q9E9(s@0>C*1ft`V{*UWz^K<0Ispxi@4umgGXW!j%7n+NC~*
zBDhZ~k6sS44(G}*zg||X#9Weto;u*Ty;fP!+v*7be%cYG|yEOBomch#m8Np!Sw`L)q+T`
zmrTMf2^}7j=RPwgpO9@eXfb{Q>GW#{X=+xt`AwTl!=TgYm)aS2x5*`FSUaaP_I{Xi
zA#irF%G33Bw>t?^1YqX%czv|JF0+@Pzi%!KJ?z!u$A`Catug*tYPO`_Zho5iip0@!
z;`rR0-|Ao!YUO3yaujlSQ+j-@*{m9dHLtve!sY1Xq_T2L3&=8N;n!!Eb8P0Z^p4PL
zQDdZ?An2uzbIakOpC|d@=xEA}v-srucnX3Ym{~I#Ghl~JZU(a~Ppo9Gy1oZH&Wh%y
zI=KH_s!Lm%lAY&`_KGm*Ht)j*C{-t}Nn71drvS!o|I|g>ZKjE3&Mq0TCs6}W;p>%M
zQ(e!h*U~b;rsZ1OPigud>ej=&hRzs@b>>sq6@Yjhnw?M26YLnDH_Wt#*7S$-BtL08
zVyIKBm$}^vp?ILpIJetMkW1VtIc&7P3z0M|{y5gA!Yi5x4}UNz5C0Wdh02!h
zNS>923}vrkzl07CX`hi)nj-B?#n?BJ2Vk0zOGsF<~{Fo7OMCN_85daxhk*pO}x_8;-h>}pcw26V6CqR-=x2vRL?GB#y%tYqi;J}kvxaz}*iFO6YO0ha6!fHU9#UI2Nv
z_(`F#QU1B+P;E!t#Lb)^KaQYYSewj4L!_w$RH%@IL-M($?DV@lGj%3ZgVdHe^q>n(x
zyd5PDpGbvR-&p*eU9$#e5#g3-W_Z@loCSz}f~{94>k6VRG`e5lI=SE0AJ7Z_+=nnE
zTuHEW)W|a8{fJS>2TaX
zuRoa=LCP~kP)kx4L+OqTjtJOtXiF=y;*eUFgCn^Y@`gtyp?n14PvWF=zhNGGsM{R-
z^DsGxtoDtx+g^hZi@E2Y(msb-hm{dWiHdoQvdX88EdM>^DS#f}&kCGpPFDu*KjEpv$FZtLpeT>@)mf|z#ZWEsueeW~hF78Hu
zfY9a+Gp?<)s{Poh_qdcSATV2oZJo$OH~K@QzE2kCADZ@xX(;
z)0i=kcAi%nvlsYagvUp(z0>3`39iKG9WBDu3z)h38p|hLGdD+Khk394PF3qkX!02H
z#rNE`T~P9vwNQ_pNe0toMCRCBHuJUmNUl)KFn6Gu2je+p>{<9^oZ4Gfb!)rLZ3CR3
z-o&b;Bh>51JOt=)$-9+Z!P}c@cKev_4F1ZZGs$I(A{*PoK!6j@ZJrAt
zv2LxN#p1z2_0Ox|Q8PVblp9N${kXkpsNVa^tNWhof)8x8&VxywcJz#7&P&d8vvxn`
zt75mu>yV=Dl#SuiV!^1BPh5R)`}k@Nr2+s8VGp?%Le>+fa{3&(XYi~{k{
z-u4#CgYIdhp~GxLC+_wT%I*)tm4=w;ErgmAt<5i6c~)7JD2olIaK8by{u-!tZWT#RQddptXRfEZxmfpt|@bs<*uh?Y_<
zD>W09Iy4iM@@80&!e^~gj!N`3lZwosC!!ydvJtc0nH==K)v#ta_I}4Tar|;TLb|+)
zSF(;=?$Z0?ZFdG6>Qz)6oPM}y1&zx_Mf`A&chb
znSERvt9%wdPDBIU(07X+CY74u`J{@SSgesGy~)!Mqr#yV6$=w-dO;C`JDmv=YciTH
zvcrN1kVvq|(3O)NNdth>X?ftc`W2X|FGnWV%s})+uV*bw>aoJ#0|$pIqK6K0Lw!@-
z3pkPbzd`ljS=H2Bt0NYe)u+%kU%DWwWa>^vKo=lzDZHr>ruL5Ky&#q7davj-_$C6J
z>V8D-XJ}0cL$8}Xud{T_{19#W5y}D9HT~$&YY-@=Th219U+#nT{tu=d|B)3K`pL53
zf7`I*|L@^dPEIDJkI3_oA9vsH7n7O}JaR{G~8
zfi$?kmKvu20(l`dV7=0S43VwVKvtF!7njv1Q{Ju#ysj=|dASq&iTE8ZTbd-iiu|2&
zmll%Ee1|M?n9pf~?_tdQ<7%JA53!ulo1b^h#s|Su2S4r{TH7BRB3iIOiX5|vc^;5(
zKfE1+ah18YA9o1EPT(AhBtve5(%GMbspXV)|1wf5VdvzeYt8GVGt0e*3|ELBhwRaO
zE|yMhl;Bm?8Ju3-;DNnxM3Roelg`^!S%e({t)jvYtJCKPqN`LmMg^V&S
z$9OIFLF$%Py~{l?#ReyMzpWixvm(n(Y^Am*#>atEZ8#YD&?>NUU=zLxOdSh0m6mL?
z_twklB0SjM!3+7U^>-vV=KyQZI-6<(EZiwmNBzGy;Sjc#hQk%D;bay$v#zczt%mFCHL*817X4R;E$~N5(N$1Tv{VZh7d4mhu?HgkE>O+^-C*R@
zR0ima8PsEV*WFvz`NaB+lhX3&LUZcWWJJrG7ZjQrOWD%_jxv=)`cbCk
zMgelcftZ%1-p9u!I-Zf_LLz{hcn5NRbxkWby@sj2XmYfAV?iw^0?hM<$&ZDctdC`;
zsL|C-7d;w$z2Gt0@hsltNlytoPnK&$>ksr(=>!7}Vk#;)Hp)LuA7(2(Hh(y3LcxRY
zim!`~j6`~B+sRBv4
z<#B{@38kH;sLB4eH2+8IPWklhd25r5j2VR}YK$lpZ%7eVF5CBr#~=kUp`i
zlb+>Z%i%BJH}5dmfg1>h7U5Q(-F{1d=aHDbMv9TugohX5lq#szPAvPE|HaokMQIi_
zTcTNsO53(oX=hg2w!XA&+qP}nwr$(C)pgG8emS@Mf7m0&*kiA!wPLS`88c=aD$niJ
zp?3j%NI^uy|5*MzF`k4hFbsyQZ@wu!*IY+U&&9PwumdmyfL(S0#!2RFfmtzD3m9V7
zsNOw9RQofl-XBfKBF^~~{oUVouka#r3EqRf=SnleD=r1Hm@~`y8U7R)w16fgHvK-6?-TFth)f3WlklbZh+}0
zx*}7oDF4U^1tX4^$qd%987I}g;+o0*$Gsd=J>~Uae~XY6UtbdF)J8TzJXoSrqHVC)
zJ@pMgE#;zmuz?N2MIC+{&)tx=7A%$yq-{GAzyz
zLzZLf=%2Jqy8wGHD;>^x57VG)sDZxU+EMfe0L{@1DtxrFOp)=zKY1i%HUf~Dro#8}
zUw_Mj10K7iDsX}+fThqhb@&GI7PwONx!5z;`yLmB_92z0sBd#HiqTzDvAsTdx+%W{
z2YL#U=9r!@3pNXMp_nvximh+@HV3psUaVa-lOBekVuMf1RUd26~P*|MLouQrb}XM-bEw(UgQxMI6M&l3Nha
z{MBcV=tl(b_4}oFdAo}WX$~$Mj-z70FowdoB{TN|h2BdYs?$imcj{IQpEf9q
z)rzpttc0?iwopSmEoB&V!1aoZqEWEeO-MKMx(4iK7&Fhc(94c
zdy}SOnSCOHX+A8q@i>gB@mQ~Anv|yiUsW!bO9hb&5JqTfDit9X6xDEz*mQEiNu$ay
zwqkTV%WLat|Ar+xCOfYs0UQNM`sdsnn*zJr>5T=qOU4#Z(d90!IL76DaHIZeWKyE1
zqwN%9+~lPf2d7)vN2*Q?En?DEPcM+GQwvA<#;X3v=fqsxmjYtLJpc3)A8~*g(KqFx
zZEnqqruFDnEagXUM>TC7ngwKMjc2Gx%#Ll#=N4qkOuK|;>4%=0Xl7k`E69@QJ-*Vq
zk9p5!+Ek#bjuPa<@Xv7ku4uiWo|_wy)6tIr`aO!)h>m5zaMS-@{HGIXJ0UilA7*I}
z?|NZ!Tp8@o-lnyde*H+@8IHME8VTQOGh96&XX3E+}OB
zA>VLAGW+urF&J{H{9Gj3&u+Gyn?JAVW84_XBeGs1;mm?2SQm9^!3UE@(_FiMwgkJI
zZ*caE={wMm`7>9R?z3Ewg!{PdFDrbzCmz=RF<@(yQJ_A6?PCd_MdUf5vv6G#9Mf)i#G
z($OxDT~8RNZ>1R-vw|nN699a}MQN4gJE_9gA-0%>a?Q<9;f3ymgoi$OI!=aE6Elw
z2I`l!qe-1J$T$X&x9Zz#;3!P$I);jdOgYY1nqny-k=4|Q4F!mkqACSN`blRji>z1`
zc8M57`~1lgL+Ha%@V9_G($HFBXH%k;Swyr>EsQvg%6rNi){Tr&+NAMga2;@85531V
z_h+h{jdB&-l+%aY{$oy2hQfx`d{&?#psJ78iXrhrO)McOFt-o80(W^LKM{Zw93O}m
z;}G!51qE?hi=Gk2VRUL2kYOBRuAzktql%_KYF4>944&lJKfbr+uo@)hklCHkC=i)E
zE*%WbWr@9zoNjumq|kT<9Hm*%&ahcQ)|TCjp@uymEU!&mqqgS;d|v)QlBsE0Jw|+^
zFi9xty2hOk?rlGYT3)Q7i4k65@$RJ-d<38o<`}3KsOR}t8sAShiVWevR8z^Si4>dS
z)$&ILfZ9?H#H&lumngpj7`|rKQQ`|tmMmFR+y-9PP`;-425w+#PRKKnx7o-Rw8;}*Ctyw
zKh~1oJ5+0hNZ79!1fb(t7IqD8*O1I_hM;o*V~vd_LKqu7c_thyLalEF8Y3oAV=ODv
z$F_m(Z>ucO(@?+g_vZ`S9+=~Msu6W-V5I-V6h7->50nQ@+TELlpl{SIfYYNvS6T6D
z`9cq=at#zEZUmTfTiM3*vUamr!OB~g$#?9$&QiwDMbSaEmciWf3O2E8?oE0ApScg38hb&iN%K+kvRt#d))-tr^
zD+%!d`i!OOE3in0Q_HzNXE!JcZ<0;cu6P_@;_TIyMZ@Wv!J
z)HSXAYKE%-oBk`Ye@W3ShYu-bfCAZ}1|J16hFnLy
z?Bmg2_kLhlZ*?`5R8(1%Y?{O?xT)IMv{-)VWa9#1pKH|oVRm4!lLmls=u}Lxs44@g^Zwa0Z_h>Rk<(_mHN47=Id4oba
zQ-=qXGz^cNX(b*=NT0<^23+hpS&#OXzzVO@$Z2)D`@oS=#(s+eQ@+FSQcpXD@9npp
zlxNC&q-PFU6|!;RiM`?o&Sj&)<4xG3#ozRyQxcW4=EE;E)wcZ&zUG*5elg;{9!j}I
z9slay#_bb<)N!IKO16`n3^@w=Y%duKA-{8q``*!w9SW|SRbxcNl50{k&CsV@b`5Xg
zWGZ1lX)zs_M65Yt&lO%mG0^IFxzE_CL_6$rDFc&#xX5EXEKbV8E2FOAt>Ka@e0aHQ
zMBf>J$FLrCGL@$VgPKSbRkkqo>sOXmU!Yx+Dp7E3SRfT`v~!mjU3qj-*!!YjgI*^)
z+*05x78FVnVwSGKr^A|FW*0B|HYgc{c;e3Ld}z4rMI7hVBKaiJRL_e$rxDW^8!nGLdJ<7ex9dFoyj|EkODflJ#Xl`j&bTO%=$v)c+gJsLK_%H3}A_}
z6%rfG?a7+k7Bl(HW;wQ7BwY=YFMSR3J43?!;#~E&)-RV_L!|S%XEPYl&#`s!LcF>l
zn&K8eemu&CJp2hOHJKaYU#hxEutr+O161ze&=j3w12)UKS%+LAwbjqR8sDoZHnD=m0(p62!zg
zxt!Sj65S?6WPmm
zL&U9c`6G}T`irf=NcOiZ!V)qhnvMNOPjVkyO2^CGJ+dKTnNAPa?!AxZEpO7yL_LkB
zWpolpaDfSaO-&Uv=dj7`03^BT3_HJOAjn~X;wz-}03kNs@D^()_{*BD|0mII!J>5p
z1h06PTyM#3BWzAz1FPewjtrQfvecWhkRR=^gKeFDe$rmaYAo!np6iuio3>$w?az$E
zwGH|zy@OgvuXok}C)o1_&N6B3P7ZX&-yimXc1hAbXr!K&vclCL%hjVF$yHpK6i_Wa
z*CMg1RAH1(EuuA01@lA$sMfe*s@9-
z$jNWqM;a%d3?(>Hzp*MiOUM*?8eJ$=(0fYFis!YA;0m8s^Q=M0Hx4ai3eLn%CBm14
zOb8lfI!^UAu_RkuHmKA-8gx8Z;##oCpZV{{NlNSe<i;9!MfIN!&;JI-{|n{(A19|s
z9oiGesENcLf@NN^9R0uIrgg(46r%kjR{0SbnjBqPq()wDJ@LC2{kUu_j$VR=l`#RdaRe
zxx;b7bu+@IntWaV$si1_nrQpo*IWGLBhhMS13qH
zTy4NpK<-3aVc;M)5v(8JeksSAGQJ%6(PXGnQ-g^GQPh|xCop?zVXlFz>42%rbP@jg
z)n)%
zM9anq5(R=uo4tq~W7wES$g|Ko
z1iNIw@-{x@xKxSXAuTx@SEcw(%E49+JJCpT(y=d+n9PO0Gv1SmHkYbcxPgDHF}4iY
zkXU4rkqkwVBz<{mcv~A0K|{zpX}aJcty9s(u-$je2&=1u(e#Q~UA{gA!f;0EAaDzdQ=}x7g(9gWrWYe~
zV98=VkHbI!5Rr;+SM;*#tOgYNlfr7;nLU~MD^jSdSpn@gYOa$TQPv+e8DyJ&>aInB
zDk>JmjH=}<4H4N4z&QeFx>1VPY8GU&^1c&71T*@2#dINft%ibtY(bAm%<2YwPL?J0Mt{
z7l7BR718o5=v|jB!<7PDBafdL>?cCdVmKC;)MCOobo5edt%RTWiReAMaIU5X9h`@El0sR&Z
z7Ed+FiyA+QAyWn
zf7=%(8XpcS*C4^-L24TBUu%0;@s!Nzy{e95qjgkzElf0#ou`sYng<}wG1M|L?
zKl6ITA1X9mt6o@S(#R3B{uwJI8O$&<3{+A?T~t>Kapx6#QJDol6%?i-{b1aRu?&9B
z*W@$T*o&IQ&5Kc*4LK_)MK-f&Ys^OJ9FfE?0SDbAPd(RB)Oju#S(LK)?EVandS1qb#KR;OP|86J?;TqI%E8`vszd&-kS%&~;1Als=NaLzRNnj4q=+
zu5H#z)BDKHo1EJTC?Cd_oq0qEqNAF8PwU7fK!-WwVEp4~4g
z3SEmE3-$ddli))xY9KN$lxEIfyLzup@utHn=Q{OCoz9?>u%L^JjClW$M8OB`txg4r6Q-6UlVx3tR%%Z!VMb6#|BKRL`I))#g
zij8#9gk|p&Iwv+4s+=XRDW7VQrI(+9>DikEq!_6vIX8$>poDjSYIPcju%=qluSS&j
zI-~+ztl1f71O-B+s7Hf>AZ#}DNSf`7C7*)%(Xzf|ps6Dr7IOGSR417xsU=Rxb
z1pgk9vv${17h7mZ{)*R{mc%R=!i}8EFV9pl8V=nXCZruBff`$cqN3tpB&RK^$yH!A8RL
zJ5KltH$&5%xC7pLZD}6wjD2-uq3&XL8CM$@V9jqalF{mvZ)c4Vn?xXbvkB(q%xbSdjoXJXanVN@I;8I`)XlBX@6BjuQKD28Jrg05}
z^ImmK-Ux*QMn_A|1ionE#AurP8Vi?x)7jG?v#YyVe_9^up@6^t_Zy^T1yKW*t*
z&Z0+0Eo(==98ig=^`he&G^K$I!F~1l~gq}%o5#pR6?T+
zLmZu&_ekx%^nys<^tC@)s$kD`^r8)1^tUazRkWEYPw0P)=%cqnyeFo3nW
zyV$^0DXPKn5^QiOtOi4MIX^#3wBPJjenU#2OIAgCHPKXv$OY=e;yf7+_vI7KcjKq%
z?RVzC24ekYp2lEhIE^J$l&wNX0<}1Poir8PjM`m#zwk-AL0w6WvltT}*JN8WFmtP_
z6#rK7$6S!nS!}PSFTG6AF7giGJw5%A%14ECde3x95(%>&W3zUF!8x5%*h-zk8b@Bz
zh`7@ixoCVCZ&$$*YUJpur90Yg0X-P82>c~NMzDy7@Ed|6(#`;{)%t7#Yb>*DBiXC3
zUFq(UDFjrgOsc%0KJ_L;WQKF0q!MINpQzSsqwv?#Wg+-NO;
z84#4nk$+3C{2f#}TrRhin=Erdfs77TqBSvmxm0P?01Tn@V(}gI_ltHRzQKPyvQ2=M
zX#i1-a(>FPaESNx+wZ6J{^m_q3i})1n~JG80c<%-Ky!ZdTs8cn{qWY%x%X^27-Or_
z`KjiUE$OG9K4lWS16+?aak__C*)XA{
z6HmS*8#t_3dl}4;7ZZgn4|Tyy1lOEM1~6Qgl(|BgfQF{Mfjktch
zB5kc~4NeehRYO%)3Z!FFHhUVVcV@uEX$eft5Qn&V3g;}hScW_d)K_h5i)vxjKCxcf
zL>XlZ^*pQNuX*RJQn)b6;blT3<7@Ap)55)aK3n-H08GIx65W
zO9B%gE%`!fyT`)hKjm-&=on)l&!i-QH+mXQ&lbXg0d|F{Ac#U;6b$pqQcpqWSgAPo
zmr$gOoE*0r#7J=cu1$5YZE%uylM!i3L{;GW{ae9uy)+EaV>GqW6QJ)*B2)-W`|kLL
z)EeeBtpgm;79U_1;Ni5!c^0RbG8yZ0W98JiG~TC8rjFRjGc6Zi8BtoC);q1@8h7UV
zFa&LRzYsq%6d!o5-yrqyjXi>jg&c8bu}{Bz9F2D(B%nnuVAz74zmBGv)PAdFXS2(A
z=Z?uupM2f-ar0!A)C6l2o8a|+uT*~huH)!h3i!&$
zr>76mt|lwexD(W_+5R{e@2SwR15lGxsnEy|gbS-s5?U}l*kcfQlfnQKo5=LZXizrL
zM=0ty+$#f_qGGri-*t@LfGS?%7&LigUIU#JXvwEdJZvIgPCWFBTPT`@Re5z%%tRDO
zkMlJCoqf2A=hkU7Ih=IxmPF~fEL90)u76nfFRQwe{m7b&Ww$pnk~$4Lx#s9|($Cvt
ze|p{Xozhb^g1MNh-PqS_dLY|Fex4|rhM#lmzq&mhebD$5P>M$eqLoV|z=VQY{)7&sR#tW
zl(S1i!!Rrg7kv+V@EL51PGpm511he%MbX2-Jl+DtyYA(0gZyZQjPZP@`SAH{n&25@
zd)emg(p2T3$A!Nmzo|%=z%AhLX)W4hsZNFhmd4<1l6?b3&Fg)G(Zh%J{Cf8Q;?_++
zgO7O<(-)H|Es@QqUgcXNJEfC-BCB~#dhi6ADVZtL!)Mx|u7>ukD052z!QZ5UC-+rd
zYXWNRpCmdM{&?M9OMa;OiN{Y#0+F>lBQ=W@M;OXq;-7v3niC$pM8p!agNmq7F04;|
z@s-_98JJB&s`Pr6o$KZ=8}qO*7m6SMp7kVmmh$jfnG{r@O(auI7Z^jj!x}NTLS9>k
zdo}&Qc2m4Ws3)5qFw#<$h=g%+QUKiYog33bE)e4*H~6tfd42q+|FT5+vmr6Y$6HGC
zV!!q>B`1Ho|6E|D<2tYE;4`8WRfm2#AVBBn%_W)mi(~x@g;uyQV3_)~!#A6kmFy0p
zY~#!R1%h5E{5;rehP%-#kjMLt*{g((o@0-9*8lKVu+t~CtnOxuaMgo2ssI6@kX09{
zkn~q8Gx<6T)l}7tWYS#q0&~x|-3ho@l}qIr79qOJQcm&Kfr7H54=BQto0)vd1A_*V
z)8b2{xa5O^u95~TS=HcJF5b9gMV%&M6uaj<>E
zPNM~qGjJ~xbg%QTy#(hPtfc46^nN=Y_GmPYY_hTL{q`W3NedZyRL^kgU@Q$_KMAjEzz*eip`3u6AhPDcWXzR=Io5EtZRPme>#K9
z4lN&87i%YYjoCKN_z9YK+{fJu{yrriba#oGM|2l$ir017UH86Eoig3x+;bz32R*;n
zt)Eyg#PhQbbGr^naCv0?H<=@+Poz)Xw*3Gn00qdSL|zGiyYKOA0CP%qk=rBAlt~hr
zEvd3Z4nfW%g|c`_sfK$z8fWsXTQm@@eI-FpLGrW<^PIjYw)XC-xFk+M<6>MfG;WJr
zuN}7b;p^`uc0j(73^=XJcw;|D4B(`)Flm|qEbB?>qBBv2V?`mWA?Q3yRdLkK7b}y&
z+!3!JBI{+&`~;%Pj#n&&y+<;IQzw5SvqlbC+V=kLZLAHOQb
zS{{8E&JXy1p|B&$K!T*GKtSV^{|Uk;`oE*F;?@q1dX|>|KWb@|Dy*lbGV0Gx;gpA$
z*N16`v*gQ?6Skw(f^|SL;;^ox6jf2AQ$Zl?gvEV&H|-ep*hIS@0TmGu1X1ZmEPY&f
zKCrV{UgRAiNU*=+Uw%gjIQhTAC@67m)6(_D+N>)(^gK74F%M2NUpWpho}aq|Kxh$3
zz#DWOmQV4Lg&}`XTU41Z|P~5;wN2c?2L{a=)Xi~!m#*=22c~&AW
zgG#yc!_p##fI&E{xQD9l#^x|9`wSyCMxXe<3^kDIkS0N>=oAz7b`@M>aT?e$IGZR;
zS;I{gnr4cS^u$#>D(sjkh^T6_$s=*o%vNLC5+6J=HA$&0v6(Y1lm|RDn&v|^CTV{=
zjVrg_S}WZ|k=zzp>DX08AtfT@LhW&}!rv^);ds7|mKc5^zge_Li>FTNFoA8dbk@K$
zuuzmDQRL1leikp%m}2_`A7*7=1p2!HBlj0KjPC|WT?5{_aa%}rQ+9MqcfXI0NtjvXz1U)|H>0{6^JpHspI4MfXjV%1Tc1O!tdvd{!IpO+@
z!nh()i-J3`AXow^MP!oVLVhVW&!CDaQxlD9b|Zsc%IzsZ@d~OfMvTFXoEQg9Nj|_L
zI+^=(GK9!FGck+y8!KF!nzw8ZCX>?kQr=p@7EL_^;2Mlu1e7@ixfZQ#pqpyCJ```(m;la2NpJNoLQR};i4E;hd+|QBL@GdQy(Cc
zTSgZ)4O~hXj86x<7&ho5ePzDrVD`XL7{7PjjNM1|6d5>*1hFPY!E(XDMA+AS;_%E~
z(dOs)vy29&I`5_yEw0x{8Adg%wvmoW&Q;x?5`HJFB@KtmS+o0ZFkE@f)v>YYh-z&m
z#>ze?@JK4oE7kFRFD%MPC@x$^p{aW}*CH9Y_(oJ~St#(2)4e-b34D>VG6giMGFA83
zpZTHM2I*c8HE}5G;?Y7RXMA2k{Y?RxHb2
zZFQv?!*Kr_q;jt3`{?B5Wf}_a7`roT&m1BN9{;5Vqo6JPh*gnN(gj}#=A$-F(SRJj
zUih_ce0f%K19VLXi5(VBGOFbc(YF
zLvvOJl+W<}>_6_4O?LhD>MRGlrk;~J{S#Q;Q9F^;Cu@>EgZAH=-5fp02(VND(v#7n
zK-`CfxEdonk!!65?3Ry(s$=|CvNV}u$5YpUf?9kZl8h@M!AMR7RG<9#=`_@qF@})d
ztJDH>=F!5I+h!4#^DN6C$pd6^)_;0Bz7|#^edb9_qFg&eI}x{Roovml5^Yf5;=ehZ
zGqz-x{I`J$ejkmGTFipKrUbv-+1S_Yga=)I2ZsO16_ye@!%&Op^6;#*Bm;=I^#F;?
z27Sz-pXm4x-ykSW*3`)y4$89wy6dNOP$(@VYuPfb97XPDTY2FE{Z+{6=}LLA23mAc
zskjZJ05>b)I7^SfVc)LnKW(&*(kP*jBnj>jtph`ZD@&30362cnQpZW8juUWcDnghc
zy|tN1T6m?R7E8iyrL%)53`ymXX~_;#r${G`4Q(&7=m7b#jN%wdLlS0lb~r9RMdSuU
zJ{~>>zGA5N`^QmrzaqDJ(=9y*?@HZyE!yLFONJO!8q5Up#2v>fR6CkquE$PEcvw5q
zC8FZX!15JgSn{Gqft&>A9r0e#be^C<%)psE*nyW^e>tsc8s4Q}OIm})rOhuc{3o)g1r>Q^w5mas)
zDlZQyjQefhl0PmH%cK05*&v{-M1QCiK=rAP%c#pdCq_StgDW}mmw$S&K6ASE=`u4+
z5wcmtrP27nAlQCc4qazffZoFV7*l2=Va}SVJD6CgRY^=5Ul=VYLGqR7H^LHA;H^1g}ekn=4K8SPRCT+pel*@jUXnLz+AIePjz@mUsslCN2
z({jl?BWf&DS+FlE5Xwp%5zXC7{!C=k9oQLP5B;sLQxd`pg+B@qPRqZ6FU(k~QkQu{
zF~5P=kLhs+D}8qqa|CQo2=cv$wkqAzBRmz_HL9(HRBj&73T@+B{(zZahlkkJ>EQmQ
zenp59dy+L;sSWYde!z_W+I~-+2Xnm;c;wI_wH=RTgxpMlCW@;Us*0}L74J#E
z8XbDWJGpBscw?W$&ZxZNxUq(*DKDwNzW7_}AIw$HF6Ix|;AJ3t6lN=v(c9=?n9;Y0
zK9A0uW4Ib9|Mp-itnzS#5in=Ny+XhGO8#(1_H4%Z6yEBciBiHfn*h;^r9gWb^$UB4
zJtN8^++GfT`1!WfQt#3sXGi-p<~gIVdMM<#ZZ0e_kdPG%Q5s20NNt3Jj^t$(?5cJ$
zGZ#FT(Lt>-0fP4b5V3az4_byF12k%}Spc$WsRydi&H|9H5u1RbfPC#lq=z#a9W(r1
z!*}KST!Yhsem0tO#r!z`znSL-=NnP~f(pw-sE+Z$e7i7t9nBP^5ts1~WFmW+j+<@7
zIh@^zKO{1%Lpx^$w8-S+T_59v;%N;EZtJzcfN%&@(Ux5
z@YzX^MwbbXESD*d(&qT7-eOHD6iaH-^N>p2sVdq&(`C$;?#mgBANIc5$r|
z^A$r)@c{Z}N%sbfo?T`tTHz9-YpiMW?6>kr&W9t$Cuk{q^g1<$I~L
zo++o2!!$;|U93cI#p4hyc!_Mv2QKXxv419}Ej#w#%N+YIBDdnn8;35!f2QZkUG?8O
zpP47Wf9rnoI^^!9!dy~XsZ&!DU4bVTAi3Fc<9$_krGR&3TI=Az9uMgYU5dd~ksx+}
zP+bs9y+NgEL>c@l>H1R%@>5SWg2k&@QZL(qNUI4XwDl6(=!Q^U%o984{|0e|mR$p+
z9BcwttR#7?As?@Q{+j?K6H7R71PuiA^Dl$=f47nUKL|koCwutc_P<-m{|Al3C~o7w
z=4S=}s5LcJFT1zjS)+10X_r$74`K78pz!nGGH%JV%w75!YSIt#hT7}}K>+@{{a+Im
z5p#6%^X*txY?}|T17xWW*sa^?G2QHt#@tlcw0GIcy;|NR2vaCBDvn=`h)1il7E5Rx
z%)mA4$`$OZx)NF5vXZnaJ1)*cA6ryx6Ll~t!LzhxvcTedxT;>JS&e=?-&DXUPaQ2~
zH*69ezE`hgV{K-|0z|m~ld}=X^-Ob={wpex&}*+Rz{gx)G}gn!C_VN{UN=>^EV=Xc
zr$-HO09cW&p4^M}V3yBjTP_xrVcc8iU_^Y-JD~(bgw*@GXGB1gYKz5DWO+O`>})|N
zWrC)MR93yA)3{&27-M)TJB6Ml3~?zZg#mYsF=#OSTaw&K
z@hBftpt+2l@)YK@|3DvTjl(8wZtpLp9Ik!6G$CSL_idZ$Ti?R)4toe8bb)l|)lNb}?K;O2K9vyn1QG
zd=v#y-Ld49UVkmfRU>Egc+(Y$^-;6vW;3Lcu*6~etz}0|@+b|+!UCal)DEYGLbHWJ
zll5Wi^$Y<6@S%^y%hdjRh6&{!z1Py|lZ|q&Wub3l41uN2zEF8E&5H5?PL*&V}?*a}Lp%
zCYi{ghjpRNT^^B+_U59No50Ghih5qn(W5`RkrsDWr{~A1dgtv{sRkH4RU2^A{jb&0
zxVRnrm|u<;$iI;M6A>$POP)TWGU-gSjAERk*EGmVT(aw$!XUSe~7Ql-oRA54^4V(JWS6Q1mG?!vZ
zx+pE!FEtvqr|Xrcb3oR`%LHFLmU_&{=p%mGy6MRe2Yz_5WJ8p@IgU2
zdVvvhhQtiQkChK%*&PsiPCBL9oDOoJX8!$S(V>R}+1M}wzK*U*A{KJ`r=lM;mPrKU
zQDqqN(W*u-5-?$(SIk<6A0E}34y&@-IVC%S!a1F4kz<3bIKjlyD)ooO_7ftl%S_(6w`!vX&1PZ!K`@D@L6JR)6zO@Dl!YF{RY}d3HZ7?Q5E>w=$
ze)H_)48Ds*Ov4?zoGb2fe3}{!5Ooc|KCIni1o)(Gj+CO?`*7jsV`hIv@8J(22o4Q?
zu?Bvi)zDG(me?7XKeL|iF9ZRgZdT*}Ffsl62Cu;{Gv9j6dO
zPt*H2GqC)-C`V`ceuu=tM{7!2yTEj=*5+T~5DYiZ)Hy)*PARYI6R2lZXoOj;v8M4W
z*O-NX(7_~Q&A3>Oaw&1lBH_H%SwmISX-i3)HfHvBOeVwTT{LUM3}ZuZmg<(>)KE;d
zbs2!0v6>J;1nQ0UJkUxnkE@Ibi~Q}M=-=Rk;hcOnxO$luOKEVxZc|!XECgex(2`}T
z3Y;Q_6rL)e+SrOZhQj5_e}Lv>w7n*Pep$yWZNQl>ubBgb_NIWWDn3kNpn+MPQXV;8
zV|_Ba5jsQ(w&Ey^IM|@|y!AqcJ#3m0#Q6_qvgCG~eoF#mnGmbO(;DP+bW%_aOs1R_
z@9p#7X2UA^--#Nwx_Hvk2l1`eO{P*#j@q2UELtH|Uh6hxR`h_847wIJo0=5CQQ`6it|%a-I$^&a@we1rc&*;QIu5Ck^?)
zx*5eSd*mG#=6Hi(5!;5uUi&{HfnT1S8X-)?gE5CZ6KWoqM5|CyrULmuFBKOU8SOp*
z{IB1$OCcq`S-k*xs;4fmhKsIGZ;GYAY*%(@875NxhMq|j*m4CNLI(Vho|N|F);!E0cS5y^$H^Izje?z}oTgyr`9x9G&rlJZw&uqIoBMtz
zzhU0(9;w02?m#0!)cFi*r+8YvooQ;(s2lLVvyLqAE%Xqe!vtWbIs!l1Bpp(FIht-Z
zPn#CN-2C|J*GhA2fuHqYQ2mJiXlGTzD}mkr2;ia8Wp}h^;OS7+N^Mw|en!1${vN6
z-x{8N*4UekA~`IV2&K-GzhAqau|}d*pEQ$1MH$cFi03OG^1NetZ_jW^STaEzr&Xho
zB452St%v3ez2#TFm~`gZh$vi=in+y2d!z<{OZ~Kty-5bQ;0O=k_ESi8Nx9{*T`LJy6jqR>&|+>OZ;+=0hA04
zE25t^sE9HG)3^KKR_A5WDkqispweP9!I-@dCO&N!JrD@i{WBHnfQ
z95o8;d$`AFnca3;N-0iX-CmbbAp5yQ!GoH;h7Cn?m{ammZJI8igP{U73lFnl2&gCs
zqJ4(Vo~^j`{zOAzScL5B_Sm?Mjtek1d(A6X5ObcZi$;aOYy|g$}BY
z$GEP3#i60Ju_&3SHzryH!gUFwC9-295u??cf+aYRQ1$+!rc#42YNattd6mZEFI@?C
zqFM>6+zxEunIHDZ>{Z15u##>N(28Dw!>G(k*dB{NHvip@aP}f`@=Q;!o;zRMWo{Cx
zo?kyzh8n7#f1g0&g>Cd>O-2g?uPwy8sy8hZbHSsXPmU;@l=HL=zm7mN(=@*|D$i+u
zs~TllkCTvD$f&-#b9B?}#Lg*-ibK13R_a$RyoN3m5`10tdhAq{+VW)K#Bht-ra1*J
z+n$N%V>u0rVtx`aKJDwXXrxaD7nS<>$=c82v7@KVx^S@vT;h=SZE37K>iahpx3;VDzEr9GY=2(%uaqM;^76eSP0QLzo4sI
z>p_Eei*T$K;|qK`sq;?Hesp}(@VvX2Q4sAMYAJ}b&d$htDMC{FG-$o4k9ApECi1$a
zXdamjiOGKHBh(4M<3(2x6n-CrmZMCknkQxdSS!qlis#I}btfX;J`JU3RlvtLdrymP
zG0ZzrsGXVFiq+Wk1=BFay&9ZiCE#(`h~CL+c-Hs@iGTU@YxM%vlg;)`Tf~IknA^02
zXkN#Txo6aR{j$wP5T#|UH#5AP2{rSY8p?jKFv
zG3kn3y`FaV!*Jq%m39_TQEhD>M@l*bhEPGe1{ft3q#K5AknT=F2_=T^l#ou5ln@D#
z5Tzs(kRG@qNDa~HLNvfv7Z0g=bSlb?`QAx|Gfoni|iHJ%K0cy
z;~Nsaa+{8HP_qrb{nj+xzkdYhSI@W4N_1`z(eSGIkbDP)!Ko|M%}Rqp(~KI2hl~eE
zvJ!j4m6iwMgKy>fkCLC)`M$z9EV}B+sq1}}kVf$(ig0pWTY?rHz1Sm=4srTGNb^JG
z=2$9wz-C@aZZZ2!HY#HNejqZRmE=pN(D$Kui$NpfhU`!y_s{@MIxiJdHb1|{6xb`>
zE74_@QtgtG{4=3P1$^vn&m}7Aw8!1DnT$2thO#~44wl(N#ao8S0@t@m+Z!KD2CfK;
z)n5DAPKV_etmH1aLDK$?`;sL91iVt$D
z*SG}=-LIAg(*+JON!-5ivqOMQ1S!OQUgHglDsKik&Mwg;vva523`JwQH6SRz9eTY#
zTIi23145~kc3r1mSWC_RzD%hs$S#!pkI9!BU80jJCJcwo*FZolQG$q`8C1d9pP@ND
zG^&-ZraIvhg_FDVSfKGwkcI=avIan%2sK4coUs~Nr8jC*&!G0#?}_^s3r-c}-uAqi
zM-Lw>Y}I``T;IS%Y|qH;s{F*ZefM!4{I5awr!K+T@uPd*Vu*iPWI}>(-D{zxsN>LG
z=@747a_Rb2>q?y8xYf?dq2HM5tFO8Y5e4N;Y=xy8yAhI
zsm>oy%R5;7)7T3V_b2%`aH^tNlsQpFxIFW#iV#8?{6{^cGr{A0@1bA)|K
z>MMTuZD(pd2t|7vmHtywGXb%%=)S<`OG~}U+jm#xd%H8
z$v8-C%F?ah3$;hn?{G3(LT!SgvCVi$vwsZssAQvUwT`Q%qSw!LSd!(I!64w1=%Sc1Mck)q1@pZ@)=SY
zoX}d+L3-RA|c?G3_BQNm&(
z!i$AZ7cI(z7q|e9VM##6T3Xorj1JG(9os$;(I$y%mBy(#8{|3l4|x*oBAQL^XhZ0g
zy1FR1teRrpKq{uLAibTLx#n({qwjlkOvR{OdSAeT5ah4-sNN)n4Clg1T9lzF)&yj;
zyal1%+s4n1IG;^VPWJ;#olpk8Z42Gj-tjFeQ&PlxB)`oCNoUYKj4U$AeG8rYiD{pK
zndDf&2;2;)D|KvOZP+e7fcPU9k4M2sfhr@vC~Ly0?S-4dz)ZGAYpCsAhChgbxLd4g
zhTrbIPkO5SEp_kD>Ha0m12h5n3s;mE8kn515&nzSf+^D=
zyE{JnJ;43l&BH55CL<=W%CF;6iUI)V5C*6!`**KqvzR2=Fj*3Y4`HYwx}TYD445(K
z-QtXwtL?m*(F=LVH*H4oM>dXHBW=38q_dZ-_Vr&qpEPxd9Fs95P5W~@Z|Rt+WZP6l
zPSQ}~Dh4V?Pp1g&Hk*Px?lm16C@X6M29Vrk%Rw@E||E-v~$
zb_E~{z<}#8i`Mx9mkqtd#Z1lZ-E_J8I+2oumc#x1)jdvh{W76NKm6x-RYpM~v!P8$
zw3e|YVf|}Hse9~oC@N7^j}Fi$hNpyaYnu1}bdXsD=^oI*%WKvbme|BI}$G3>smu#6y)ls|j?
zF7Bhu9Z)j)C;3cZb+I>0stSK^WLOYV^U{pUYkgv>?+Nt^5j*CUB=eGw-CvU&40>y~
zGoHLXxY^7k5Xgv62{iQy|5jJQuq0|LU`}lE@flQ2Z*Zn*VWcQjm4FTb>LSVox^S4q
zLn`LfS@mrjKCmg$nb^af?d?0&$aX6#2u(JyzIJvuJ*lwPrh|0~aEnSACCTezSdG%h
zmSQg`17j@$Iq)r1&?+eR@1nlX|H`<}_!?BQSF&N+QQnvEAqZe+mIFui!0V49R?|9*$
zv!K1A01{8xq;L()Tv*Qk0-$Oj6+vCT*TUD{HvxO@3JjxBwM!4g3ydy&eaJw4CoQBF
zJtULJ!YxgNR7_Ls%LmogyI7uIs=!B&?=MYY^yX+v;j@D_xGeZg>eZk0C;4e|HRNSi
z6KlD9>q=3v-$4Zik&^ZDhNm1X)+7LCH1k!s+T3tn
zUn@={1U&NJLq@K?~w|(=Y<4W{ucX}FdRr6pLw(l2$iK)At%t3gYBMlJz#(K0Nqm;=KAML!&MMSNz=%k=j*zh77r34Rs37iCY`
z=_kva_41bdrj(b=4Wc5MO0~q^z#pIWJ>)vDSgIQF=3JVJe1iDy%h)8oNy{s_r&;m`
zL{DYKSB_5xRb9xKNOS{qAY3qv5sSXVrrf%~*q5HO|CQ&lbKMePa$M5D{vlJcoGrCZ
zD?fKbZN$6rWwz)w7`9h4DAmh1ij2}EO|bO#A9L0_RW6l*$sPPUJrUbhLC75L9%W5iO$Iw5~Yut-qBeu~hF|xD7-eQ%l
z412vpq_;t%^F*pYDk%Q35c-erK|6Ve=FxQbAv~ikZ4c9$Y4;ee#ciOD9{yRqf55Qk
zumv}#+JciT|Gj$uFOxBUze)=?l{B}qaC0_7m`t82<$K53!4Xvi9Tr)ADp3Off?O8o
zVDG0Yx|tfn@r((m?Nxrh(b0DGjg)$;DfO&$6uY;4&F!4jnxkhP}Y3x
zS?WFFt>=HWzqlQhffVfvM$Ta8Sg*r3j!Eo&rUOW7SCL2~lG7<+XZ;+{&8h5g8ElI+P>>yR2U%S93NN!Xhm|C682t6ysH-=o1=Bd*N*VlnG%l+KZFtjG`UkL;%65qn0UYQ`h
zh0{9jDQx(`aBe7J0Aj3Z)4}`A|4OMM0a;?{j}qkYwi)~O8$9D}ITiMH2buiU>ixYp
zhL${nwj6X($*OwmpVG`y5b6v45tX*J8?og}Qju6eJ9H}`X87iEd%BUo7<`2q(HJx+
zMR}d-J4oAf{V1W^a2~`M-YAdZ81dd4o6NPO{cmZaAS@RS4ir#Sr
zfFZO-VIL|VN<%nEXr2`
z$0FK2L#8O_f1w~c@G70JrB@N}r(gJ!Vmkk6{r68w!o$qO?HrFcjeU0_3F5;*!E2%(
zTx>4?gP8w
z1B?3UVZmz^%d_dIps>>0{cB~mp3{9UoPR6uQFecVq&}
zY{ebB?AlPAD_}(ll{fK99;Wh1cgRbnw)maD^F>*J!R}eHM*W0VYN1TADWMy9H=$00
z5bHY${oDgwX7(W9LZw?}{!8(_{JB~Xkje6{0x4fgC4kUmpfJ+LT1DYD*TWu4#h{Y7
zFLronmc=hS=W=j1ar3r1JNjQoWo2hMWsqW*e?TF%#&{GpsaLp}iN~$)ar+7Ti}E&X
z-nq~+Gkp(`qF0F_4A22>VZn-x>I$?PDZSeG8h_ifoWf^DxIb5%T7UytYo3}F|4#RC
zUHpg$=)qVqD~=m(!~?XwocuxU1u}9qhhM7d^eqmJPi_e-!IO`*{u7A
zbu*?L$Mbj-X9n3G2>+Kc#l`@d8}Xb9{l*IN{#M*d;s+3Pdr8FO$EBELR=8{
zd?LJbSv9fI`{OqTH)5{b?WulgMb)psp+W|@cSp=jtl-&5C}9lw@*0H+gEW(}mAWNz
zf{~U;;N}|wdSaphgqnH{FWUy!{y3^=AC*c?RJ5Eb<^
zCgH_v7^axIUVmHSFL^zlj2R$zow$|y#7>%#U7d#Vp_ezcp3lefMyd5ES=q$>4pWyA
zp_Zso^^NP~lu2=S6nD(3Z5u=Uy&B&F1i$J*3;3KhEkD_lgscHGR*;T;U!9vgQa(hI}oh9IzEf_PU_8F+i77t-~gDX
z490Sb)LyVZmf18N6w{+37$aO<2!Av0
ztLaPOv^J<2@p{WnMiDudoghX_`luFZt_4eNU}*~cF5i%eEcNLs;D>QVIwr8mH;=dc
z09`}JV;aaF;13@&iS(w>Jc=k~|d_1hcpM(l|O
zu>!@}me%isTT$xT#hNUvh(ATd0wT4fbv=6htcHNEZIw9%E6wlYmwfu2{j0kh1y=$;Yf!|NldgB9ul
zB{dbE&LfRnr8ITm@;-68wo#VV?8lG3ed&9k1}QBS3}WGV9%26?A1rBkkDR9Z3o+g+
z)eQg8BY3y(Dh5&z?VLLNdDV`C=muUvCPpGg!oYxIgOI3^%4>5d7jTh~ni!Fg2;fhx
z(*c%H6Je84kmQh;5tC3*l~7khLxK-e|Cz?FLh!yYe7g|*LwqU?2wv^_ZyKT$fYVkGJo@AK0$+ml?}zJeB~deT2WL1vz}dxB
z)y??t!}%M@)u$_IyW~)6u1SttJ!awd6N5lx|xBrmyrBh>tb&D*=C+Z3nPfq$1%WgY0bY*?PZ#Hk|=xn
zGM#0*w4CaB^y0G(J4q=;5NeM@m-P}#mv7QZNF)M!dK^w{mk_!n0`+Y3PQutu-%NBt
zzgPXug?JLEbUL{e_dk;Vd896&yPe(hliVK!lj%5+@BKdcrEZ2Nc_*i@ve*2lB>u~{
zFozd2FM|_0+nAGR4TLNHanQn_Oeb!JrUcvzJ?7p9TTNB}ocO3j$7ij!li8#k6
z@2tSd1>K03K9A#_-MIq)S;T#oE^;>U$)&}okIvDf3lm?kI{d80$>~xKUoS!%q1Pi?WpsUUt(tI
ztjNjY*y&Rm9(S(DC2GuPHBJs@5M{RGm`c1z<6nwyN^)rMo-AS{M2$oM9|y%fM|}G~
DHx0+F

diff --git a/auth-service/gradle/wrapper/gradle-wrapper.properties b/auth-service/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 37f853b..0000000
--- a/auth-service/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,7 +0,0 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
-networkTimeout=10000
-validateDistributionUrl=true
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
diff --git a/auth-service/gradlew b/auth-service/gradlew
deleted file mode 100644
index faf9300..0000000
--- a/auth-service/gradlew
+++ /dev/null
@@ -1,251 +0,0 @@
-#!/bin/sh
-
-#
-# Copyright © 2015-2021 the original authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-# SPDX-License-Identifier: Apache-2.0
-#
-
-##############################################################################
-#
-#   Gradle start up script for POSIX generated by Gradle.
-#
-#   Important for running:
-#
-#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
-#       noncompliant, but you have some other compliant shell such as ksh or
-#       bash, then to run this script, type that shell name before the whole
-#       command line, like:
-#
-#           ksh Gradle
-#
-#       Busybox and similar reduced shells will NOT work, because this script
-#       requires all of these POSIX shell features:
-#         * functions;
-#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
-#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
-#         * compound commands having a testable exit status, especially «case»;
-#         * various built-in commands including «command», «set», and «ulimit».
-#
-#   Important for patching:
-#
-#   (2) This script targets any POSIX shell, so it avoids extensions provided
-#       by Bash, Ksh, etc; in particular arrays are avoided.
-#
-#       The "traditional" practice of packing multiple parameters into a
-#       space-separated string is a well documented source of bugs and security
-#       problems, so this is (mostly) avoided, by progressively accumulating
-#       options in "$@", and eventually passing that to Java.
-#
-#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
-#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
-#       see the in-line comments for details.
-#
-#       There are tweaks for specific operating systems such as AIX, CygWin,
-#       Darwin, MinGW, and NonStop.
-#
-#   (3) This script is generated from the Groovy template
-#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
-#       within the Gradle project.
-#
-#       You can find Gradle at https://github.com/gradle/gradle/.
-#
-##############################################################################
-
-# Attempt to set APP_HOME
-
-# Resolve links: $0 may be a link
-app_path=$0
-
-# Need this for daisy-chained symlinks.
-while
-    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
-    [ -h "$app_path" ]
-do
-    ls=$( ls -ld "$app_path" )
-    link=${ls#*' -> '}
-    case $link in             #(
-      /*)   app_path=$link ;; #(
-      *)    app_path=$APP_HOME$link ;;
-    esac
-done
-
-# This is normally unused
-# shellcheck disable=SC2034
-APP_BASE_NAME=${0##*/}
-# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD=maximum
-
-warn () {
-    echo "$*"
-} >&2
-
-die () {
-    echo
-    echo "$*"
-    echo
-    exit 1
-} >&2
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-nonstop=false
-case "$( uname )" in                #(
-  CYGWIN* )         cygwin=true  ;; #(
-  Darwin* )         darwin=true  ;; #(
-  MSYS* | MINGW* )  msys=true    ;; #(
-  NONSTOP* )        nonstop=true ;;
-esac
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
-    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
-        # IBM's JDK on AIX uses strange locations for the executables
-        JAVACMD=$JAVA_HOME/jre/sh/java
-    else
-        JAVACMD=$JAVA_HOME/bin/java
-    fi
-    if [ ! -x "$JAVACMD" ] ; then
-        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-    fi
-else
-    JAVACMD=java
-    if ! command -v java >/dev/null 2>&1
-    then
-        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-    fi
-fi
-
-# Increase the maximum file descriptors if we can.
-if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
-    case $MAX_FD in #(
-      max*)
-        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
-        # shellcheck disable=SC2039,SC3045
-        MAX_FD=$( ulimit -H -n ) ||
-            warn "Could not query maximum file descriptor limit"
-    esac
-    case $MAX_FD in  #(
-      '' | soft) :;; #(
-      *)
-        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
-        # shellcheck disable=SC2039,SC3045
-        ulimit -n "$MAX_FD" ||
-            warn "Could not set maximum file descriptor limit to $MAX_FD"
-    esac
-fi
-
-# Collect all arguments for the java command, stacking in reverse order:
-#   * args from the command line
-#   * the main class name
-#   * -classpath
-#   * -D...appname settings
-#   * --module-path (only if needed)
-#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if "$cygwin" || "$msys" ; then
-    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
-    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
-
-    JAVACMD=$( cygpath --unix "$JAVACMD" )
-
-    # Now convert the arguments - kludge to limit ourselves to /bin/sh
-    for arg do
-        if
-            case $arg in                                #(
-              -*)   false ;;                            # don't mess with options #(
-              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
-                    [ -e "$t" ] ;;                      #(
-              *)    false ;;
-            esac
-        then
-            arg=$( cygpath --path --ignore --mixed "$arg" )
-        fi
-        # Roll the args list around exactly as many times as the number of
-        # args, so each arg winds up back in the position where it started, but
-        # possibly modified.
-        #
-        # NB: a `for` loop captures its iteration list before it begins, so
-        # changing the positional parameters here affects neither the number of
-        # iterations, nor the values presented in `arg`.
-        shift                   # remove old arg
-        set -- "$@" "$arg"      # push replacement arg
-    done
-fi
-
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
-
-# Collect all arguments for the java command:
-#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
-#     and any embedded shellness will be escaped.
-#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
-#     treated as '${Hostname}' itself on the command line.
-
-set -- \
-        "-Dorg.gradle.appname=$APP_BASE_NAME" \
-        -classpath "$CLASSPATH" \
-        org.gradle.wrapper.GradleWrapperMain \
-        "$@"
-
-# Stop when "xargs" is not available.
-if ! command -v xargs >/dev/null 2>&1
-then
-    die "xargs is not available"
-fi
-
-# Use "xargs" to parse quoted args.
-#
-# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
-#
-# In Bash we could simply go:
-#
-#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
-#   set -- "${ARGS[@]}" "$@"
-#
-# but POSIX shell has neither arrays nor command substitution, so instead we
-# post-process each arg (as a line of input to sed) to backslash-escape any
-# character that might be a shell metacharacter, then use eval to reverse
-# that process (while maintaining the separation between arguments), and wrap
-# the whole thing up as a single "set" statement.
-#
-# This will of course break if any of these variables contains a newline or
-# an unmatched quote.
-#
-
-eval "set -- $(
-        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
-        xargs -n1 |
-        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
-        tr '\n' ' '
-    )" '"$@"'
-
-exec "$JAVACMD" "$@"
diff --git a/auth-service/gradlew.bat b/auth-service/gradlew.bat
deleted file mode 100644
index 9d21a21..0000000
--- a/auth-service/gradlew.bat
+++ /dev/null
@@ -1,94 +0,0 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem      https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-@rem SPDX-License-Identifier: Apache-2.0
-@rem
-
-@if "%DEBUG%"=="" @echo off
-@rem ##########################################################################
-@rem
-@rem  Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%"=="" set DIRNAME=.
-@rem This is normally unused
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if %ERRORLEVEL% equ 0 goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-goto fail
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if %ERRORLEVEL% equ 0 goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-set EXIT_CODE=%ERRORLEVEL%
-if %EXIT_CODE% equ 0 set EXIT_CODE=1
-if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
-exit /b %EXIT_CODE%
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
diff --git a/auth-service/settings.gradle b/auth-service/settings.gradle
deleted file mode 100644
index d4c3eb3..0000000
--- a/auth-service/settings.gradle
+++ /dev/null
@@ -1 +0,0 @@
-rootProject.name = 'auth-service'
diff --git a/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java b/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java
deleted file mode 100644
index 59186b4..0000000
--- a/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.example.authservice;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-@SpringBootApplication
-public class AuthServiceApplication {
-
-    public static void main(String[] args) {
-        SpringApplication.run(AuthServiceApplication.class, args);
-    }
-
-}
diff --git a/auth-service/src/main/resources/application.properties b/auth-service/src/main/resources/application.properties
deleted file mode 100644
index e536707..0000000
--- a/auth-service/src/main/resources/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-spring.application.name=auth-service
diff --git a/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java b/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java
deleted file mode 100644
index 2e763c4..0000000
--- a/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.example.authservice;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class AuthServiceApplicationTests {
-
-    @Test
-    void contextLoads() {
-    }
-
-}

From 890b391ba1c5cd395bcf57b5764920a90206053e Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 16:56:37 +0900
Subject: [PATCH 165/361] =?UTF-8?q?auth=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?=
 =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 area.sql                                      | 116 --------
 auth-service/.gitattributes                   |   3 -
 auth-service/.gitignore                       |  37 ---
 .../.gradle/8.13/checksums/checksums.lock     | Bin 17 -> 0 bytes
 .../.gradle/8.13/checksums/md5-checksums.bin  | Bin 21097 -> 0 bytes
 .../.gradle/8.13/checksums/sha1-checksums.bin | Bin 27407 -> 0 bytes
 .../executionHistory/executionHistory.lock    | Bin 17 -> 0 bytes
 .../.gradle/8.13/fileChanges/last-build.bin   | Bin 1 -> 0 bytes
 .../.gradle/8.13/fileHashes/fileHashes.lock   | Bin 17 -> 0 bytes
 auth-service/.gradle/8.13/gc.properties       |   0
 .../buildOutputCleanup.lock                   | Bin 17 -> 0 bytes
 .../buildOutputCleanup/cache.properties       |   2 -
 auth-service/.gradle/vcs-1/gc.properties      |   0
 auth-service/bin/main/application.properties  |   1 -
 .../authservice/AuthServiceApplication.class  | Bin 738 -> 0 bytes
 .../AuthServiceApplicationTests.class         | Bin 566 -> 0 bytes
 auth-service/build.gradle                     |  58 ----
 .../gradle/wrapper/gradle-wrapper.jar         | Bin 43705 -> 0 bytes
 .../gradle/wrapper/gradle-wrapper.properties  |   7 -
 auth-service/gradlew                          | 251 ------------------
 auth-service/gradlew.bat                      |  94 -------
 auth-service/settings.gradle                  |   1 -
 .../authservice/AuthServiceApplication.java   |  13 -
 .../src/main/resources/application.properties |   1 -
 .../AuthServiceApplicationTests.java          |  13 -
 25 files changed, 597 deletions(-)
 delete mode 100644 area.sql
 delete mode 100644 auth-service/.gitattributes
 delete mode 100644 auth-service/.gitignore
 delete mode 100644 auth-service/.gradle/8.13/checksums/checksums.lock
 delete mode 100644 auth-service/.gradle/8.13/checksums/md5-checksums.bin
 delete mode 100644 auth-service/.gradle/8.13/checksums/sha1-checksums.bin
 delete mode 100644 auth-service/.gradle/8.13/executionHistory/executionHistory.lock
 delete mode 100644 auth-service/.gradle/8.13/fileChanges/last-build.bin
 delete mode 100644 auth-service/.gradle/8.13/fileHashes/fileHashes.lock
 delete mode 100644 auth-service/.gradle/8.13/gc.properties
 delete mode 100644 auth-service/.gradle/buildOutputCleanup/buildOutputCleanup.lock
 delete mode 100644 auth-service/.gradle/buildOutputCleanup/cache.properties
 delete mode 100644 auth-service/.gradle/vcs-1/gc.properties
 delete mode 100644 auth-service/bin/main/application.properties
 delete mode 100644 auth-service/bin/main/com/example/authservice/AuthServiceApplication.class
 delete mode 100644 auth-service/bin/test/com/example/authservice/AuthServiceApplicationTests.class
 delete mode 100644 auth-service/build.gradle
 delete mode 100644 auth-service/gradle/wrapper/gradle-wrapper.jar
 delete mode 100644 auth-service/gradle/wrapper/gradle-wrapper.properties
 delete mode 100644 auth-service/gradlew
 delete mode 100644 auth-service/gradlew.bat
 delete mode 100644 auth-service/settings.gradle
 delete mode 100644 auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java
 delete mode 100644 auth-service/src/main/resources/application.properties
 delete mode 100644 auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java

diff --git a/area.sql b/area.sql
deleted file mode 100644
index d2119f5..0000000
--- a/area.sql
+++ /dev/null
@@ -1,116 +0,0 @@
-INSERT INTO area (area_id, name, name_eng, category, x, y) VALUES
-('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705),
-('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576),
-('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864),
-('POI004', '이태원 관광특구', 'Itaewon Special Tourist Zone', '관광특구', 37.53475718815058, 126.99591956660342),
-('POI005', '잠실 관광특구', 'Jamsil Special Tourist Zone', '관광특구', 37.51868923685722, 127.11694379878114),
-('POI006', '종로·청계 관광특구', 'Jongno Cheonggye Special Toruist Zone', '관광특구', 37.56984229024498, 126.99866678943675),
-('POI007', '홍대 관광특구', 'HongDae Culture & Arts Special Tourist Zone', '관광특구', 37.55506170187393, 126.92159115672915),
-('POI008', '경복궁', 'Gyeongbokgung Palace', '고궁·문화유산', 37.5809150911806, 126.97764726625209),
-('POI009', '광화문·덕수궁', 'Gwanghwamun & Deoksugung Palace', '고궁·문화유산', 37.56847081447724, 126.97620594802834),
-('POI010', '보신각', 'Bosingak', '고궁·문화유산', 37.57089797616037, 126.98337607511172),
-('POI011', '서울 암사동 유적', 'Amsa Prehistoric Settlement Site', '고궁·문화유산', 37.56102809827398, 127.13078491934499),
-('POI012', '창덕궁·종묘', 'Changdeokgung Palace & Jongmyo Shrine', '고궁·문화유산', 37.5795304952381, 126.99416802285715),
-('POI013', '가산디지털단지역', 'Gasan Digital Complex station', '인구밀집지역', 37.48267268376068, 126.88014526210826),
-('POI014', '강남역', 'Gangnam station', '인구밀집지역', 37.50019794543511, 127.02822145618981),
-('POI015', '건대입구역', 'Konkuk University station', '인구밀집지역', 37.540005312302846, 127.06718185804415),
-('POI016', '고덕역', 'Godeok station', '인구밀집지역', 37.553377141111774, 127.15503341723486),
-('POI017', '고속터미널역', 'Express Bus Terminal station', '인구밀집지역', 37.50472316731823, 127.00542276570697),
-('POI018', '교대역', 'Seoul National University of Education station', '인구밀집지역', 37.49185444557083, 127.01456630812736),
-('POI019', '구로디지털단지역', 'Guro Digital Complex station', '인구밀집지역', 37.48349491938897, 126.89557915984915),
-('POI020', '구로역', 'Guro station', '인구밀집지역', 37.501517014484925, 126.88285531403136),
-('POI021', '군자역', 'Gunja station', '인구밀집지역', 37.555916439038526, 127.08008933356267),
-('POI022', '남구로역', 'Namguro station', '인구밀집지역', 37.484184300878496, 126.88539683418863),
-('POI023', '대림역', 'Daerim station', '인구밀집지역', 37.492818237753816, 126.89567307890546),
-('POI024', '동대문역', 'Dongdaemun station', '인구밀집지역', 37.57127393968462, 127.0096854515538),
-('POI025', '뚝섬역', 'Ttukseom station', '인구밀집지역', 37.548249670448335, 127.04603079475764),
-('POI026', '미아사거리역', 'Miasageori station', '인구밀집지역', 37.61244898963818, 127.0305835226811),
-('POI027', '발산역', 'Balsan station', '인구밀집지역', 37.55796811527854, 126.84179723439551),
-('POI028', '북한산우이역', 'Bukhansan Ui station', '인구밀집지역', 37.66281154490492, 127.01070580250341),
-('POI029', '사당역', 'Sadang station', '인구밀집지역', 37.47635049189678, 126.98129519152415),
-('POI030', '삼각지역', 'Samgakji station', '인구밀집지역', 37.535323919468816, 126.97392611189508),
-('POI031', '서울대입구역', 'Seoul National University station', '인구밀집지역', 37.48060991955129, 126.95360407634334),
-('POI032', '서울식물원·마곡나루역', 'Seoul Botanic Park·Magongnaru station', '인구밀집지역', 37.56745047572558, 126.82986126533689),
-('POI033', '서울역', 'Seoul station', '인구밀집지역', 37.55662336923077, 126.97216878769233),
-('POI034', '선릉역', 'Seolleung station', '인구밀집지역', 37.50584166753116, 127.0489100692203),
-('POI035', '성신여대입구역', 'Sungshin Women''s University station', '인구밀집지역', 37.59262801654558, 127.01658994286346),
-('POI036', '수유역', 'Suyu station', '인구밀집지역', 37.641215514705884, 127.02565393627451),
-('POI037', '신논현역·논현역', 'Sinnonhyeon·Nonhyeon station', '인구밀집지역', 37.50848054554439, 127.02291493850254),
-('POI038', '신도림역', 'Sindorim station', '인구밀집지역', 37.50941493666961, 126.89096014067083),
-('POI039', '신림역', 'Sillim station', '인구밀집지역', 37.48396968904173, 126.92987028995233),
-('POI040', '신촌·이대역', 'Sinchon·Ewha Womans University station', '인구밀집지역', 37.55564146783625, 126.93761215497075),
-('POI041', '양재역', 'Yangjae station', '인구밀집지역', 37.48451422353994, 127.03551115644439),
-('POI042', '역삼역', 'Yeoksam station', '인구밀집지역', 37.50011177778084, 127.03721339389585),
-('POI043', '연신내역', 'Yeonsinnae station', '인구밀집지역', 37.61903547457627, 126.92180184322034),
-('POI044', '오목교역·목동운동장', 'Omokgyo·Mok-dong Stadium station', '인구밀집지역', 37.53174788866626, 126.87924768246015),
-('POI045', '왕십리역', 'Wangsimni station', '인구밀집지역', 37.561636745463275, 127.03857508984515),
-('POI046', '용산역', 'Yongsan station', '인구밀집지역', 37.53059674809017, 126.95953798376128),
-('POI047', '이태원역', 'Itaewon station', '인구밀집지역', 37.534847968613946, 126.99261390234051),
-('POI048', '장지역', 'Jangji station', '인구밀집지역', 37.47861018962738, 127.12424263037641),
-('POI049', '장한평역', 'Janghanpyeong station', '인구밀집지역', 37.562849061361526, 127.06235115915185),
-('POI050', '천호역', 'Cheonho station', '인구밀집지역', 37.53921065146112, 127.12470374198884),
-('POI051', '총신대입구(이수)역', 'Chongshin University(Isu) station', '인구밀집지역', 37.48677821828824, 126.9807539648491),
-('POI052', '충정로역', 'Chungjeongno station', '인구밀집지역', 37.559524839900476, 126.9642059673645),
-('POI053', '합정역', 'Hapjeong station', '인구밀집지역', 37.54857307399022, 126.91084543497917),
-('POI054', '혜화역', 'Hyehwa station', '인구밀집지역', 37.58237426286851, 127.0014404389915),
-('POI055', '홍대입구역(2호선)', 'Hongik University station(Line 2)', '인구밀집지역', 37.5560688250626, 126.92329766229786),
-('POI056', '회기역', 'Hoegi station', '인구밀집지역', 37.590538258503685, 127.05700039395965),
-('POI057', '4·19 카페거리', '4·19 Cafe street', '발달상권', 37.64481185893745, 127.00739701908337),
-('POI058', '가락시장', 'Garak Market', '발달상권', 37.49445476721721, 127.11054976163454),
-('POI059', '가로수길', 'Garosu-gil', '발달상권', 37.522662622516556, 127.02543305960263),
-('POI060', '광장(전통)시장', 'Gwangjang(Traditional) Market', '발달상권', 37.569817036518465, 127.00014893169192),
-('POI061', '김포공항', 'Gimpo Airport', '발달상권', 37.56228914604604, 126.80289965159135),
-('POI062', '낙산공원·이화마을', 'Naksan Park·Ihwa Village', '발달상권', 37.580295002146926, 127.00692469978675),
-('POI063', '노량진', 'Noryangjin', '발달상권', 37.513690699957316, 126.94907070192423),
-('POI064', '덕수궁길·정동길', 'Deoksugung-gil·Jeongdong-gil', '발달상권', 37.5659744042236, 126.97244482634493),
-('POI065', '방배역 먹자골목', 'Bangbae food alley', '발달상권', 37.48234102283986, 126.99597969571558),
-('POI066', '북촌한옥마을', 'Bukchon Hanok Village', '발달상권', 37.58425649416171, 126.98386049085175),
-('POI067', '서촌', 'Seochon', '발달상권', 37.58018833392947, 126.9666234325548),
-('POI068', '성수카페거리', 'Seongsu Cafe Street', '발달상권', 37.54227657813654, 127.05560237183015),
-('POI069', '수유리 먹자골목', 'Suyuri food alley', '발달상권', 37.64270823057644, 127.02733855833522),
-('POI070', '쌍문동 맛집거리', 'Ssangmun-dong restaurant street', '발달상권', 37.6477285443038, 127.03242528481013),
-('POI071', '압구정로데오거리', 'Apgujeong Rodeo Street', '발달상권', 37.52560441860466, 127.04040171511626),
-('POI072', '여의도', 'Yeouido', '발달상권', 37.52549260943663, 126.92338937129487),
-('POI073', '연남동', 'Yeonnam-dong', '발달상권', 37.561426426204264, 126.92406273242905),
-('POI074', '영등포 타임스퀘어', 'Yeongdeungpo Time square', '발달상권', 37.51653179760669, 126.90523931614634),
-('POI075', '외대앞', 'Hankuk University of Foreign Studies', '발달상권', 37.59442412843283, 127.06153833102397),
-('POI076', '용리단길', 'Yongnidan-gil', '발달상권', 37.5311056439386, 126.97147359358237),
-('POI077', '이태원 앤틱가구거리', 'Itaewon Antiques street', '발달상권', 37.53187270743738, 126.99343376513602),
-('POI078', '인사동·익선동', 'Insa-dong·Ikseon-dong', '발달상권', 37.5741577489609, 126.98736374162822),
-('POI079', '창동 신경제 중심지', 'Changdong New Economic Center', '발달상권', 37.656167961335676, 127.05328808787348),
-('POI080', '청담동 명품거리', 'Cheongdam-dong Luxury Fashion street', '발달상권', 37.52645537152884, 127.04354058968364),
-('POI081', '청량리 제기동 일대 전통시장', 'Traditional market in Cheongnyangni Jegi-dong', '발달상권', 37.579977738703214, 127.0382790126348),
-('POI082', '해방촌·경리단길', 'Haebangchon·Gyeongnidan-gil', '발달상권', 37.54143829714048, 126.98836800969178),
-('POI083', 'DDP(동대문디자인플라자)', 'DDP(Dongdaemun Design Plaza)', '발달상권', 37.56654201564661, 127.00993045294622),
-('POI084', 'DMC(디지털미디어시티)', 'DMC(Digital Media City)', '발달상권', 37.577407110655734, 126.8936691147541),
-('POI085', '강서한강공원', 'Gangseo Hangang Park', '공원', 37.5858708295553, 126.82002478578644),
-('POI086', '고척돔', 'Gocheok Dome', '공원', 37.49776790791694, 126.86713336195083),
-('POI087', '광나루한강공원', 'Gwangnaru Hangang Park', '공원', 37.550866089267345, 127.12370647922764),
-('POI088', '광화문광장', 'Gwanghwamun Square', '공원', 37.57505062349029, 126.97680679071502),
-('POI089', '국립중앙박물관·용산가족공원', 'The National Museum of Korea·Yongsan Family Park', '공원', 37.52202119313341, 126.9824679850178),
-('POI090', '난지한강공원', 'Nanji Hangang Park', '공원', 37.566316740172454, 126.87770001597329),
-('POI091', '남산공원', 'Namsan Park', '공원', 37.555070650369004, 126.99365012730628),
-('POI092', '노들섬', 'Nodeul island', '공원', 37.517556872131706, 126.95871397855296),
-('POI093', '뚝섬한강공원', 'Ttukseom Hangang Park', '공원', 37.52882503002481, 127.07460742502332),
-('POI094', '망원한강공원', 'Mangwon Hangang Park', '공원', 37.55480087656761, 126.89823749216892),
-('POI095', '반포한강공원', 'Banpo Hangang Park', '공원', 37.51065462735107, 126.99457550649858),
-('POI096', '북서울꿈의숲', 'Dream Forest', '공원', 37.62244979881656, 127.04069568441815),
-('POI097', '불광천', 'Bulgwangcheon River', '공원', 37.58940646530798, 126.91194122899687),
-('POI098', '서리풀공원·몽마르뜨공원', 'Seoripul Park·Montmartre Park', '공원', 37.49022054658995, 127.00173655775917),
-('POI100', '서울대공원', 'Seoul Grand Park', '공원', 37.42839607329216, 127.01863790236284),
-('POI101', '서울숲공원', 'Seoul Forest', '공원', 37.54402680167043, 127.03673083133076),
-('POI099', '서울광장', 'Seoul Plaza', '공원', 37.5655010296151, 126.97798702124584),
-('POI102', '아차산', 'Achasan', '공원', 37.56307021845223, 127.10047918048643),
-('POI103', '양화한강공원', 'Yanghwa Hangang Park', '공원', 37.54029653900909, 126.89963558394624),
-('POI104', '어린이대공원', 'Children''s Grand Park', '공원', 37.54957100980681, 127.0824313180458),
-('POI105', '여의도한강공원', 'Yeouido Hangang Park', '공원', 37.52868909088513, 126.92627587984478),
-('POI106', '월드컵공원', 'World Cup Park', '공원', 37.5660391150387, 126.88740201899091),
-('POI107', '응봉산', 'Eungbongsan', '공원', 37.54797346368485, 127.02904643771303),
-('POI108', '이촌한강공원', 'Ichon Hangang Park', '공원', 37.52139035897837, 126.96356631928725),
-('POI109', '잠실종합운동장', 'Jamsil (Seoul) Sports Complex', '공원', 37.51610665876664, 127.07230325047283),
-('POI110', '잠실한강공원', 'Jamsil Hangang Park', '공원', 37.51981181577704, 127.08507449607517),
-('POI111', '잠원한강공원', 'Jamwon Hangang Park', '공원', 37.52336245003122, 127.01397131593632),
-('POI112', '청계산', 'Cheonggyesan', '공원', 37.443365424959026, 127.0469697170313),
-('POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264),
-('POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616),
-('POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821);
\ No newline at end of file
diff --git a/auth-service/.gitattributes b/auth-service/.gitattributes
deleted file mode 100644
index 8af972c..0000000
--- a/auth-service/.gitattributes
+++ /dev/null
@@ -1,3 +0,0 @@
-/gradlew text eol=lf
-*.bat text eol=crlf
-*.jar binary
diff --git a/auth-service/.gitignore b/auth-service/.gitignore
deleted file mode 100644
index c2065bc..0000000
--- a/auth-service/.gitignore
+++ /dev/null
@@ -1,37 +0,0 @@
-HELP.md
-.gradle
-build/
-!gradle/wrapper/gradle-wrapper.jar
-!**/src/main/**/build/
-!**/src/test/**/build/
-
-### STS ###
-.apt_generated
-.classpath
-.factorypath
-.project
-.settings
-.springBeans
-.sts4-cache
-bin/
-!**/src/main/**/bin/
-!**/src/test/**/bin/
-
-### IntelliJ IDEA ###
-.idea
-*.iws
-*.iml
-*.ipr
-out/
-!**/src/main/**/out/
-!**/src/test/**/out/
-
-### NetBeans ###
-/nbproject/private/
-/nbbuild/
-/dist/
-/nbdist/
-/.nb-gradle/
-
-### VS Code ###
-.vscode/
diff --git a/auth-service/.gradle/8.13/checksums/checksums.lock b/auth-service/.gradle/8.13/checksums/checksums.lock
deleted file mode 100644
index b1d2a8d2f692684808fb4cfe80a007e413c235fe..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 17
UcmZQp++t@_wroch0|eX$04u`;Z~y=R

diff --git a/auth-service/.gradle/8.13/checksums/md5-checksums.bin b/auth-service/.gradle/8.13/checksums/md5-checksums.bin
deleted file mode 100644
index 01c17bdefd353cfae0fc456139e95b61f09ba191..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 21097
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
a=~s7#v0USHY6diF@m31n!X?4B)PDhIMUcq=

diff --git a/auth-service/.gradle/8.13/executionHistory/executionHistory.lock b/auth-service/.gradle/8.13/executionHistory/executionHistory.lock
deleted file mode 100644
index d22c4ab2276dbc5c29d737790cbaacddc10d9753..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 17
TcmZQBmdv@|c}#RO0~7!NFfjx2

diff --git a/auth-service/.gradle/8.13/fileChanges/last-build.bin b/auth-service/.gradle/8.13/fileChanges/last-build.bin
deleted file mode 100644
index f76dd238ade08917e6712764a16a22005a50573d..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1
IcmZPo000310RR91

diff --git a/auth-service/.gradle/8.13/fileHashes/fileHashes.lock b/auth-service/.gradle/8.13/fileHashes/fileHashes.lock
deleted file mode 100644
index 0c961dfdc2280d13c3ca8b5f49adba48fba78245..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 17
TcmZQRy{|fQ?$^0l3{U_7KNJM|

diff --git a/auth-service/.gradle/8.13/gc.properties b/auth-service/.gradle/8.13/gc.properties
deleted file mode 100644
index e69de29..0000000
diff --git a/auth-service/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/auth-service/.gradle/buildOutputCleanup/buildOutputCleanup.lock
deleted file mode 100644
index 8cb4cde28e862ced8d3109ea4ffcbcd881d35864..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 17
UcmZQx&nb4BcPFW@5%EQt1wH2<{}0PG|TnDl!!L
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
DHx0+F

diff --git a/auth-service/gradle/wrapper/gradle-wrapper.properties b/auth-service/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 37f853b..0000000
--- a/auth-service/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,7 +0,0 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
-networkTimeout=10000
-validateDistributionUrl=true
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
diff --git a/auth-service/gradlew b/auth-service/gradlew
deleted file mode 100644
index faf9300..0000000
--- a/auth-service/gradlew
+++ /dev/null
@@ -1,251 +0,0 @@
-#!/bin/sh
-
-#
-# Copyright © 2015-2021 the original authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-# SPDX-License-Identifier: Apache-2.0
-#
-
-##############################################################################
-#
-#   Gradle start up script for POSIX generated by Gradle.
-#
-#   Important for running:
-#
-#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
-#       noncompliant, but you have some other compliant shell such as ksh or
-#       bash, then to run this script, type that shell name before the whole
-#       command line, like:
-#
-#           ksh Gradle
-#
-#       Busybox and similar reduced shells will NOT work, because this script
-#       requires all of these POSIX shell features:
-#         * functions;
-#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
-#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
-#         * compound commands having a testable exit status, especially «case»;
-#         * various built-in commands including «command», «set», and «ulimit».
-#
-#   Important for patching:
-#
-#   (2) This script targets any POSIX shell, so it avoids extensions provided
-#       by Bash, Ksh, etc; in particular arrays are avoided.
-#
-#       The "traditional" practice of packing multiple parameters into a
-#       space-separated string is a well documented source of bugs and security
-#       problems, so this is (mostly) avoided, by progressively accumulating
-#       options in "$@", and eventually passing that to Java.
-#
-#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
-#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
-#       see the in-line comments for details.
-#
-#       There are tweaks for specific operating systems such as AIX, CygWin,
-#       Darwin, MinGW, and NonStop.
-#
-#   (3) This script is generated from the Groovy template
-#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
-#       within the Gradle project.
-#
-#       You can find Gradle at https://github.com/gradle/gradle/.
-#
-##############################################################################
-
-# Attempt to set APP_HOME
-
-# Resolve links: $0 may be a link
-app_path=$0
-
-# Need this for daisy-chained symlinks.
-while
-    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
-    [ -h "$app_path" ]
-do
-    ls=$( ls -ld "$app_path" )
-    link=${ls#*' -> '}
-    case $link in             #(
-      /*)   app_path=$link ;; #(
-      *)    app_path=$APP_HOME$link ;;
-    esac
-done
-
-# This is normally unused
-# shellcheck disable=SC2034
-APP_BASE_NAME=${0##*/}
-# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD=maximum
-
-warn () {
-    echo "$*"
-} >&2
-
-die () {
-    echo
-    echo "$*"
-    echo
-    exit 1
-} >&2
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-nonstop=false
-case "$( uname )" in                #(
-  CYGWIN* )         cygwin=true  ;; #(
-  Darwin* )         darwin=true  ;; #(
-  MSYS* | MINGW* )  msys=true    ;; #(
-  NONSTOP* )        nonstop=true ;;
-esac
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
-    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
-        # IBM's JDK on AIX uses strange locations for the executables
-        JAVACMD=$JAVA_HOME/jre/sh/java
-    else
-        JAVACMD=$JAVA_HOME/bin/java
-    fi
-    if [ ! -x "$JAVACMD" ] ; then
-        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-    fi
-else
-    JAVACMD=java
-    if ! command -v java >/dev/null 2>&1
-    then
-        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-    fi
-fi
-
-# Increase the maximum file descriptors if we can.
-if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
-    case $MAX_FD in #(
-      max*)
-        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
-        # shellcheck disable=SC2039,SC3045
-        MAX_FD=$( ulimit -H -n ) ||
-            warn "Could not query maximum file descriptor limit"
-    esac
-    case $MAX_FD in  #(
-      '' | soft) :;; #(
-      *)
-        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
-        # shellcheck disable=SC2039,SC3045
-        ulimit -n "$MAX_FD" ||
-            warn "Could not set maximum file descriptor limit to $MAX_FD"
-    esac
-fi
-
-# Collect all arguments for the java command, stacking in reverse order:
-#   * args from the command line
-#   * the main class name
-#   * -classpath
-#   * -D...appname settings
-#   * --module-path (only if needed)
-#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if "$cygwin" || "$msys" ; then
-    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
-    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
-
-    JAVACMD=$( cygpath --unix "$JAVACMD" )
-
-    # Now convert the arguments - kludge to limit ourselves to /bin/sh
-    for arg do
-        if
-            case $arg in                                #(
-              -*)   false ;;                            # don't mess with options #(
-              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
-                    [ -e "$t" ] ;;                      #(
-              *)    false ;;
-            esac
-        then
-            arg=$( cygpath --path --ignore --mixed "$arg" )
-        fi
-        # Roll the args list around exactly as many times as the number of
-        # args, so each arg winds up back in the position where it started, but
-        # possibly modified.
-        #
-        # NB: a `for` loop captures its iteration list before it begins, so
-        # changing the positional parameters here affects neither the number of
-        # iterations, nor the values presented in `arg`.
-        shift                   # remove old arg
-        set -- "$@" "$arg"      # push replacement arg
-    done
-fi
-
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
-
-# Collect all arguments for the java command:
-#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
-#     and any embedded shellness will be escaped.
-#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
-#     treated as '${Hostname}' itself on the command line.
-
-set -- \
-        "-Dorg.gradle.appname=$APP_BASE_NAME" \
-        -classpath "$CLASSPATH" \
-        org.gradle.wrapper.GradleWrapperMain \
-        "$@"
-
-# Stop when "xargs" is not available.
-if ! command -v xargs >/dev/null 2>&1
-then
-    die "xargs is not available"
-fi
-
-# Use "xargs" to parse quoted args.
-#
-# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
-#
-# In Bash we could simply go:
-#
-#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
-#   set -- "${ARGS[@]}" "$@"
-#
-# but POSIX shell has neither arrays nor command substitution, so instead we
-# post-process each arg (as a line of input to sed) to backslash-escape any
-# character that might be a shell metacharacter, then use eval to reverse
-# that process (while maintaining the separation between arguments), and wrap
-# the whole thing up as a single "set" statement.
-#
-# This will of course break if any of these variables contains a newline or
-# an unmatched quote.
-#
-
-eval "set -- $(
-        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
-        xargs -n1 |
-        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
-        tr '\n' ' '
-    )" '"$@"'
-
-exec "$JAVACMD" "$@"
diff --git a/auth-service/gradlew.bat b/auth-service/gradlew.bat
deleted file mode 100644
index 9d21a21..0000000
--- a/auth-service/gradlew.bat
+++ /dev/null
@@ -1,94 +0,0 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem      https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-@rem SPDX-License-Identifier: Apache-2.0
-@rem
-
-@if "%DEBUG%"=="" @echo off
-@rem ##########################################################################
-@rem
-@rem  Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%"=="" set DIRNAME=.
-@rem This is normally unused
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if %ERRORLEVEL% equ 0 goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-goto fail
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if %ERRORLEVEL% equ 0 goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-set EXIT_CODE=%ERRORLEVEL%
-if %EXIT_CODE% equ 0 set EXIT_CODE=1
-if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
-exit /b %EXIT_CODE%
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
diff --git a/auth-service/settings.gradle b/auth-service/settings.gradle
deleted file mode 100644
index d4c3eb3..0000000
--- a/auth-service/settings.gradle
+++ /dev/null
@@ -1 +0,0 @@
-rootProject.name = 'auth-service'
diff --git a/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java b/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java
deleted file mode 100644
index 59186b4..0000000
--- a/auth-service/src/main/java/com/example/authservice/AuthServiceApplication.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.example.authservice;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-@SpringBootApplication
-public class AuthServiceApplication {
-
-    public static void main(String[] args) {
-        SpringApplication.run(AuthServiceApplication.class, args);
-    }
-
-}
diff --git a/auth-service/src/main/resources/application.properties b/auth-service/src/main/resources/application.properties
deleted file mode 100644
index e536707..0000000
--- a/auth-service/src/main/resources/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-spring.application.name=auth-service
diff --git a/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java b/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java
deleted file mode 100644
index 2e763c4..0000000
--- a/auth-service/src/test/java/com/example/authservice/AuthServiceApplicationTests.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.example.authservice;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class AuthServiceApplicationTests {
-
-    @Test
-    void contextLoads() {
-    }
-
-}

From ec5d0efaed4f6b3bcda1ae9352a56ce18ecea640 Mon Sep 17 00:00:00 2001
From: suminmmmmmmm 
Date: Mon, 28 Apr 2025 17:02:26 +0900
Subject: [PATCH 166/361] =?UTF-8?q?Refactor:=20redis=EC=97=90=20=EB=A6=AC?=
 =?UTF-8?q?=ED=94=84=EB=A0=88=EC=8B=9C=20=EC=A0=80=EC=9E=A5=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

---
 auth-service/bin/main/application.properties  |   1 +
 .../authservice/AuthServiceApplication.class  | Bin 0 -> 738 bytes
 .../AuthServiceApplicationTests.class         | Bin 0 -> 566 bytes
 .../userservice/config/ApplicationConfig.java |   2 +-
 .../OAuth2AuthenticationSuccessHandler.java   |   2 +-
 .../userservice/config/RedisConfig.java       |   5 +--
 .../com/example/userservice/dto/AuthDto.java  |   8 +++-
 .../userservice/entity/RefreshToken.java      |  19 +++-----
 .../userservice/service/AuthService.java      |  41 +++++++++---------
 .../userservice/service/TokenService.java     |  16 +++----
 10 files changed, 43 insertions(+), 51 deletions(-)
 create mode 100644 auth-service/bin/main/application.properties
 create mode 100644 auth-service/bin/main/com/example/authservice/AuthServiceApplication.class
 create mode 100644 auth-service/bin/test/com/example/authservice/AuthServiceApplicationTests.class

diff --git a/auth-service/bin/main/application.properties b/auth-service/bin/main/application.properties
new file mode 100644
index 0000000..e536707
--- /dev/null
+++ b/auth-service/bin/main/application.properties
@@ -0,0 +1 @@
+spring.application.name=auth-service
diff --git a/auth-service/bin/main/com/example/authservice/AuthServiceApplication.class b/auth-service/bin/main/com/example/authservice/AuthServiceApplication.class
new file mode 100644
index 0000000000000000000000000000000000000000..7800eb1a8ef5c1114461dfa6455a448a4f10e2b0
GIT binary patch
literal 738
zcmb7Cu~Hi`5PdQRo5KWSh`~UU&bUCS(l{^!o=J5>W@5%EQt1wH2<{}0PG|TnDl!!L
zfP7RYtFve9;u@M=?OW;X+g<(r_x1towy=&3hMiDp
zd0k|Yv^OG3B||4Pu}CMvDjzSvy1Q37!{_jQs)kfC6G}65`@h1ay0NoLC&SZ8fL-i0
z&|w%DoA7+<@jELr`DE-PkBo7AT!JgTwy=+n)R8TSIO?zPj#825!>USQ6@6^9lh2Ni
zjJ{JzVSU3ZbqMxp--NIUwZB)v%n*!CVPpAC`RN^3ujkD7&d~c&Xs0qcQMvN9v^K6>
zACX^FN-QTp>o=se)F>D8xT4XlXdU7cXe9K3fEt}>xnvs@aq^7(`rsAKmogoY^=Z(B
zEwZio1@hYzZQ=uV2=R} memberOptional = memberRepository.findByUserId(request.getUser_id());
-        if (memberOptional.isEmpty()) {
-            System.out.println("사용자를 찾을 수 없음: " + request.getUser_id());
-            throw new RuntimeException("User not found");
-        } else {
-            System.out.println("사용자 찾음: ID=" + memberOptional.get().getMemberId() + ", UserID=" + memberOptional.get().getUserId());
-        }
-        Member member = memberOptional.get();
+        // 1. 사용자 조회 (userId로 조회)
+        Member member = memberRepository.findByUserId(request.getUser_id())
+                .orElseThrow(() -> {
+                    System.out.println("사용자를 찾을 수 없음: " + request.getUser_id());
+                    return new RuntimeException("User not found");
+                });
+
         System.out.println("사용자 찾음: ID=" + member.getMemberId() + ", UserID=" + member.getUserId());
 
-        // 인증 시도 - userId 사용
+        // 2. 인증 시도
         try {
             authenticationManager.authenticate(
                     new UsernamePasswordAuthenticationToken(
@@ -52,18 +49,15 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
             throw e;
         }
 
-        Member member = memberOptional.get();
-
-        // JWT 토큰 생성 - userId 사용
+        // 3. JWT 토큰 발급
         UserDetails userDetails = new User(member.getUserId(), member.getPassword(), Collections.emptyList());
         String accessToken = jwtUtil.generateToken(userDetails);
         String refreshToken = jwtUtil.generateRefreshToken(userDetails);
 
-        // 4. Refresh Token을 Redis에 저장
-        // 리프레시 토큰 저장
+        // 4. Refresh Token Redis 저장
         tokenService.saveRefreshToken(member.getMemberId(), refreshToken);
 
-        // 5. 결과 리턴
+        // 5. 결과 반환
         return AuthDto.LoginResponse.builder()
                 .accessToken(accessToken)
                 .refreshToken(refreshToken)
@@ -73,11 +67,16 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
                 .build();
     }
 
+    // 로그아웃
     public AuthDto.LogoutResponse logout(AuthDto.LogoutRequest request) {
-        // 로그아웃 로직도 나중에 redis에서 refresh token 삭제하는 걸 추가할 수 있음
+        // 1. 사용자의 Redis Refresh Token 삭제
+        Long memberId = Long.parseLong(request.getMemberId());
+        tokenService.deleteRefreshToken(memberId);
+
+        // 2. 성공 리턴
         return AuthDto.LogoutResponse.builder()
                 .success(true)
-                .message("Logout successful")
+                .message("로그아웃 완료: RefreshToken 삭제 완료")
                 .build();
     }
 }
diff --git a/user-service/src/main/java/com/example/userservice/service/TokenService.java b/user-service/src/main/java/com/example/userservice/service/TokenService.java
index e16a237..dd8ca0c 100644
--- a/user-service/src/main/java/com/example/userservice/service/TokenService.java
+++ b/user-service/src/main/java/com/example/userservice/service/TokenService.java
@@ -2,8 +2,8 @@
 
 import com.example.userservice.entity.Member;
 import com.example.userservice.entity.RefreshToken;
+import com.example.userservice.repository.jpa.MemberRepository;
 import com.example.userservice.repository.redis.RefreshTokenRepository;
-import com.example.userservice.repository.MemberRepository;
 import com.example.userservice.security.JwtUtil;
 import lombok.RequiredArgsConstructor;
 import org.springframework.security.core.userdetails.User;
@@ -27,34 +27,32 @@ public String createNewAccessToken(String refreshToken) {
             throw new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다.");
         }
 
-        // 데이터베이스에서 리프레시 토큰 확인
+        // 데이터베이스(레디스)에서 리프레시 토큰 확인
         Optional storedToken = refreshTokenRepository.findByRefreshToken(refreshToken);
         if (storedToken.isEmpty()) {
             throw new IllegalArgumentException("존재하지 않는 리프레시 토큰입니다.");
         }
 
         // 사용자 ID로 사용자 조회
-        Long userId = storedToken.get().getUserId();
+        Long userId = Long.valueOf(storedToken.get().getId()); // id는 String으로 저장되니까 Long으로 변환
         Member member = memberRepository.findById(userId)
                 .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
 
         // 새 액세스 토큰 생성
-        UserDetails userDetails = new User(member.getNickname(), member.getPassword(), Collections.emptyList());
+        UserDetails userDetails = new User(member.getUserId(), member.getPassword(), Collections.emptyList());
         return jwtUtil.generateToken(userDetails);
     }
 
     public void saveRefreshToken(Long memberId, String refreshToken) {
         RefreshToken token = RefreshToken.builder()
-                .id(String.valueOf(memberId))  // 여기!! id로 memberId를 문자열로 저장
+                .id(String.valueOf(memberId))   // memberId를 String으로 변환해서 저장
                 .refreshToken(refreshToken)
                 .build();
-        RefreshToken token = new RefreshToken(memberId, refreshToken);
+
         refreshTokenRepository.save(token);
     }
 
     public void deleteRefreshToken(Long memberId) {
-        refreshTokenRepository.deleteById((memberId));
+        refreshTokenRepository.deleteById(Long.valueOf(String.valueOf(memberId)));  // 여기서도 String 변환 필요!
     }
 }
-
-}
\ No newline at end of file

From cb9d7b0b64f4297b02fa0013b4af9e5eee7d2dc7 Mon Sep 17 00:00:00 2001
From: suminmmmmmmm 
Date: Mon, 28 Apr 2025 17:02:26 +0900
Subject: [PATCH 167/361] =?UTF-8?q?Refactor:=20redis=EC=97=90=20=EB=A6=AC?=
 =?UTF-8?q?=ED=94=84=EB=A0=88=EC=8B=9C=20=EC=A0=80=EC=9E=A5=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

---
 auth-service/bin/main/application.properties  |   1 +
 .../authservice/AuthServiceApplication.class  | Bin 0 -> 738 bytes
 .../AuthServiceApplicationTests.class         | Bin 0 -> 566 bytes
 .../userservice/config/ApplicationConfig.java |   2 +-
 .../OAuth2AuthenticationSuccessHandler.java   |   2 +-
 .../userservice/config/RedisConfig.java       |   5 +--
 .../com/example/userservice/dto/AuthDto.java  |   8 +++-
 .../userservice/entity/RefreshToken.java      |  19 +++-----
 .../userservice/service/AuthService.java      |  41 +++++++++---------
 .../userservice/service/TokenService.java     |  16 +++----
 10 files changed, 43 insertions(+), 51 deletions(-)
 create mode 100644 auth-service/bin/main/application.properties
 create mode 100644 auth-service/bin/main/com/example/authservice/AuthServiceApplication.class
 create mode 100644 auth-service/bin/test/com/example/authservice/AuthServiceApplicationTests.class

diff --git a/auth-service/bin/main/application.properties b/auth-service/bin/main/application.properties
new file mode 100644
index 0000000..e536707
--- /dev/null
+++ b/auth-service/bin/main/application.properties
@@ -0,0 +1 @@
+spring.application.name=auth-service
diff --git a/auth-service/bin/main/com/example/authservice/AuthServiceApplication.class b/auth-service/bin/main/com/example/authservice/AuthServiceApplication.class
new file mode 100644
index 0000000000000000000000000000000000000000..7800eb1a8ef5c1114461dfa6455a448a4f10e2b0
GIT binary patch
literal 738
zcmb7Cu~Hi`5PdQRo5KWSh`~UU&bUCS(l{^!o=J5>W@5%EQt1wH2<{}0PG|TnDl!!L
zfP7RYtFve9;u@M=?OW;X+g<(r_x1towy=&3hMiDp
zd0k|Yv^OG3B||4Pu}CMvDjzSvy1Q37!{_jQs)kfC6G}65`@h1ay0NoLC&SZ8fL-i0
z&|w%DoA7+<@jELr`DE-PkBo7AT!JgTwy=+n)R8TSIO?zPj#825!>USQ6@6^9lh2Ni
zjJ{JzVSU3ZbqMxp--NIUwZB)v%n*!CVPpAC`RN^3ujkD7&d~c&Xs0qcQMvN9v^K6>
zACX^FN-QTp>o=se)F>D8xT4XlXdU7cXe9K3fEt}>xnvs@aq^7(`rsAKmogoY^=Z(B
zEwZio1@hYzZQ=uV2=R} memberOptional = memberRepository.findByUserId(request.getUser_id());
-        if (memberOptional.isEmpty()) {
-            System.out.println("사용자를 찾을 수 없음: " + request.getUser_id());
-            throw new RuntimeException("User not found");
-        } else {
-            System.out.println("사용자 찾음: ID=" + memberOptional.get().getMemberId() + ", UserID=" + memberOptional.get().getUserId());
-        }
-        Member member = memberOptional.get();
+        // 1. 사용자 조회 (userId로 조회)
+        Member member = memberRepository.findByUserId(request.getUser_id())
+                .orElseThrow(() -> {
+                    System.out.println("사용자를 찾을 수 없음: " + request.getUser_id());
+                    return new RuntimeException("User not found");
+                });
+
         System.out.println("사용자 찾음: ID=" + member.getMemberId() + ", UserID=" + member.getUserId());
 
-        // 인증 시도 - userId 사용
+        // 2. 인증 시도
         try {
             authenticationManager.authenticate(
                     new UsernamePasswordAuthenticationToken(
@@ -52,18 +49,15 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
             throw e;
         }
 
-        Member member = memberOptional.get();
-
-        // JWT 토큰 생성 - userId 사용
+        // 3. JWT 토큰 발급
         UserDetails userDetails = new User(member.getUserId(), member.getPassword(), Collections.emptyList());
         String accessToken = jwtUtil.generateToken(userDetails);
         String refreshToken = jwtUtil.generateRefreshToken(userDetails);
 
-        // 4. Refresh Token을 Redis에 저장
-        // 리프레시 토큰 저장
+        // 4. Refresh Token Redis 저장
         tokenService.saveRefreshToken(member.getMemberId(), refreshToken);
 
-        // 5. 결과 리턴
+        // 5. 결과 반환
         return AuthDto.LoginResponse.builder()
                 .accessToken(accessToken)
                 .refreshToken(refreshToken)
@@ -73,11 +67,16 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
                 .build();
     }
 
+    // 로그아웃
     public AuthDto.LogoutResponse logout(AuthDto.LogoutRequest request) {
-        // 로그아웃 로직도 나중에 redis에서 refresh token 삭제하는 걸 추가할 수 있음
+        // 1. 사용자의 Redis Refresh Token 삭제
+        Long memberId = Long.parseLong(request.getMemberId());
+        tokenService.deleteRefreshToken(memberId);
+
+        // 2. 성공 리턴
         return AuthDto.LogoutResponse.builder()
                 .success(true)
-                .message("Logout successful")
+                .message("로그아웃 완료: RefreshToken 삭제 완료")
                 .build();
     }
 }
diff --git a/user-service/src/main/java/com/example/userservice/service/TokenService.java b/user-service/src/main/java/com/example/userservice/service/TokenService.java
index e16a237..dd8ca0c 100644
--- a/user-service/src/main/java/com/example/userservice/service/TokenService.java
+++ b/user-service/src/main/java/com/example/userservice/service/TokenService.java
@@ -2,8 +2,8 @@
 
 import com.example.userservice.entity.Member;
 import com.example.userservice.entity.RefreshToken;
+import com.example.userservice.repository.jpa.MemberRepository;
 import com.example.userservice.repository.redis.RefreshTokenRepository;
-import com.example.userservice.repository.MemberRepository;
 import com.example.userservice.security.JwtUtil;
 import lombok.RequiredArgsConstructor;
 import org.springframework.security.core.userdetails.User;
@@ -27,34 +27,32 @@ public String createNewAccessToken(String refreshToken) {
             throw new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다.");
         }
 
-        // 데이터베이스에서 리프레시 토큰 확인
+        // 데이터베이스(레디스)에서 리프레시 토큰 확인
         Optional storedToken = refreshTokenRepository.findByRefreshToken(refreshToken);
         if (storedToken.isEmpty()) {
             throw new IllegalArgumentException("존재하지 않는 리프레시 토큰입니다.");
         }
 
         // 사용자 ID로 사용자 조회
-        Long userId = storedToken.get().getUserId();
+        Long userId = Long.valueOf(storedToken.get().getId()); // id는 String으로 저장되니까 Long으로 변환
         Member member = memberRepository.findById(userId)
                 .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
 
         // 새 액세스 토큰 생성
-        UserDetails userDetails = new User(member.getNickname(), member.getPassword(), Collections.emptyList());
+        UserDetails userDetails = new User(member.getUserId(), member.getPassword(), Collections.emptyList());
         return jwtUtil.generateToken(userDetails);
     }
 
     public void saveRefreshToken(Long memberId, String refreshToken) {
         RefreshToken token = RefreshToken.builder()
-                .id(String.valueOf(memberId))  // 여기!! id로 memberId를 문자열로 저장
+                .id(String.valueOf(memberId))   // memberId를 String으로 변환해서 저장
                 .refreshToken(refreshToken)
                 .build();
-        RefreshToken token = new RefreshToken(memberId, refreshToken);
+
         refreshTokenRepository.save(token);
     }
 
     public void deleteRefreshToken(Long memberId) {
-        refreshTokenRepository.deleteById((memberId));
+        refreshTokenRepository.deleteById(Long.valueOf(String.valueOf(memberId)));  // 여기서도 String 변환 필요!
     }
 }
-
-}
\ No newline at end of file

From 9bf3d34cb0f3674f7996bd63435bb002c49cda54 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 17:09:52 +0900
Subject: [PATCH 168/361] =?UTF-8?q?place=20=EC=84=9C=EB=B9=84=EC=8A=A4=20D?=
 =?UTF-8?q?ockerfile=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 place-service/Dockerfile | 11 +----------
 1 file changed, 1 insertion(+), 10 deletions(-)

diff --git a/place-service/Dockerfile b/place-service/Dockerfile
index b03d5d3..5508683 100644
--- a/place-service/Dockerfile
+++ b/place-service/Dockerfile
@@ -8,15 +8,6 @@ FROM eclipse-temurin:17-jdk
 WORKDIR /app
 COPY --from=build /app/build/libs/*.jar app.jar
 EXPOSE 8080
-<<<<<<< HEAD
 ENTRYPOINT ["java", "-jar", "app.jar"]
-=======
-ENTRYPOINT ["java", "-jar", "app.jar"]
-
-ARG TOUR_API_KEY
-<<<<<<< HEAD
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
-=======
 
-COPY .env .env
->>>>>>> 7d9e41f58fcc018b3cd8b94bb5aa79ab788ea9b5
+COPY .env .env
\ No newline at end of file

From 2be0d7114c3b6a403c404cdef88c664eda891b8f Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 17:09:52 +0900
Subject: [PATCH 169/361] =?UTF-8?q?place=20=EC=84=9C=EB=B9=84=EC=8A=A4=20D?=
 =?UTF-8?q?ockerfile=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 place-service/Dockerfile | 11 +----------
 1 file changed, 1 insertion(+), 10 deletions(-)

diff --git a/place-service/Dockerfile b/place-service/Dockerfile
index b03d5d3..5508683 100644
--- a/place-service/Dockerfile
+++ b/place-service/Dockerfile
@@ -8,15 +8,6 @@ FROM eclipse-temurin:17-jdk
 WORKDIR /app
 COPY --from=build /app/build/libs/*.jar app.jar
 EXPOSE 8080
-<<<<<<< HEAD
 ENTRYPOINT ["java", "-jar", "app.jar"]
-=======
-ENTRYPOINT ["java", "-jar", "app.jar"]
-
-ARG TOUR_API_KEY
-<<<<<<< HEAD
->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793
-=======
 
-COPY .env .env
->>>>>>> 7d9e41f58fcc018b3cd8b94bb5aa79ab788ea9b5
+COPY .env .env
\ No newline at end of file

From 30d34a29a7a03fb9320762283fb27b00ceead470 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 17:24:15 +0900
Subject: [PATCH 170/361] =?UTF-8?q?auth=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?=
 =?UTF-8?q?=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Back-STARS                                    |   1 -
 auth-service/bin/main/application.properties  |   1 -
 .../authservice/AuthServiceApplication.class  | Bin 738 -> 0 bytes
 .../AuthServiceApplicationTests.class         | Bin 566 -> 0 bytes
 gateway/.gitattributes                        |   3 +
 gateway/.gitignore                            |  37 +++
 gateway/build.gradle                          |  29 ++
 gateway/gradle/wrapper/gradle-wrapper.jar     | Bin 0 -> 43705 bytes
 .../gradle/wrapper/gradle-wrapper.properties  |   7 +
 gateway/gradlew                               | 251 ++++++++++++++++++
 gateway/gradlew.bat                           |  94 +++++++
 gateway/settings.gradle                       |   1 +
 .../example/gateway/GatewayApplication.java   |  13 +
 .../src/main/resources/application.properties |   1 +
 .../gateway/GatewayApplicationTests.java      |  13 +
 15 files changed, 449 insertions(+), 2 deletions(-)
 delete mode 160000 Back-STARS
 delete mode 100644 auth-service/bin/main/application.properties
 delete mode 100644 auth-service/bin/main/com/example/authservice/AuthServiceApplication.class
 delete mode 100644 auth-service/bin/test/com/example/authservice/AuthServiceApplicationTests.class
 create mode 100644 gateway/.gitattributes
 create mode 100644 gateway/.gitignore
 create mode 100644 gateway/build.gradle
 create mode 100644 gateway/gradle/wrapper/gradle-wrapper.jar
 create mode 100644 gateway/gradle/wrapper/gradle-wrapper.properties
 create mode 100644 gateway/gradlew
 create mode 100644 gateway/gradlew.bat
 create mode 100644 gateway/settings.gradle
 create mode 100644 gateway/src/main/java/com/example/gateway/GatewayApplication.java
 create mode 100644 gateway/src/main/resources/application.properties
 create mode 100644 gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java

diff --git a/Back-STARS b/Back-STARS
deleted file mode 160000
index c42a641..0000000
--- a/Back-STARS
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit c42a641af776e3112ad553dcd34f2ed7659c268b
diff --git a/auth-service/bin/main/application.properties b/auth-service/bin/main/application.properties
deleted file mode 100644
index e536707..0000000
--- a/auth-service/bin/main/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-spring.application.name=auth-service
diff --git a/auth-service/bin/main/com/example/authservice/AuthServiceApplication.class b/auth-service/bin/main/com/example/authservice/AuthServiceApplication.class
deleted file mode 100644
index 7800eb1a8ef5c1114461dfa6455a448a4f10e2b0..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 738
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
DHx0+F

literal 0
HcmV?d00001

diff --git a/gateway/gradle/wrapper/gradle-wrapper.properties b/gateway/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..37f853b
--- /dev/null
+++ b/gateway/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gateway/gradlew b/gateway/gradlew
new file mode 100644
index 0000000..faf9300
--- /dev/null
+++ b/gateway/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gateway/gradlew.bat b/gateway/gradlew.bat
new file mode 100644
index 0000000..9d21a21
--- /dev/null
+++ b/gateway/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/gateway/settings.gradle b/gateway/settings.gradle
new file mode 100644
index 0000000..a239fa0
--- /dev/null
+++ b/gateway/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'gateway'
diff --git a/gateway/src/main/java/com/example/gateway/GatewayApplication.java b/gateway/src/main/java/com/example/gateway/GatewayApplication.java
new file mode 100644
index 0000000..b92f26a
--- /dev/null
+++ b/gateway/src/main/java/com/example/gateway/GatewayApplication.java
@@ -0,0 +1,13 @@
+package com.example.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/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties
new file mode 100644
index 0000000..6365994
--- /dev/null
+++ b/gateway/src/main/resources/application.properties
@@ -0,0 +1 @@
+spring.application.name=gateway
diff --git a/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java b/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java
new file mode 100644
index 0000000..0c9e48c
--- /dev/null
+++ b/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java
@@ -0,0 +1,13 @@
+package com.example.gateway;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class GatewayApplicationTests {
+
+    @Test
+    void contextLoads() {
+    }
+
+}

From 0806a0fa573c6d69cff8ac877e63629e644cf630 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Mon, 28 Apr 2025 17:24:15 +0900
Subject: [PATCH 171/361] =?UTF-8?q?auth=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?=
 =?UTF-8?q?=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Back-STARS                                    |   1 -
 auth-service/bin/main/application.properties  |   1 -
 .../authservice/AuthServiceApplication.class  | Bin 738 -> 0 bytes
 .../AuthServiceApplicationTests.class         | Bin 566 -> 0 bytes
 gateway/.gitattributes                        |   3 +
 gateway/.gitignore                            |  37 +++
 gateway/build.gradle                          |  29 ++
 gateway/gradle/wrapper/gradle-wrapper.jar     | Bin 0 -> 43705 bytes
 .../gradle/wrapper/gradle-wrapper.properties  |   7 +
 gateway/gradlew                               | 251 ++++++++++++++++++
 gateway/gradlew.bat                           |  94 +++++++
 gateway/settings.gradle                       |   1 +
 .../example/gateway/GatewayApplication.java   |  13 +
 .../src/main/resources/application.properties |   1 +
 .../gateway/GatewayApplicationTests.java      |  13 +
 15 files changed, 449 insertions(+), 2 deletions(-)
 delete mode 160000 Back-STARS
 delete mode 100644 auth-service/bin/main/application.properties
 delete mode 100644 auth-service/bin/main/com/example/authservice/AuthServiceApplication.class
 delete mode 100644 auth-service/bin/test/com/example/authservice/AuthServiceApplicationTests.class
 create mode 100644 gateway/.gitattributes
 create mode 100644 gateway/.gitignore
 create mode 100644 gateway/build.gradle
 create mode 100644 gateway/gradle/wrapper/gradle-wrapper.jar
 create mode 100644 gateway/gradle/wrapper/gradle-wrapper.properties
 create mode 100644 gateway/gradlew
 create mode 100644 gateway/gradlew.bat
 create mode 100644 gateway/settings.gradle
 create mode 100644 gateway/src/main/java/com/example/gateway/GatewayApplication.java
 create mode 100644 gateway/src/main/resources/application.properties
 create mode 100644 gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java

diff --git a/Back-STARS b/Back-STARS
deleted file mode 160000
index c42a641..0000000
--- a/Back-STARS
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit c42a641af776e3112ad553dcd34f2ed7659c268b
diff --git a/auth-service/bin/main/application.properties b/auth-service/bin/main/application.properties
deleted file mode 100644
index e536707..0000000
--- a/auth-service/bin/main/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-spring.application.name=auth-service
diff --git a/auth-service/bin/main/com/example/authservice/AuthServiceApplication.class b/auth-service/bin/main/com/example/authservice/AuthServiceApplication.class
deleted file mode 100644
index 7800eb1a8ef5c1114461dfa6455a448a4f10e2b0..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 738
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
DHx0+F

literal 0
HcmV?d00001

diff --git a/gateway/gradle/wrapper/gradle-wrapper.properties b/gateway/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..37f853b
--- /dev/null
+++ b/gateway/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gateway/gradlew b/gateway/gradlew
new file mode 100644
index 0000000..faf9300
--- /dev/null
+++ b/gateway/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gateway/gradlew.bat b/gateway/gradlew.bat
new file mode 100644
index 0000000..9d21a21
--- /dev/null
+++ b/gateway/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/gateway/settings.gradle b/gateway/settings.gradle
new file mode 100644
index 0000000..a239fa0
--- /dev/null
+++ b/gateway/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'gateway'
diff --git a/gateway/src/main/java/com/example/gateway/GatewayApplication.java b/gateway/src/main/java/com/example/gateway/GatewayApplication.java
new file mode 100644
index 0000000..b92f26a
--- /dev/null
+++ b/gateway/src/main/java/com/example/gateway/GatewayApplication.java
@@ -0,0 +1,13 @@
+package com.example.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/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties
new file mode 100644
index 0000000..6365994
--- /dev/null
+++ b/gateway/src/main/resources/application.properties
@@ -0,0 +1 @@
+spring.application.name=gateway
diff --git a/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java b/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java
new file mode 100644
index 0000000..0c9e48c
--- /dev/null
+++ b/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java
@@ -0,0 +1,13 @@
+package com.example.gateway;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class GatewayApplicationTests {
+
+    @Test
+    void contextLoads() {
+    }
+
+}

From 8ed2fd0c2a0ea5b8837a600a619c2b9a4b0bb138 Mon Sep 17 00:00:00 2001
From: YunHwa0905 
Date: Tue, 29 Apr 2025 10:46:12 +0900
Subject: [PATCH 172/361] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?=
 =?UTF-8?q?=EC=9E=85=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=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

---
 user-service/.gitignore                       |   1 -
 .../userservice/config/ApplicationConfig.java |  25 +++--
 .../userservice/config/SecurityConfig.java    |  22 ++--
 .../userservice/controller/AdminSignup.java   | 104 +++++++++++++++---
 .../controller/AuthController.java            |  61 +++++++++-
 .../controller/MemberController.java          |  73 ++++++++++++
 .../com/example/userservice/dto/AuthDto.java  |  38 +++++--
 .../example/userservice/dto/MemberDto.java    |  49 +++++++--
 .../example/userservice/dto/MemberSign.java   |  21 ++--
 .../example/userservice/entity/Member.java    |  24 ++--
 .../userservice/entity/RefreshToken.java      |   6 +-
 .../redis/RefreshTokenRepository.java         |   5 +-
 .../security/JwtAuthenticationFilter.java     |  17 ++-
 .../userservice/service/AuthService.java      |  26 ++++-
 .../userservice/service/MemberService.java    |  92 +++++++++++++++-
 .../userservice/service/TokenService.java     |  30 ++++-
 16 files changed, 501 insertions(+), 93 deletions(-)
 create mode 100644 user-service/src/main/java/com/example/userservice/controller/MemberController.java

diff --git a/user-service/.gitignore b/user-service/.gitignore
index 1f22f61..261a098 100644
--- a/user-service/.gitignore
+++ b/user-service/.gitignore
@@ -37,5 +37,4 @@ out/
 ### VS Code ###
 .vscode/
 
-.env
 
diff --git a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
index 5e16036..44f2998 100644
--- a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
+++ b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
@@ -8,11 +8,16 @@
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
 import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
 @Configuration
 @RequiredArgsConstructor
 public class ApplicationConfig {
@@ -22,12 +27,18 @@ public class ApplicationConfig {
     @Bean
     public UserDetailsService userDetailsService() {
         return username -> memberRepository.findByUserId(username)
-                .map(member -> new org.springframework.security.core.userdetails.User(
-                        member.getUserId(),  // userId를 사용하도록 변경
-                        member.getPassword(),
-                        java.util.Collections.emptyList()
-                ))
-                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
+                .map(member -> {
+                    List authorities = new ArrayList<>();
+                    // 사용자 역할에 따른 권한 추가
+                    authorities.add(new SimpleGrantedAuthority(member.getRole()));
+
+                    return new org.springframework.security.core.userdetails.User(
+                            member.getUserId(), // userId를 사용자 이름으로 사용
+                            member.getPassword(), // 암호화된 비밀번호
+                            authorities // 권한 목록
+                    );
+                })
+                .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다"));
     }
 
     @Bean
@@ -45,6 +56,6 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration c
 
     @Bean
     public PasswordEncoder passwordEncoder() {
-        return new BCryptPasswordEncoder();
+        return new BCryptPasswordEncoder(); // 비밀번호 암호화에 BCrypt 알고리즘 사용
     }
 }
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
index 0868b9b..9faf817 100644
--- a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
+++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
@@ -4,6 +4,7 @@
 import lombok.RequiredArgsConstructor;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -22,13 +23,22 @@ public class SecurityConfig {
     @Bean
     public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
         http
-                .csrf(csrf -> csrf.disable())
+                .csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화
                 .authorizeHttpRequests(auth -> auth
+                        // 공개 엔드포인트 - 인증 불필요
                         .requestMatchers("/auth/**").permitAll()
-                        .requestMatchers("/admin/**").authenticated()
+
+                        // 관리자 전용 엔드포인트
+                        .requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
+
+                        // 사용자/관리자 접근 가능 엔드포인트
+                        .requestMatchers("/mypage/**").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
+
+                        // 그 외 모든 요청은 인증 필요
                         .anyRequest().authenticated()
                 )
                 .sessionManagement(session -> session
+                        // 세션 상태 없음 (JWT 사용하므로)
                         .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                 )
                 .authenticationProvider(authenticationProvider)
@@ -36,10 +46,4 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
 
         return http.build();
     }
-
-    //비밀번호 암호화 적용
-//    @Bean
-//    public BCryptPasswordEncoder passwordEncoder() {
-//        return new BCryptPasswordEncoder();
-//    }
-}
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
index 073cc06..e069e82 100644
--- a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
+++ b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
@@ -4,51 +4,125 @@
 import com.example.userservice.entity.Member;
 import com.example.userservice.security.JwtUtil;
 import com.example.userservice.service.MemberService;
+import com.example.userservice.service.TokenService;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetails;
-import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
-@Controller
+@RestController
 @RequiredArgsConstructor
 @RequestMapping("/auth")
 public class AdminSignup {
     private final MemberService memberService;
     private final JwtUtil jwtUtil;
+    private final TokenService tokenService;
 
+    /**
+     * 일반 사용자 회원가입 API
+     * @param dto 회원가입 정보 (user_id, password, nickname, birth_year, mbti, gender)
+     * @return 토큰 및 사용자 정보가 포함된 응답
+     */
     @PostMapping("/signup")
     public ResponseEntity> signup(@RequestBody MemberSign dto) {
         try {
             // 디버깅 로그 출력
-            System.out.println("회원가입 요청: userId=" + dto.getUserId() + ", nickname=" + dto.getNickname() + ", birth_year=" + dto.getBirthYear());
+            System.out.println("회원가입 요청: userId=" + dto.getUserId() + ", nickname=" + dto.getNickname()
+                    + ", birth_year=" + dto.getBirthYear()
+                    + ", mbti=" + dto.getMbti()
+                    + ", gender=" + dto.getGender());
 
-            Member member = memberService.registerAdmin(dto);
+            // 일반 사용자로 등록
+            Member member = memberService.registerUser(dto);
+
+            System.out.println("회원가입 성공: memberId=" + member.getMemberId()
+                    + ", userId=" + member.getUserId()
+                    + ", nickname=" + member.getNickname()
+                    + ", role=" + member.getRole());
 
-            System.out.println("회원가입 성공: memberId=" + member.getMemberId() + ", userId=" + member.getUserId() + ", nickname=" + member.getNickname());
+            // 사용자 권한 설정
+            List authorities = Collections.singletonList(
+                    new SimpleGrantedAuthority(member.getRole())
+            );
 
-            // 회원가입 성공하면 토큰 발급 - userId 사용
-            UserDetails userDetails = new User(member.getUserId(), member.getPassword(), Collections.emptyList());
+            // 회원가입 성공하면 토큰 발급
+            UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities);
             String accessToken = jwtUtil.generateToken(userDetails);
             String refreshToken = jwtUtil.generateRefreshToken(userDetails);
 
-            Map tokens = new HashMap<>();
-            tokens.put("accessToken", accessToken);
-            tokens.put("refreshToken", refreshToken);
-            tokens.put("member_id", String.valueOf(member.getMemberId()));
-            tokens.put("user_id", member.getUserId());
-            tokens.put("nickname", member.getNickname());
+            // 리프레시 토큰 저장
+            tokenService.saveRefreshToken(member.getMemberId(), refreshToken);
+
+            // 응답 생성
+            Map response = new HashMap<>();
+            response.put("accessToken", accessToken);
+            response.put("refreshToken", refreshToken);
+            response.put("member_id", String.valueOf(member.getMemberId()));
+            response.put("user_id", member.getUserId());
+            response.put("nickname", member.getNickname());
+            response.put("role", member.getRole());
 
-            return ResponseEntity.ok(tokens);
-        } catch (IllegalArgumentException e) {
+            return ResponseEntity.ok(response);
+        } catch (IllegalStateException e) {
             Map errorResponse = new HashMap<>();
             errorResponse.put("error", "회원가입 실패: " + e.getMessage());
             return ResponseEntity.badRequest().body(errorResponse);
         }
     }
+
+    /**
+     * 관리자 회원가입 API
+     * @param dto 관리자 회원가입 정보
+     * @return 토큰 및 관리자 정보가 포함된 응답
+     */
+    @PostMapping("/admin/signup")
+    public ResponseEntity> adminSignup(@RequestBody MemberSign dto) {
+        try {
+            // 디버깅 로그 출력
+            System.out.println("관리자 회원가입 요청: userId=" + dto.getUserId() + ", nickname=" + dto.getNickname());
+
+            // 관리자로 등록
+            Member member = memberService.registerAdmin(dto);
+
+            System.out.println("관리자 회원가입 성공: memberId=" + member.getMemberId()
+                    + ", userId=" + member.getUserId()
+                    + ", nickname=" + member.getNickname()
+                    + ", role=" + member.getRole());
+
+            // 관리자 권한 설정
+            List authorities = Collections.singletonList(
+                    new SimpleGrantedAuthority(member.getRole())
+            );
+
+            // 회원가입 성공하면 토큰 발급
+            UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities);
+            String accessToken = jwtUtil.generateToken(userDetails);
+            String refreshToken = jwtUtil.generateRefreshToken(userDetails);
+
+            // 리프레시 토큰 저장
+            tokenService.saveRefreshToken(member.getMemberId(), refreshToken);
+
+            // 응답 생성
+            Map response = new HashMap<>();
+            response.put("accessToken", accessToken);
+            response.put("refreshToken", refreshToken);
+            response.put("member_id", String.valueOf(member.getMemberId()));
+            response.put("user_id", member.getUserId());
+            response.put("nickname", member.getNickname());
+            response.put("role", member.getRole());
+
+            return ResponseEntity.ok(response);
+        } catch (IllegalStateException e) {
+            Map errorResponse = new HashMap<>();
+            errorResponse.put("error", "관리자 회원가입 실패: " + e.getMessage());
+            return ResponseEntity.badRequest().body(errorResponse);
+        }
+    }
 }
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/controller/AuthController.java b/user-service/src/main/java/com/example/userservice/controller/AuthController.java
index 9defdce..cc38cbf 100644
--- a/user-service/src/main/java/com/example/userservice/controller/AuthController.java
+++ b/user-service/src/main/java/com/example/userservice/controller/AuthController.java
@@ -1,8 +1,13 @@
 package com.example.userservice.controller;
 
 import com.example.userservice.dto.AuthDto;
+import com.example.userservice.entity.Member;
+import com.example.userservice.repository.jpa.MemberRepository;
+import com.example.userservice.security.JwtUtil;
 import com.example.userservice.service.AuthService;
 import com.example.userservice.service.CookieUtil;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.ResponseEntity;
@@ -18,7 +23,15 @@ public class AuthController {
 
     private final AuthService authService;
     private final CookieUtil cookieUtil;
+    private final JwtUtil jwtUtil;
+    private final MemberRepository memberRepository;
 
+    /**
+     * 로그인 API
+     * @param request 로그인 요청 (user_id, password)
+     * @param response HTTP 응답 (쿠키 설정용)
+     * @return 로그인 성공 시 토큰 및 사용자 정보
+     */
     @PostMapping("/login")
     public ResponseEntity login(
             @RequestBody AuthDto.LoginRequest request,
@@ -36,16 +49,58 @@ public ResponseEntity login(
         return ResponseEntity.ok(loginResponse);
     }
 
+    /**
+     * 로그아웃 API
+     * @param request HTTP 요청 (토큰 추출용)
+     * @param response HTTP 응답 (쿠키 삭제용)
+     * @return 로그아웃 성공 여부 및 메시지
+     */
     @PostMapping("/logout")
     public ResponseEntity logout(
-            @RequestBody AuthDto.LogoutRequest request,
+            HttpServletRequest request,
             HttpServletResponse response) {
 
         // 쿠키 삭제
         cookieUtil.addCookie(response, "access_token", "", 0);
         cookieUtil.addCookie(response, "refresh_token", "", 0);
 
-        // authService의 logout 메서드 호출
-        return ResponseEntity.ok(authService.logout(request));
+        // 헤더에서 토큰 추출
+        String authHeader = request.getHeader("Authorization");
+        String jwt = null;
+
+        if (authHeader != null && authHeader.startsWith("Bearer ")) {
+            jwt = authHeader.substring(7);
+        } else {
+            // 쿠키에서 토큰 찾기 (선택적)
+            Cookie[] cookies = request.getCookies();
+            if (cookies != null) {
+                for (Cookie cookie : cookies) {
+                    if ("access_token".equals(cookie.getName())) {
+                        jwt = cookie.getValue();
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (jwt != null) {
+            // 토큰에서 사용자 정보 추출
+            String userId = jwtUtil.extractUsername(jwt);
+            // 사용자 ID로 Member 조회
+            Member member = memberRepository.findByUserId(userId)
+                    .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다"));
+
+            // 로그아웃 처리
+            return ResponseEntity.ok(authService.logout(
+                    AuthDto.LogoutRequest.builder()
+                            .memberId(String.valueOf(member.getMemberId()))
+                            .build()));
+        }
+
+        return ResponseEntity.badRequest().body(
+                AuthDto.LogoutResponse.builder()
+                        .success(false)
+                        .message("유효한 토큰이 없습니다.")
+                        .build());
     }
 }
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/controller/MemberController.java b/user-service/src/main/java/com/example/userservice/controller/MemberController.java
new file mode 100644
index 0000000..d51b3e1
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/controller/MemberController.java
@@ -0,0 +1,73 @@
+package com.example.userservice.controller;
+
+import com.example.userservice.dto.MemberDto;
+import com.example.userservice.security.JwtUtil;
+import com.example.userservice.service.MemberService;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequiredArgsConstructor
+public class MemberController {
+
+    private final MemberService memberService;
+    private final JwtUtil jwtUtil;
+
+    /**
+     * 사용자 프로필 조회 API
+     * @param request HTTP 요청 (토큰에서 사용자 ID 추출용)
+     * @return 사용자 프로필 정보
+     */
+    @GetMapping("/mypage/profile")
+    public ResponseEntity getProfile(HttpServletRequest request) {
+        // 현재 인증된 사용자의 userId 추출
+        String userId = extractUserIdFromRequest(request);
+
+        // 서비스를 통해 프로필 정보 조회
+        MemberDto.ProfileResponse profile = memberService.getProfile(userId);
+
+        return ResponseEntity.ok(profile);
+    }
+
+    /**
+     * 사용자 프로필 수정 API
+     * @param request HTTP 요청 (토큰에서 사용자 ID 추출용)
+     * @param updateRequest 프로필 수정 정보 (닉네임, 생년월일, MBTI, 성별, 비밀번호 등)
+     * @return 업데이트된 사용자 정보
+     */
+    @PostMapping("/mypage/profile/edit")
+    public ResponseEntity updateProfile(
+            HttpServletRequest request,
+            @RequestBody MemberDto.UpdateRequest updateRequest) {
+
+        // 현재 인증된 사용자의 userId 추출
+        String userId = extractUserIdFromRequest(request);
+
+        // 서비스를 통해 프로필 정보 업데이트
+        MemberDto.MemberResponse updatedProfile = memberService.updateProfile(userId, updateRequest);
+
+        return ResponseEntity.ok(updatedProfile);
+    }
+
+    /**
+     * JWT 토큰에서 userId 추출 헬퍼 메서드
+     * @param request HTTP 요청
+     * @return 추출된 사용자 ID
+     */
+    private String extractUserIdFromRequest(HttpServletRequest request) {
+        String authHeader = request.getHeader("Authorization");
+        String jwt = null;
+
+        if (authHeader != null && authHeader.startsWith("Bearer ")) {
+            jwt = authHeader.substring(7);
+        }
+
+        if (jwt != null) {
+            return jwtUtil.extractUsername(jwt);
+        }
+
+        throw new RuntimeException("인증 토큰이 유효하지 않습니다.");
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/AuthDto.java b/user-service/src/main/java/com/example/userservice/dto/AuthDto.java
index dfa8fc8..54cac85 100644
--- a/user-service/src/main/java/com/example/userservice/dto/AuthDto.java
+++ b/user-service/src/main/java/com/example/userservice/dto/AuthDto.java
@@ -7,53 +7,69 @@
 
 public class AuthDto {
 
+    /**
+     * 로그인 요청 DTO
+     */
     @Data
     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     public static class LoginRequest {
         private String user_id;  // 로그인 시 사용자가 입력하는 ID
-        private String password;
+        private String password; // 비밀번호
     }
 
+    /**
+     * 로그인 응답 DTO
+     */
     @Data
     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     public static class LoginResponse {
-        private String accessToken;
-        private String refreshToken;
-        private String member_id;  // 내부 관리용 ID
-        private String user_id;    // 로그인용 ID
-        private String nickname;
+        private String accessToken;   // 액세스 토큰
+        private String refreshToken;  // 리프레시 토큰
+        private String member_id;     // 내부 관리용 ID
+        private String user_id;       // 로그인용 ID
+        private String nickname;      // 닉네임
+        private String role;          // 사용자 역할 (ROLE_USER or ROLE_ADMIN)
     }
 
+    /**
+     * 로그아웃 요청 DTO
+     */
     @Data
     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     public static class LogoutRequest {
-        private String memberId;
+        private String memberId;      // 회원 ID
 
         public String getMemberId() {
             return memberId;
         }
     }
 
+    /**
+     * 로그아웃 응답 DTO
+     */
     @Data
     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     public static class LogoutResponse {
-        private boolean success;
-        private String message;
+        private boolean success;      // 성공 여부
+        private String message;       // 메시지
     }
 
+    /**
+     * 토큰 갱신 요청 DTO
+     */
     @Data
     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     public static class RefreshTokenRequest {
-        private String refreshToken;
+        private String refreshToken;  // 리프레시 토큰
     }
-}
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java
index 3648c46..8b6e619 100644
--- a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java
@@ -9,36 +9,61 @@
 
 public class MemberDto {
 
+    // 회원 정보 응답 DTO
     @Data
     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     public static class MemberResponse {
-        private String user_id;
-        private String nickname;
-        private Short birth_year;
-        private LocalDateTime created_at;
+        private String user_id;       // 로그인 ID
+        private String nickname;      // 닉네임
+        private Short birth_year;     // 출생연도
+        private String mbti;          // MBTI 유형
+        private String gender;        // 성별
+        private String role;          // 역할 (ROLE_USER, ROLE_ADMIN)
+        private LocalDateTime created_at; // 계정 생성일
     }
 
+    // 프로필 조회 응답 DTO
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class ProfileResponse {
+        private String nickname;      // 닉네임
+        private Short birth_year;     // 출생연도
+        private String mbti;          // MBTI 유형
+        private String gender;        // 성별
+        private String role;          // 역할 (ROLE_USER, ROLE_ADMIN)
+        private LocalDateTime created_at; // 계정 생성일
+    }
+
+    // 프로필 업데이트 요청 DTO
     @Data
     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     public static class UpdateRequest {
-        private String nickname;
-        private Short birth_year;
-        private String current_password;
-        private String new_password;
+        private String nickname;          // 변경할 닉네임
+        private Short birth_year;         // 변경할 출생연도
+        private String mbti;              // 변경할 MBTI
+        private String gender;            // 변경할 성별
+        private String current_password;  // 현재 비밀번호 (비밀번호 변경 시 필요)
+        private String new_password;      // 새 비밀번호 (비밀번호 변경 시 필요)
     }
 
+    // 회원 등록 요청 DTO
     @Data
     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     public static class RegisterRequest {
-        private String user_id;
-        private String password;
-        private String nickname;
-        private Short birth_year;
+        private String user_id;           // 로그인 ID
+        private String password;          // 비밀번호
+        private String nickname;          // 닉네임
+        private Short birth_year;         // 출생연도
+        private String mbti;              // MBTI 유형
+        private String gender;            // 성별
+        private String role;              // 역할 (옵션)
     }
 }
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
index 058318e..c9f4e94 100644
--- a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
@@ -7,13 +7,20 @@
 @Getter
 @Setter
 public class MemberSign {
-    @JsonProperty("user_id")  // JSON 필드 이름 매핑
-    private String userId;
+    @JsonProperty("user_id") // JSON 필드와 매핑
+    private String userId; // 로그인에 사용할 ID
 
-    private String password;
-    private String nickname;
-    private String createTime;
+    private String password; // 비밀번호
 
-    @JsonProperty("birth_year")  // JSON 필드 이름 매핑
-    private Short birthYear;
+    private String nickname; // 닉네임
+
+    @JsonProperty("birth_year") // JSON 필드와 매핑
+    private Short birthYear; // 출생연도
+
+    private String mbti; // MBTI 유형
+
+    private String gender; // 성별
+
+    // 관리자 회원가입용 옵션 필드
+    private String role; // 사용자 권한 (기본값 ROLE_USER, ROLE_ADMIN 가능)
 }
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/Member.java b/user-service/src/main/java/com/example/userservice/entity/Member.java
index 3815518..5df9b7c 100644
--- a/user-service/src/main/java/com/example/userservice/entity/Member.java
+++ b/user-service/src/main/java/com/example/userservice/entity/Member.java
@@ -19,21 +19,29 @@ public class Member {
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     @Column(name = "member_id", nullable = false)
-    private Long memberId;
+    private Long memberId; // 회원 고유 ID (시스템 내부용)
 
     @Column(name = "user_id", nullable = false, unique = true)
-    private String userId;
-
+    private String userId; // 사용자 로그인 ID
 
     @Column(nullable = false)
-    private String password;
+    private String password; // 비밀번호
 
     @Column
-    private String nickname;
+    private String nickname; // 닉네임
 
     @Column(name = "birth_year")
-    private Short birthYear;
+    private Short birthYear; // 출생연도
+
+    @Column(name = "mbti")
+    private String mbti; // MBTI 유형
+
+    @Column(name = "gender")
+    private String gender; // 성별
 
     @Column(name = "created_at")
-    private LocalDateTime createdAt;
-}
+    private LocalDateTime createdAt; // 계정 생성일
+
+    @Column(name = "role", nullable = false)
+    private String role; // 사용자 권한 ("ROLE_ADMIN" 또는 "ROLE_USER")
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
index 6612a36..fe4a75d 100644
--- a/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
+++ b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
@@ -14,6 +14,6 @@
 @RedisHash(value = "refresh_token", timeToLive = 60 * 60 * 24 * 7) // 7일 저장
 public class RefreshToken {
     @Id
-    private String id;   // memberId를 String으로 저장
-    private String refreshToken;
-}
+    private String id;   // memberId를 String으로 저장 (Redis 키)
+    private String refreshToken; // 리프레시 토큰 값
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java b/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java
index f871368..a680309 100644
--- a/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java
+++ b/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java
@@ -1,13 +1,12 @@
 package com.example.userservice.repository.redis;
 
 import com.example.userservice.entity.RefreshToken;
-import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.repository.CrudRepository;
 import org.springframework.stereotype.Repository;
 
 import java.util.Optional;
 
 @Repository
-public interface RefreshTokenRepository extends JpaRepository {
-    Optional findByUserId(Long userId);
+public interface RefreshTokenRepository extends CrudRepository {
     Optional findByRefreshToken(String refreshToken);
 }
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
index b917cea..7f40fe9 100644
--- a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
+++ b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
@@ -26,7 +26,12 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
     private final UserDetailsService userDetailsService;
 
     // 필터링하지 않을 URL 패턴 목록
-    private final List excludedPaths = Arrays.asList("/auth/signup", "/auth/login", "/auth/logout");
+    private final List excludedPaths = Arrays.asList(
+            "/auth/signup",
+            "/auth/admin/signup",
+            "/auth/login",
+            "/auth/logout"
+    );
 
     @Override
     protected void doFilterInternal(
@@ -36,7 +41,6 @@ protected void doFilterInternal(
     ) throws ServletException, IOException {
 
         // 인증이 필요없는 경로는 필터를 적용하지 않음
-        String requestPath = request.getServletPath();
         if (shouldNotFilter(request)) {
             filterChain.doFilter(request, response);
             return;
@@ -48,7 +52,7 @@ protected void doFilterInternal(
 
         // Authorization 헤더가 있으면 해당 토큰 사용
         if (authHeader != null && authHeader.startsWith("Bearer ")) {
-            jwt = authHeader.substring(7);
+            jwt = authHeader.substring(7); // "Bearer " 이후의 문자열 추출
         }
         // 아니면 쿠키에서 토큰 찾기
         else {
@@ -70,21 +74,26 @@ protected void doFilterInternal(
         }
 
         try {
+            // 토큰에서 사용자 이름(userId) 추출
             final String username = jwtUtil.extractUsername(jwt);
 
+            // 보안 컨텍스트에 인증 정보가 없으면 새로 설정
             if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                 UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
 
+                // 토큰이 유효한지 확인
                 if (jwtUtil.validateToken(jwt, userDetails)) {
                     UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                             userDetails,
                             null,
-                            userDetails.getAuthorities()
+                            userDetails.getAuthorities() // 사용자 권한 포함
                     );
                     authToken.setDetails(
                             new WebAuthenticationDetailsSource().buildDetails(request)
                     );
                     SecurityContextHolder.getContext().setAuthentication(authToken);
+
+                    logger.info("사용자 인증 성공: " + username + " (권한: " + userDetails.getAuthorities() + ")");
                 }
             }
         } catch (Exception e) {
diff --git a/user-service/src/main/java/com/example/userservice/service/AuthService.java b/user-service/src/main/java/com/example/userservice/service/AuthService.java
index 184877f..347ef7d 100644
--- a/user-service/src/main/java/com/example/userservice/service/AuthService.java
+++ b/user-service/src/main/java/com/example/userservice/service/AuthService.java
@@ -7,11 +7,13 @@
 import lombok.RequiredArgsConstructor;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.stereotype.Service;
 
 import java.util.Collections;
+import java.util.List;
 
 @Service
 @RequiredArgsConstructor
@@ -22,7 +24,11 @@ public class AuthService {
     private final AuthenticationManager authenticationManager;
     private final TokenService tokenService;
 
-    // 로그인
+    /**
+     * 로그인 처리
+     * @param request 로그인 요청 정보 (user_id, password)
+     * @return 토큰과 사용자 정보가 포함된 응답
+     */
     public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
         System.out.println("로그인 시도: " + request.getUser_id());
 
@@ -30,7 +36,7 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
         Member member = memberRepository.findByUserId(request.getUser_id())
                 .orElseThrow(() -> {
                     System.out.println("사용자를 찾을 수 없음: " + request.getUser_id());
-                    return new RuntimeException("User not found");
+                    return new RuntimeException("사용자를 찾을 수 없습니다");
                 });
 
         System.out.println("사용자 찾음: ID=" + member.getMemberId() + ", UserID=" + member.getUserId());
@@ -49,8 +55,13 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
             throw e;
         }
 
+        // 사용자 권한 설정
+        List authorities = Collections.singletonList(
+                new SimpleGrantedAuthority(member.getRole())
+        );
+
         // 3. JWT 토큰 발급
-        UserDetails userDetails = new User(member.getUserId(), member.getPassword(), Collections.emptyList());
+        UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities);
         String accessToken = jwtUtil.generateToken(userDetails);
         String refreshToken = jwtUtil.generateRefreshToken(userDetails);
 
@@ -64,10 +75,15 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
                 .member_id(String.valueOf(member.getMemberId()))
                 .user_id(member.getUserId())
                 .nickname(member.getNickname())
+                .role(member.getRole())
                 .build();
     }
 
-    // 로그아웃
+    /**
+     * 로그아웃 처리
+     * @param request 로그아웃 요청 정보 (memberId)
+     * @return 로그아웃 성공 여부와 메시지
+     */
     public AuthDto.LogoutResponse logout(AuthDto.LogoutRequest request) {
         // 1. 사용자의 Redis Refresh Token 삭제
         Long memberId = Long.parseLong(request.getMemberId());
@@ -79,4 +95,4 @@ public AuthDto.LogoutResponse logout(AuthDto.LogoutRequest request) {
                 .message("로그아웃 완료: RefreshToken 삭제 완료")
                 .build();
     }
-}
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java
index cba58f6..955c347 100644
--- a/user-service/src/main/java/com/example/userservice/service/MemberService.java
+++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java
@@ -1,8 +1,10 @@
 package com.example.userservice.service;
 
+import com.example.userservice.dto.MemberDto;
 import com.example.userservice.dto.MemberSign;
 import com.example.userservice.entity.Member;
 import com.example.userservice.repository.jpa.MemberRepository;
+import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
@@ -15,24 +17,110 @@ public class MemberService {
     private final MemberRepository memberRepository;
     private final PasswordEncoder passwordEncoder;
 
+    // 일반 사용자 회원가입 (기본 권한은 ROLE_USER)
+    public Member registerUser(MemberSign dto) {
+        return registerMember(dto, "ROLE_USER");
+    }
+
+    // 관리자 회원가입
     public Member registerAdmin(MemberSign dto) {
+        return registerMember(dto, "ROLE_ADMIN");
+    }
+
+    // 공통 회원가입 로직
+    private Member registerMember(MemberSign dto, String defaultRole) {
         // userId 중복 체크
         if (memberRepository.existsByUserId(dto.getUserId())) {
             throw new IllegalStateException("이미 존재하는 사용자 ID입니다.");
         }
 
+        // 닉네임 중복 체크
         if (memberRepository.existsByNickname(dto.getNickname())) {
             throw new IllegalStateException("이미 존재하는 닉네임입니다.");
         }
 
+        // DTO에서 역할이 지정되었고 관리자로 등록하는 경우 해당 역할 사용, 아니면 기본 역할 사용
+        String role = dto.getRole() != null && defaultRole.equals("ROLE_ADMIN")
+                ? dto.getRole()
+                : defaultRole;
+
+        // 회원 엔티티 생성
         Member member = Member.builder()
-                .userId(dto.getUserId())  // userId 설정
-                .password(passwordEncoder.encode(dto.getPassword()))
+                .userId(dto.getUserId())
+                .password(passwordEncoder.encode(dto.getPassword())) // 비밀번호 암호화
                 .nickname(dto.getNickname())
                 .birthYear(dto.getBirthYear())
+                .mbti(dto.getMbti())
+                .gender(dto.getGender())
+                .role(role)
                 .createdAt(LocalDateTime.now())
                 .build();
 
+        // 저장 후 반환
         return memberRepository.save(member);
     }
+
+    // 회원 정보 조회
+    public MemberDto.ProfileResponse getProfile(String userId) {
+        Member member = memberRepository.findByUserId(userId)
+                .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
+
+        return MemberDto.ProfileResponse.builder()
+                .nickname(member.getNickname())
+                .birth_year(member.getBirthYear())
+                .mbti(member.getMbti())
+                .gender(member.getGender())
+                .role(member.getRole())
+                .created_at(member.getCreatedAt())
+                .build();
+    }
+
+    // 회원 정보 수정
+    @Transactional
+    public MemberDto.MemberResponse updateProfile(String userId, MemberDto.UpdateRequest request) {
+        Member member = memberRepository.findByUserId(userId)
+                .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
+
+        // 비밀번호 변경이 요청된 경우
+        if (request.getCurrent_password() != null && request.getNew_password() != null) {
+            // 현재 비밀번호 확인
+            if (!passwordEncoder.matches(request.getCurrent_password(), member.getPassword())) {
+                throw new RuntimeException("현재 비밀번호가 일치하지 않습니다.");
+            }
+
+            // 새 비밀번호로 업데이트 (암호화)
+            member.setPassword(passwordEncoder.encode(request.getNew_password()));
+        }
+
+        // 기타 정보 업데이트
+        if (request.getNickname() != null) {
+            member.setNickname(request.getNickname());
+        }
+
+        if (request.getBirth_year() != null) {
+            member.setBirthYear(request.getBirth_year());
+        }
+
+        if (request.getMbti() != null) {
+            member.setMbti(request.getMbti());
+        }
+
+        if (request.getGender() != null) {
+            member.setGender(request.getGender());
+        }
+
+        // 저장
+        Member updatedMember = memberRepository.save(member);
+
+        // 응답 DTO로 변환
+        return MemberDto.MemberResponse.builder()
+                .user_id(updatedMember.getUserId())
+                .nickname(updatedMember.getNickname())
+                .birth_year(updatedMember.getBirthYear())
+                .mbti(updatedMember.getMbti())
+                .gender(updatedMember.getGender())
+                .role(updatedMember.getRole())
+                .created_at(updatedMember.getCreatedAt())
+                .build();
+    }
 }
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/TokenService.java b/user-service/src/main/java/com/example/userservice/service/TokenService.java
index dd8ca0c..3d6070f 100644
--- a/user-service/src/main/java/com/example/userservice/service/TokenService.java
+++ b/user-service/src/main/java/com/example/userservice/service/TokenService.java
@@ -6,6 +6,7 @@
 import com.example.userservice.repository.redis.RefreshTokenRepository;
 import com.example.userservice.security.JwtUtil;
 import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.stereotype.Service;
@@ -21,6 +22,11 @@ public class TokenService {
     private final MemberRepository memberRepository;
     private final RefreshTokenRepository refreshTokenRepository;
 
+    /**
+     * 리프레시 토큰을 사용하여 새 액세스 토큰 생성
+     * @param refreshToken 리프레시 토큰
+     * @return 새 액세스 토큰
+     */
     public String createNewAccessToken(String refreshToken) {
         // 리프레시 토큰 유효성 검사
         if (!jwtUtil.validateToken(refreshToken)) {
@@ -38,11 +44,24 @@ public String createNewAccessToken(String refreshToken) {
         Member member = memberRepository.findById(userId)
                 .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
 
+        // 사용자 권한 설정
+        SimpleGrantedAuthority authority = new SimpleGrantedAuthority(member.getRole());
+
         // 새 액세스 토큰 생성
-        UserDetails userDetails = new User(member.getUserId(), member.getPassword(), Collections.emptyList());
+        UserDetails userDetails = new User(
+                member.getUserId(),
+                member.getPassword(),
+                Collections.singletonList(authority)
+        );
+
         return jwtUtil.generateToken(userDetails);
     }
 
+    /**
+     * 리프레시 토큰 저장
+     * @param memberId 회원 ID
+     * @param refreshToken 리프레시 토큰
+     */
     public void saveRefreshToken(Long memberId, String refreshToken) {
         RefreshToken token = RefreshToken.builder()
                 .id(String.valueOf(memberId))   // memberId를 String으로 변환해서 저장
@@ -52,7 +71,12 @@ public void saveRefreshToken(Long memberId, String refreshToken) {
         refreshTokenRepository.save(token);
     }
 
+    /**
+     * 리프레시 토큰 삭제
+     * @param memberId 회원 ID
+     */
     public void deleteRefreshToken(Long memberId) {
-        refreshTokenRepository.deleteById(Long.valueOf(String.valueOf(memberId)));  // 여기서도 String 변환 필요!
+        // memberId를 String으로 변환하여 deleteById 호출
+        refreshTokenRepository.deleteById(String.valueOf(memberId));
     }
-}
+}
\ No newline at end of file

From 87295d2e39efedf6ab804298b871fe3eacf59ab1 Mon Sep 17 00:00:00 2001
From: YunHwa0905 
Date: Tue, 29 Apr 2025 10:46:12 +0900
Subject: [PATCH 173/361] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?=
 =?UTF-8?q?=EC=9E=85=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=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

---
 user-service/.gitignore                       |   1 -
 .../userservice/config/ApplicationConfig.java |  25 +++--
 .../userservice/config/SecurityConfig.java    |  22 ++--
 .../userservice/controller/AdminSignup.java   | 104 +++++++++++++++---
 .../controller/AuthController.java            |  61 +++++++++-
 .../controller/MemberController.java          |  73 ++++++++++++
 .../com/example/userservice/dto/AuthDto.java  |  38 +++++--
 .../example/userservice/dto/MemberDto.java    |  49 +++++++--
 .../example/userservice/dto/MemberSign.java   |  21 ++--
 .../example/userservice/entity/Member.java    |  24 ++--
 .../userservice/entity/RefreshToken.java      |   6 +-
 .../redis/RefreshTokenRepository.java         |   5 +-
 .../security/JwtAuthenticationFilter.java     |  17 ++-
 .../userservice/service/AuthService.java      |  26 ++++-
 .../userservice/service/MemberService.java    |  92 +++++++++++++++-
 .../userservice/service/TokenService.java     |  30 ++++-
 16 files changed, 501 insertions(+), 93 deletions(-)
 create mode 100644 user-service/src/main/java/com/example/userservice/controller/MemberController.java

diff --git a/user-service/.gitignore b/user-service/.gitignore
index 1f22f61..261a098 100644
--- a/user-service/.gitignore
+++ b/user-service/.gitignore
@@ -37,5 +37,4 @@ out/
 ### VS Code ###
 .vscode/
 
-.env
 
diff --git a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
index 5e16036..44f2998 100644
--- a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
+++ b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java
@@ -8,11 +8,16 @@
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
 import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
 @Configuration
 @RequiredArgsConstructor
 public class ApplicationConfig {
@@ -22,12 +27,18 @@ public class ApplicationConfig {
     @Bean
     public UserDetailsService userDetailsService() {
         return username -> memberRepository.findByUserId(username)
-                .map(member -> new org.springframework.security.core.userdetails.User(
-                        member.getUserId(),  // userId를 사용하도록 변경
-                        member.getPassword(),
-                        java.util.Collections.emptyList()
-                ))
-                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
+                .map(member -> {
+                    List authorities = new ArrayList<>();
+                    // 사용자 역할에 따른 권한 추가
+                    authorities.add(new SimpleGrantedAuthority(member.getRole()));
+
+                    return new org.springframework.security.core.userdetails.User(
+                            member.getUserId(), // userId를 사용자 이름으로 사용
+                            member.getPassword(), // 암호화된 비밀번호
+                            authorities // 권한 목록
+                    );
+                })
+                .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다"));
     }
 
     @Bean
@@ -45,6 +56,6 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration c
 
     @Bean
     public PasswordEncoder passwordEncoder() {
-        return new BCryptPasswordEncoder();
+        return new BCryptPasswordEncoder(); // 비밀번호 암호화에 BCrypt 알고리즘 사용
     }
 }
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
index 0868b9b..9faf817 100644
--- a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
+++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java
@@ -4,6 +4,7 @@
 import lombok.RequiredArgsConstructor;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
 import org.springframework.security.authentication.AuthenticationProvider;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -22,13 +23,22 @@ public class SecurityConfig {
     @Bean
     public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
         http
-                .csrf(csrf -> csrf.disable())
+                .csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화
                 .authorizeHttpRequests(auth -> auth
+                        // 공개 엔드포인트 - 인증 불필요
                         .requestMatchers("/auth/**").permitAll()
-                        .requestMatchers("/admin/**").authenticated()
+
+                        // 관리자 전용 엔드포인트
+                        .requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
+
+                        // 사용자/관리자 접근 가능 엔드포인트
+                        .requestMatchers("/mypage/**").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
+
+                        // 그 외 모든 요청은 인증 필요
                         .anyRequest().authenticated()
                 )
                 .sessionManagement(session -> session
+                        // 세션 상태 없음 (JWT 사용하므로)
                         .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                 )
                 .authenticationProvider(authenticationProvider)
@@ -36,10 +46,4 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
 
         return http.build();
     }
-
-    //비밀번호 암호화 적용
-//    @Bean
-//    public BCryptPasswordEncoder passwordEncoder() {
-//        return new BCryptPasswordEncoder();
-//    }
-}
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
index 073cc06..e069e82 100644
--- a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
+++ b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java
@@ -4,51 +4,125 @@
 import com.example.userservice.entity.Member;
 import com.example.userservice.security.JwtUtil;
 import com.example.userservice.service.MemberService;
+import com.example.userservice.service.TokenService;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetails;
-import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
-@Controller
+@RestController
 @RequiredArgsConstructor
 @RequestMapping("/auth")
 public class AdminSignup {
     private final MemberService memberService;
     private final JwtUtil jwtUtil;
+    private final TokenService tokenService;
 
+    /**
+     * 일반 사용자 회원가입 API
+     * @param dto 회원가입 정보 (user_id, password, nickname, birth_year, mbti, gender)
+     * @return 토큰 및 사용자 정보가 포함된 응답
+     */
     @PostMapping("/signup")
     public ResponseEntity> signup(@RequestBody MemberSign dto) {
         try {
             // 디버깅 로그 출력
-            System.out.println("회원가입 요청: userId=" + dto.getUserId() + ", nickname=" + dto.getNickname() + ", birth_year=" + dto.getBirthYear());
+            System.out.println("회원가입 요청: userId=" + dto.getUserId() + ", nickname=" + dto.getNickname()
+                    + ", birth_year=" + dto.getBirthYear()
+                    + ", mbti=" + dto.getMbti()
+                    + ", gender=" + dto.getGender());
 
-            Member member = memberService.registerAdmin(dto);
+            // 일반 사용자로 등록
+            Member member = memberService.registerUser(dto);
+
+            System.out.println("회원가입 성공: memberId=" + member.getMemberId()
+                    + ", userId=" + member.getUserId()
+                    + ", nickname=" + member.getNickname()
+                    + ", role=" + member.getRole());
 
-            System.out.println("회원가입 성공: memberId=" + member.getMemberId() + ", userId=" + member.getUserId() + ", nickname=" + member.getNickname());
+            // 사용자 권한 설정
+            List authorities = Collections.singletonList(
+                    new SimpleGrantedAuthority(member.getRole())
+            );
 
-            // 회원가입 성공하면 토큰 발급 - userId 사용
-            UserDetails userDetails = new User(member.getUserId(), member.getPassword(), Collections.emptyList());
+            // 회원가입 성공하면 토큰 발급
+            UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities);
             String accessToken = jwtUtil.generateToken(userDetails);
             String refreshToken = jwtUtil.generateRefreshToken(userDetails);
 
-            Map tokens = new HashMap<>();
-            tokens.put("accessToken", accessToken);
-            tokens.put("refreshToken", refreshToken);
-            tokens.put("member_id", String.valueOf(member.getMemberId()));
-            tokens.put("user_id", member.getUserId());
-            tokens.put("nickname", member.getNickname());
+            // 리프레시 토큰 저장
+            tokenService.saveRefreshToken(member.getMemberId(), refreshToken);
+
+            // 응답 생성
+            Map response = new HashMap<>();
+            response.put("accessToken", accessToken);
+            response.put("refreshToken", refreshToken);
+            response.put("member_id", String.valueOf(member.getMemberId()));
+            response.put("user_id", member.getUserId());
+            response.put("nickname", member.getNickname());
+            response.put("role", member.getRole());
 
-            return ResponseEntity.ok(tokens);
-        } catch (IllegalArgumentException e) {
+            return ResponseEntity.ok(response);
+        } catch (IllegalStateException e) {
             Map errorResponse = new HashMap<>();
             errorResponse.put("error", "회원가입 실패: " + e.getMessage());
             return ResponseEntity.badRequest().body(errorResponse);
         }
     }
+
+    /**
+     * 관리자 회원가입 API
+     * @param dto 관리자 회원가입 정보
+     * @return 토큰 및 관리자 정보가 포함된 응답
+     */
+    @PostMapping("/admin/signup")
+    public ResponseEntity> adminSignup(@RequestBody MemberSign dto) {
+        try {
+            // 디버깅 로그 출력
+            System.out.println("관리자 회원가입 요청: userId=" + dto.getUserId() + ", nickname=" + dto.getNickname());
+
+            // 관리자로 등록
+            Member member = memberService.registerAdmin(dto);
+
+            System.out.println("관리자 회원가입 성공: memberId=" + member.getMemberId()
+                    + ", userId=" + member.getUserId()
+                    + ", nickname=" + member.getNickname()
+                    + ", role=" + member.getRole());
+
+            // 관리자 권한 설정
+            List authorities = Collections.singletonList(
+                    new SimpleGrantedAuthority(member.getRole())
+            );
+
+            // 회원가입 성공하면 토큰 발급
+            UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities);
+            String accessToken = jwtUtil.generateToken(userDetails);
+            String refreshToken = jwtUtil.generateRefreshToken(userDetails);
+
+            // 리프레시 토큰 저장
+            tokenService.saveRefreshToken(member.getMemberId(), refreshToken);
+
+            // 응답 생성
+            Map response = new HashMap<>();
+            response.put("accessToken", accessToken);
+            response.put("refreshToken", refreshToken);
+            response.put("member_id", String.valueOf(member.getMemberId()));
+            response.put("user_id", member.getUserId());
+            response.put("nickname", member.getNickname());
+            response.put("role", member.getRole());
+
+            return ResponseEntity.ok(response);
+        } catch (IllegalStateException e) {
+            Map errorResponse = new HashMap<>();
+            errorResponse.put("error", "관리자 회원가입 실패: " + e.getMessage());
+            return ResponseEntity.badRequest().body(errorResponse);
+        }
+    }
 }
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/controller/AuthController.java b/user-service/src/main/java/com/example/userservice/controller/AuthController.java
index 9defdce..cc38cbf 100644
--- a/user-service/src/main/java/com/example/userservice/controller/AuthController.java
+++ b/user-service/src/main/java/com/example/userservice/controller/AuthController.java
@@ -1,8 +1,13 @@
 package com.example.userservice.controller;
 
 import com.example.userservice.dto.AuthDto;
+import com.example.userservice.entity.Member;
+import com.example.userservice.repository.jpa.MemberRepository;
+import com.example.userservice.security.JwtUtil;
 import com.example.userservice.service.AuthService;
 import com.example.userservice.service.CookieUtil;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.ResponseEntity;
@@ -18,7 +23,15 @@ public class AuthController {
 
     private final AuthService authService;
     private final CookieUtil cookieUtil;
+    private final JwtUtil jwtUtil;
+    private final MemberRepository memberRepository;
 
+    /**
+     * 로그인 API
+     * @param request 로그인 요청 (user_id, password)
+     * @param response HTTP 응답 (쿠키 설정용)
+     * @return 로그인 성공 시 토큰 및 사용자 정보
+     */
     @PostMapping("/login")
     public ResponseEntity login(
             @RequestBody AuthDto.LoginRequest request,
@@ -36,16 +49,58 @@ public ResponseEntity login(
         return ResponseEntity.ok(loginResponse);
     }
 
+    /**
+     * 로그아웃 API
+     * @param request HTTP 요청 (토큰 추출용)
+     * @param response HTTP 응답 (쿠키 삭제용)
+     * @return 로그아웃 성공 여부 및 메시지
+     */
     @PostMapping("/logout")
     public ResponseEntity logout(
-            @RequestBody AuthDto.LogoutRequest request,
+            HttpServletRequest request,
             HttpServletResponse response) {
 
         // 쿠키 삭제
         cookieUtil.addCookie(response, "access_token", "", 0);
         cookieUtil.addCookie(response, "refresh_token", "", 0);
 
-        // authService의 logout 메서드 호출
-        return ResponseEntity.ok(authService.logout(request));
+        // 헤더에서 토큰 추출
+        String authHeader = request.getHeader("Authorization");
+        String jwt = null;
+
+        if (authHeader != null && authHeader.startsWith("Bearer ")) {
+            jwt = authHeader.substring(7);
+        } else {
+            // 쿠키에서 토큰 찾기 (선택적)
+            Cookie[] cookies = request.getCookies();
+            if (cookies != null) {
+                for (Cookie cookie : cookies) {
+                    if ("access_token".equals(cookie.getName())) {
+                        jwt = cookie.getValue();
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (jwt != null) {
+            // 토큰에서 사용자 정보 추출
+            String userId = jwtUtil.extractUsername(jwt);
+            // 사용자 ID로 Member 조회
+            Member member = memberRepository.findByUserId(userId)
+                    .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다"));
+
+            // 로그아웃 처리
+            return ResponseEntity.ok(authService.logout(
+                    AuthDto.LogoutRequest.builder()
+                            .memberId(String.valueOf(member.getMemberId()))
+                            .build()));
+        }
+
+        return ResponseEntity.badRequest().body(
+                AuthDto.LogoutResponse.builder()
+                        .success(false)
+                        .message("유효한 토큰이 없습니다.")
+                        .build());
     }
 }
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/controller/MemberController.java b/user-service/src/main/java/com/example/userservice/controller/MemberController.java
new file mode 100644
index 0000000..d51b3e1
--- /dev/null
+++ b/user-service/src/main/java/com/example/userservice/controller/MemberController.java
@@ -0,0 +1,73 @@
+package com.example.userservice.controller;
+
+import com.example.userservice.dto.MemberDto;
+import com.example.userservice.security.JwtUtil;
+import com.example.userservice.service.MemberService;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequiredArgsConstructor
+public class MemberController {
+
+    private final MemberService memberService;
+    private final JwtUtil jwtUtil;
+
+    /**
+     * 사용자 프로필 조회 API
+     * @param request HTTP 요청 (토큰에서 사용자 ID 추출용)
+     * @return 사용자 프로필 정보
+     */
+    @GetMapping("/mypage/profile")
+    public ResponseEntity getProfile(HttpServletRequest request) {
+        // 현재 인증된 사용자의 userId 추출
+        String userId = extractUserIdFromRequest(request);
+
+        // 서비스를 통해 프로필 정보 조회
+        MemberDto.ProfileResponse profile = memberService.getProfile(userId);
+
+        return ResponseEntity.ok(profile);
+    }
+
+    /**
+     * 사용자 프로필 수정 API
+     * @param request HTTP 요청 (토큰에서 사용자 ID 추출용)
+     * @param updateRequest 프로필 수정 정보 (닉네임, 생년월일, MBTI, 성별, 비밀번호 등)
+     * @return 업데이트된 사용자 정보
+     */
+    @PostMapping("/mypage/profile/edit")
+    public ResponseEntity updateProfile(
+            HttpServletRequest request,
+            @RequestBody MemberDto.UpdateRequest updateRequest) {
+
+        // 현재 인증된 사용자의 userId 추출
+        String userId = extractUserIdFromRequest(request);
+
+        // 서비스를 통해 프로필 정보 업데이트
+        MemberDto.MemberResponse updatedProfile = memberService.updateProfile(userId, updateRequest);
+
+        return ResponseEntity.ok(updatedProfile);
+    }
+
+    /**
+     * JWT 토큰에서 userId 추출 헬퍼 메서드
+     * @param request HTTP 요청
+     * @return 추출된 사용자 ID
+     */
+    private String extractUserIdFromRequest(HttpServletRequest request) {
+        String authHeader = request.getHeader("Authorization");
+        String jwt = null;
+
+        if (authHeader != null && authHeader.startsWith("Bearer ")) {
+            jwt = authHeader.substring(7);
+        }
+
+        if (jwt != null) {
+            return jwtUtil.extractUsername(jwt);
+        }
+
+        throw new RuntimeException("인증 토큰이 유효하지 않습니다.");
+    }
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/AuthDto.java b/user-service/src/main/java/com/example/userservice/dto/AuthDto.java
index dfa8fc8..54cac85 100644
--- a/user-service/src/main/java/com/example/userservice/dto/AuthDto.java
+++ b/user-service/src/main/java/com/example/userservice/dto/AuthDto.java
@@ -7,53 +7,69 @@
 
 public class AuthDto {
 
+    /**
+     * 로그인 요청 DTO
+     */
     @Data
     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     public static class LoginRequest {
         private String user_id;  // 로그인 시 사용자가 입력하는 ID
-        private String password;
+        private String password; // 비밀번호
     }
 
+    /**
+     * 로그인 응답 DTO
+     */
     @Data
     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     public static class LoginResponse {
-        private String accessToken;
-        private String refreshToken;
-        private String member_id;  // 내부 관리용 ID
-        private String user_id;    // 로그인용 ID
-        private String nickname;
+        private String accessToken;   // 액세스 토큰
+        private String refreshToken;  // 리프레시 토큰
+        private String member_id;     // 내부 관리용 ID
+        private String user_id;       // 로그인용 ID
+        private String nickname;      // 닉네임
+        private String role;          // 사용자 역할 (ROLE_USER or ROLE_ADMIN)
     }
 
+    /**
+     * 로그아웃 요청 DTO
+     */
     @Data
     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     public static class LogoutRequest {
-        private String memberId;
+        private String memberId;      // 회원 ID
 
         public String getMemberId() {
             return memberId;
         }
     }
 
+    /**
+     * 로그아웃 응답 DTO
+     */
     @Data
     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     public static class LogoutResponse {
-        private boolean success;
-        private String message;
+        private boolean success;      // 성공 여부
+        private String message;       // 메시지
     }
 
+    /**
+     * 토큰 갱신 요청 DTO
+     */
     @Data
     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     public static class RefreshTokenRequest {
-        private String refreshToken;
+        private String refreshToken;  // 리프레시 토큰
     }
-}
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java
index 3648c46..8b6e619 100644
--- a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java
@@ -9,36 +9,61 @@
 
 public class MemberDto {
 
+    // 회원 정보 응답 DTO
     @Data
     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     public static class MemberResponse {
-        private String user_id;
-        private String nickname;
-        private Short birth_year;
-        private LocalDateTime created_at;
+        private String user_id;       // 로그인 ID
+        private String nickname;      // 닉네임
+        private Short birth_year;     // 출생연도
+        private String mbti;          // MBTI 유형
+        private String gender;        // 성별
+        private String role;          // 역할 (ROLE_USER, ROLE_ADMIN)
+        private LocalDateTime created_at; // 계정 생성일
     }
 
+    // 프로필 조회 응답 DTO
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class ProfileResponse {
+        private String nickname;      // 닉네임
+        private Short birth_year;     // 출생연도
+        private String mbti;          // MBTI 유형
+        private String gender;        // 성별
+        private String role;          // 역할 (ROLE_USER, ROLE_ADMIN)
+        private LocalDateTime created_at; // 계정 생성일
+    }
+
+    // 프로필 업데이트 요청 DTO
     @Data
     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     public static class UpdateRequest {
-        private String nickname;
-        private Short birth_year;
-        private String current_password;
-        private String new_password;
+        private String nickname;          // 변경할 닉네임
+        private Short birth_year;         // 변경할 출생연도
+        private String mbti;              // 변경할 MBTI
+        private String gender;            // 변경할 성별
+        private String current_password;  // 현재 비밀번호 (비밀번호 변경 시 필요)
+        private String new_password;      // 새 비밀번호 (비밀번호 변경 시 필요)
     }
 
+    // 회원 등록 요청 DTO
     @Data
     @Builder
     @AllArgsConstructor
     @NoArgsConstructor
     public static class RegisterRequest {
-        private String user_id;
-        private String password;
-        private String nickname;
-        private Short birth_year;
+        private String user_id;           // 로그인 ID
+        private String password;          // 비밀번호
+        private String nickname;          // 닉네임
+        private Short birth_year;         // 출생연도
+        private String mbti;              // MBTI 유형
+        private String gender;            // 성별
+        private String role;              // 역할 (옵션)
     }
 }
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
index 058318e..c9f4e94 100644
--- a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
+++ b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java
@@ -7,13 +7,20 @@
 @Getter
 @Setter
 public class MemberSign {
-    @JsonProperty("user_id")  // JSON 필드 이름 매핑
-    private String userId;
+    @JsonProperty("user_id") // JSON 필드와 매핑
+    private String userId; // 로그인에 사용할 ID
 
-    private String password;
-    private String nickname;
-    private String createTime;
+    private String password; // 비밀번호
 
-    @JsonProperty("birth_year")  // JSON 필드 이름 매핑
-    private Short birthYear;
+    private String nickname; // 닉네임
+
+    @JsonProperty("birth_year") // JSON 필드와 매핑
+    private Short birthYear; // 출생연도
+
+    private String mbti; // MBTI 유형
+
+    private String gender; // 성별
+
+    // 관리자 회원가입용 옵션 필드
+    private String role; // 사용자 권한 (기본값 ROLE_USER, ROLE_ADMIN 가능)
 }
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/Member.java b/user-service/src/main/java/com/example/userservice/entity/Member.java
index 3815518..5df9b7c 100644
--- a/user-service/src/main/java/com/example/userservice/entity/Member.java
+++ b/user-service/src/main/java/com/example/userservice/entity/Member.java
@@ -19,21 +19,29 @@ public class Member {
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     @Column(name = "member_id", nullable = false)
-    private Long memberId;
+    private Long memberId; // 회원 고유 ID (시스템 내부용)
 
     @Column(name = "user_id", nullable = false, unique = true)
-    private String userId;
-
+    private String userId; // 사용자 로그인 ID
 
     @Column(nullable = false)
-    private String password;
+    private String password; // 비밀번호
 
     @Column
-    private String nickname;
+    private String nickname; // 닉네임
 
     @Column(name = "birth_year")
-    private Short birthYear;
+    private Short birthYear; // 출생연도
+
+    @Column(name = "mbti")
+    private String mbti; // MBTI 유형
+
+    @Column(name = "gender")
+    private String gender; // 성별
 
     @Column(name = "created_at")
-    private LocalDateTime createdAt;
-}
+    private LocalDateTime createdAt; // 계정 생성일
+
+    @Column(name = "role", nullable = false)
+    private String role; // 사용자 권한 ("ROLE_ADMIN" 또는 "ROLE_USER")
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
index 6612a36..fe4a75d 100644
--- a/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
+++ b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java
@@ -14,6 +14,6 @@
 @RedisHash(value = "refresh_token", timeToLive = 60 * 60 * 24 * 7) // 7일 저장
 public class RefreshToken {
     @Id
-    private String id;   // memberId를 String으로 저장
-    private String refreshToken;
-}
+    private String id;   // memberId를 String으로 저장 (Redis 키)
+    private String refreshToken; // 리프레시 토큰 값
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java b/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java
index f871368..a680309 100644
--- a/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java
+++ b/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java
@@ -1,13 +1,12 @@
 package com.example.userservice.repository.redis;
 
 import com.example.userservice.entity.RefreshToken;
-import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.repository.CrudRepository;
 import org.springframework.stereotype.Repository;
 
 import java.util.Optional;
 
 @Repository
-public interface RefreshTokenRepository extends JpaRepository {
-    Optional findByUserId(Long userId);
+public interface RefreshTokenRepository extends CrudRepository {
     Optional findByRefreshToken(String refreshToken);
 }
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
index b917cea..7f40fe9 100644
--- a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
+++ b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java
@@ -26,7 +26,12 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
     private final UserDetailsService userDetailsService;
 
     // 필터링하지 않을 URL 패턴 목록
-    private final List excludedPaths = Arrays.asList("/auth/signup", "/auth/login", "/auth/logout");
+    private final List excludedPaths = Arrays.asList(
+            "/auth/signup",
+            "/auth/admin/signup",
+            "/auth/login",
+            "/auth/logout"
+    );
 
     @Override
     protected void doFilterInternal(
@@ -36,7 +41,6 @@ protected void doFilterInternal(
     ) throws ServletException, IOException {
 
         // 인증이 필요없는 경로는 필터를 적용하지 않음
-        String requestPath = request.getServletPath();
         if (shouldNotFilter(request)) {
             filterChain.doFilter(request, response);
             return;
@@ -48,7 +52,7 @@ protected void doFilterInternal(
 
         // Authorization 헤더가 있으면 해당 토큰 사용
         if (authHeader != null && authHeader.startsWith("Bearer ")) {
-            jwt = authHeader.substring(7);
+            jwt = authHeader.substring(7); // "Bearer " 이후의 문자열 추출
         }
         // 아니면 쿠키에서 토큰 찾기
         else {
@@ -70,21 +74,26 @@ protected void doFilterInternal(
         }
 
         try {
+            // 토큰에서 사용자 이름(userId) 추출
             final String username = jwtUtil.extractUsername(jwt);
 
+            // 보안 컨텍스트에 인증 정보가 없으면 새로 설정
             if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                 UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
 
+                // 토큰이 유효한지 확인
                 if (jwtUtil.validateToken(jwt, userDetails)) {
                     UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                             userDetails,
                             null,
-                            userDetails.getAuthorities()
+                            userDetails.getAuthorities() // 사용자 권한 포함
                     );
                     authToken.setDetails(
                             new WebAuthenticationDetailsSource().buildDetails(request)
                     );
                     SecurityContextHolder.getContext().setAuthentication(authToken);
+
+                    logger.info("사용자 인증 성공: " + username + " (권한: " + userDetails.getAuthorities() + ")");
                 }
             }
         } catch (Exception e) {
diff --git a/user-service/src/main/java/com/example/userservice/service/AuthService.java b/user-service/src/main/java/com/example/userservice/service/AuthService.java
index 184877f..347ef7d 100644
--- a/user-service/src/main/java/com/example/userservice/service/AuthService.java
+++ b/user-service/src/main/java/com/example/userservice/service/AuthService.java
@@ -7,11 +7,13 @@
 import lombok.RequiredArgsConstructor;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.stereotype.Service;
 
 import java.util.Collections;
+import java.util.List;
 
 @Service
 @RequiredArgsConstructor
@@ -22,7 +24,11 @@ public class AuthService {
     private final AuthenticationManager authenticationManager;
     private final TokenService tokenService;
 
-    // 로그인
+    /**
+     * 로그인 처리
+     * @param request 로그인 요청 정보 (user_id, password)
+     * @return 토큰과 사용자 정보가 포함된 응답
+     */
     public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
         System.out.println("로그인 시도: " + request.getUser_id());
 
@@ -30,7 +36,7 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
         Member member = memberRepository.findByUserId(request.getUser_id())
                 .orElseThrow(() -> {
                     System.out.println("사용자를 찾을 수 없음: " + request.getUser_id());
-                    return new RuntimeException("User not found");
+                    return new RuntimeException("사용자를 찾을 수 없습니다");
                 });
 
         System.out.println("사용자 찾음: ID=" + member.getMemberId() + ", UserID=" + member.getUserId());
@@ -49,8 +55,13 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
             throw e;
         }
 
+        // 사용자 권한 설정
+        List authorities = Collections.singletonList(
+                new SimpleGrantedAuthority(member.getRole())
+        );
+
         // 3. JWT 토큰 발급
-        UserDetails userDetails = new User(member.getUserId(), member.getPassword(), Collections.emptyList());
+        UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities);
         String accessToken = jwtUtil.generateToken(userDetails);
         String refreshToken = jwtUtil.generateRefreshToken(userDetails);
 
@@ -64,10 +75,15 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
                 .member_id(String.valueOf(member.getMemberId()))
                 .user_id(member.getUserId())
                 .nickname(member.getNickname())
+                .role(member.getRole())
                 .build();
     }
 
-    // 로그아웃
+    /**
+     * 로그아웃 처리
+     * @param request 로그아웃 요청 정보 (memberId)
+     * @return 로그아웃 성공 여부와 메시지
+     */
     public AuthDto.LogoutResponse logout(AuthDto.LogoutRequest request) {
         // 1. 사용자의 Redis Refresh Token 삭제
         Long memberId = Long.parseLong(request.getMemberId());
@@ -79,4 +95,4 @@ public AuthDto.LogoutResponse logout(AuthDto.LogoutRequest request) {
                 .message("로그아웃 완료: RefreshToken 삭제 완료")
                 .build();
     }
-}
+}
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java
index cba58f6..955c347 100644
--- a/user-service/src/main/java/com/example/userservice/service/MemberService.java
+++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java
@@ -1,8 +1,10 @@
 package com.example.userservice.service;
 
+import com.example.userservice.dto.MemberDto;
 import com.example.userservice.dto.MemberSign;
 import com.example.userservice.entity.Member;
 import com.example.userservice.repository.jpa.MemberRepository;
+import jakarta.transaction.Transactional;
 import lombok.RequiredArgsConstructor;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
@@ -15,24 +17,110 @@ public class MemberService {
     private final MemberRepository memberRepository;
     private final PasswordEncoder passwordEncoder;
 
+    // 일반 사용자 회원가입 (기본 권한은 ROLE_USER)
+    public Member registerUser(MemberSign dto) {
+        return registerMember(dto, "ROLE_USER");
+    }
+
+    // 관리자 회원가입
     public Member registerAdmin(MemberSign dto) {
+        return registerMember(dto, "ROLE_ADMIN");
+    }
+
+    // 공통 회원가입 로직
+    private Member registerMember(MemberSign dto, String defaultRole) {
         // userId 중복 체크
         if (memberRepository.existsByUserId(dto.getUserId())) {
             throw new IllegalStateException("이미 존재하는 사용자 ID입니다.");
         }
 
+        // 닉네임 중복 체크
         if (memberRepository.existsByNickname(dto.getNickname())) {
             throw new IllegalStateException("이미 존재하는 닉네임입니다.");
         }
 
+        // DTO에서 역할이 지정되었고 관리자로 등록하는 경우 해당 역할 사용, 아니면 기본 역할 사용
+        String role = dto.getRole() != null && defaultRole.equals("ROLE_ADMIN")
+                ? dto.getRole()
+                : defaultRole;
+
+        // 회원 엔티티 생성
         Member member = Member.builder()
-                .userId(dto.getUserId())  // userId 설정
-                .password(passwordEncoder.encode(dto.getPassword()))
+                .userId(dto.getUserId())
+                .password(passwordEncoder.encode(dto.getPassword())) // 비밀번호 암호화
                 .nickname(dto.getNickname())
                 .birthYear(dto.getBirthYear())
+                .mbti(dto.getMbti())
+                .gender(dto.getGender())
+                .role(role)
                 .createdAt(LocalDateTime.now())
                 .build();
 
+        // 저장 후 반환
         return memberRepository.save(member);
     }
+
+    // 회원 정보 조회
+    public MemberDto.ProfileResponse getProfile(String userId) {
+        Member member = memberRepository.findByUserId(userId)
+                .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
+
+        return MemberDto.ProfileResponse.builder()
+                .nickname(member.getNickname())
+                .birth_year(member.getBirthYear())
+                .mbti(member.getMbti())
+                .gender(member.getGender())
+                .role(member.getRole())
+                .created_at(member.getCreatedAt())
+                .build();
+    }
+
+    // 회원 정보 수정
+    @Transactional
+    public MemberDto.MemberResponse updateProfile(String userId, MemberDto.UpdateRequest request) {
+        Member member = memberRepository.findByUserId(userId)
+                .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
+
+        // 비밀번호 변경이 요청된 경우
+        if (request.getCurrent_password() != null && request.getNew_password() != null) {
+            // 현재 비밀번호 확인
+            if (!passwordEncoder.matches(request.getCurrent_password(), member.getPassword())) {
+                throw new RuntimeException("현재 비밀번호가 일치하지 않습니다.");
+            }
+
+            // 새 비밀번호로 업데이트 (암호화)
+            member.setPassword(passwordEncoder.encode(request.getNew_password()));
+        }
+
+        // 기타 정보 업데이트
+        if (request.getNickname() != null) {
+            member.setNickname(request.getNickname());
+        }
+
+        if (request.getBirth_year() != null) {
+            member.setBirthYear(request.getBirth_year());
+        }
+
+        if (request.getMbti() != null) {
+            member.setMbti(request.getMbti());
+        }
+
+        if (request.getGender() != null) {
+            member.setGender(request.getGender());
+        }
+
+        // 저장
+        Member updatedMember = memberRepository.save(member);
+
+        // 응답 DTO로 변환
+        return MemberDto.MemberResponse.builder()
+                .user_id(updatedMember.getUserId())
+                .nickname(updatedMember.getNickname())
+                .birth_year(updatedMember.getBirthYear())
+                .mbti(updatedMember.getMbti())
+                .gender(updatedMember.getGender())
+                .role(updatedMember.getRole())
+                .created_at(updatedMember.getCreatedAt())
+                .build();
+    }
 }
\ No newline at end of file
diff --git a/user-service/src/main/java/com/example/userservice/service/TokenService.java b/user-service/src/main/java/com/example/userservice/service/TokenService.java
index dd8ca0c..3d6070f 100644
--- a/user-service/src/main/java/com/example/userservice/service/TokenService.java
+++ b/user-service/src/main/java/com/example/userservice/service/TokenService.java
@@ -6,6 +6,7 @@
 import com.example.userservice.repository.redis.RefreshTokenRepository;
 import com.example.userservice.security.JwtUtil;
 import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.stereotype.Service;
@@ -21,6 +22,11 @@ public class TokenService {
     private final MemberRepository memberRepository;
     private final RefreshTokenRepository refreshTokenRepository;
 
+    /**
+     * 리프레시 토큰을 사용하여 새 액세스 토큰 생성
+     * @param refreshToken 리프레시 토큰
+     * @return 새 액세스 토큰
+     */
     public String createNewAccessToken(String refreshToken) {
         // 리프레시 토큰 유효성 검사
         if (!jwtUtil.validateToken(refreshToken)) {
@@ -38,11 +44,24 @@ public String createNewAccessToken(String refreshToken) {
         Member member = memberRepository.findById(userId)
                 .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
 
+        // 사용자 권한 설정
+        SimpleGrantedAuthority authority = new SimpleGrantedAuthority(member.getRole());
+
         // 새 액세스 토큰 생성
-        UserDetails userDetails = new User(member.getUserId(), member.getPassword(), Collections.emptyList());
+        UserDetails userDetails = new User(
+                member.getUserId(),
+                member.getPassword(),
+                Collections.singletonList(authority)
+        );
+
         return jwtUtil.generateToken(userDetails);
     }
 
+    /**
+     * 리프레시 토큰 저장
+     * @param memberId 회원 ID
+     * @param refreshToken 리프레시 토큰
+     */
     public void saveRefreshToken(Long memberId, String refreshToken) {
         RefreshToken token = RefreshToken.builder()
                 .id(String.valueOf(memberId))   // memberId를 String으로 변환해서 저장
@@ -52,7 +71,12 @@ public void saveRefreshToken(Long memberId, String refreshToken) {
         refreshTokenRepository.save(token);
     }
 
+    /**
+     * 리프레시 토큰 삭제
+     * @param memberId 회원 ID
+     */
     public void deleteRefreshToken(Long memberId) {
-        refreshTokenRepository.deleteById(Long.valueOf(String.valueOf(memberId)));  // 여기서도 String 변환 필요!
+        // memberId를 String으로 변환하여 deleteById 호출
+        refreshTokenRepository.deleteById(String.valueOf(memberId));
     }
-}
+}
\ No newline at end of file

From 49853ad9c9554cadaeaf7fa56ef8ed40d692ab2d Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Tue, 29 Apr 2025 11:01:57 +0900
Subject: [PATCH 174/361] =?UTF-8?q?Gateway=20=EC=99=84=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 gateway/build.gradle                             |  9 ++++++++-
 .../src/main/resources/application.properties    | 16 ++++++++++++++++
 2 files changed, 24 insertions(+), 1 deletion(-)

diff --git a/gateway/build.gradle b/gateway/build.gradle
index 02f4e27..809df21 100644
--- a/gateway/build.gradle
+++ b/gateway/build.gradle
@@ -1,6 +1,6 @@
 plugins {
     id 'java'
-    id 'org.springframework.boot' version '3.4.5'
+    id 'org.springframework.boot' version '3.4.4'
     id 'io.spring.dependency-management' version '1.1.7'
 }
 
@@ -17,9 +17,16 @@ repositories {
     mavenCentral()
 }
 
+dependencyManagement {
+    imports {
+        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:2024.0.1'
+    }
+}
+
 dependencies {
     implementation 'org.springframework.boot:spring-boot-starter'
     implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
+    implementation 'org.springframework.boot:spring-boot-starter-webflux'
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
     testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
 }
diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties
index 6365994..3b4e197 100644
--- a/gateway/src/main/resources/application.properties
+++ b/gateway/src/main/resources/application.properties
@@ -1 +1,17 @@
 spring.application.name=gateway
+server.port=8080
+
+spring.cloud.gateway.routes[0].id=realtime-control-service
+spring.cloud.gateway.routes[0].uri=http://localhost:8081
+spring.cloud.gateway.routes[0].predicates[0]=Path=/control/**
+spring.cloud.gateway.routes[0].filters[0]=RewritePath=/control/(?.*), /$\{segment}
+
+spring.cloud.gateway.routes[1].id=place-service
+spring.cloud.gateway.routes[1].uri=http://localhost:8082
+spring.cloud.gateway.routes[1].predicates[0]=Path=/place/**
+spring.cloud.gateway.routes[1].filters[0]=RewritePath=/place/(?.*), /$\{segment}
+
+spring.cloud.gateway.routes[2].id=user-service
+spring.cloud.gateway.routes[2].uri=http://localhost:8083
+spring.cloud.gateway.routes[2].predicates[0]=Path=/user/**
+spring.cloud.gateway.routes[2].filters[0]=RewritePath=/user/(?.*), /$\{segment}
\ No newline at end of file

From cf6032fc00c433f8235fe49edd5d2d955f5e63b3 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Tue, 29 Apr 2025 11:01:57 +0900
Subject: [PATCH 175/361] =?UTF-8?q?Gateway=20=EC=99=84=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 gateway/build.gradle                             |  9 ++++++++-
 .../src/main/resources/application.properties    | 16 ++++++++++++++++
 2 files changed, 24 insertions(+), 1 deletion(-)

diff --git a/gateway/build.gradle b/gateway/build.gradle
index 02f4e27..809df21 100644
--- a/gateway/build.gradle
+++ b/gateway/build.gradle
@@ -1,6 +1,6 @@
 plugins {
     id 'java'
-    id 'org.springframework.boot' version '3.4.5'
+    id 'org.springframework.boot' version '3.4.4'
     id 'io.spring.dependency-management' version '1.1.7'
 }
 
@@ -17,9 +17,16 @@ repositories {
     mavenCentral()
 }
 
+dependencyManagement {
+    imports {
+        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:2024.0.1'
+    }
+}
+
 dependencies {
     implementation 'org.springframework.boot:spring-boot-starter'
     implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
+    implementation 'org.springframework.boot:spring-boot-starter-webflux'
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
     testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
 }
diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties
index 6365994..3b4e197 100644
--- a/gateway/src/main/resources/application.properties
+++ b/gateway/src/main/resources/application.properties
@@ -1 +1,17 @@
 spring.application.name=gateway
+server.port=8080
+
+spring.cloud.gateway.routes[0].id=realtime-control-service
+spring.cloud.gateway.routes[0].uri=http://localhost:8081
+spring.cloud.gateway.routes[0].predicates[0]=Path=/control/**
+spring.cloud.gateway.routes[0].filters[0]=RewritePath=/control/(?.*), /$\{segment}
+
+spring.cloud.gateway.routes[1].id=place-service
+spring.cloud.gateway.routes[1].uri=http://localhost:8082
+spring.cloud.gateway.routes[1].predicates[0]=Path=/place/**
+spring.cloud.gateway.routes[1].filters[0]=RewritePath=/place/(?.*), /$\{segment}
+
+spring.cloud.gateway.routes[2].id=user-service
+spring.cloud.gateway.routes[2].uri=http://localhost:8083
+spring.cloud.gateway.routes[2].predicates[0]=Path=/user/**
+spring.cloud.gateway.routes[2].filters[0]=RewritePath=/user/(?.*), /$\{segment}
\ No newline at end of file

From 6c464f564e20e45db15d0d2e07467ea317df6867 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Tue, 29 Apr 2025 11:02:51 +0900
Subject: [PATCH 176/361] =?UTF-8?q?application.properties=EB=93=A4=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 congestion-service/src/main/resources/application.properties | 1 +
 place-service/src/main/resources/application.properties      | 1 +
 2 files changed, 2 insertions(+)

diff --git a/congestion-service/src/main/resources/application.properties b/congestion-service/src/main/resources/application.properties
index 815d0c3..0719530 100644
--- a/congestion-service/src/main/resources/application.properties
+++ b/congestion-service/src/main/resources/application.properties
@@ -1,3 +1,4 @@
+server.port=8081
 spring.application.name=congestion-service
 
 # postgreSQL ??
diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties
index 27952cf..a85e46c 100644
--- a/place-service/src/main/resources/application.properties
+++ b/place-service/src/main/resources/application.properties
@@ -1,3 +1,4 @@
+server.port=8082
 spring.application.name=place-service
 
 spring.config.import=optional:file:.env[.properties]

From 9f2b8e834107b5e7fe9b0a126d306fc60757747d Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Tue, 29 Apr 2025 11:02:51 +0900
Subject: [PATCH 177/361] =?UTF-8?q?application.properties=EB=93=A4=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 congestion-service/src/main/resources/application.properties | 1 +
 place-service/src/main/resources/application.properties      | 1 +
 2 files changed, 2 insertions(+)

diff --git a/congestion-service/src/main/resources/application.properties b/congestion-service/src/main/resources/application.properties
index 815d0c3..0719530 100644
--- a/congestion-service/src/main/resources/application.properties
+++ b/congestion-service/src/main/resources/application.properties
@@ -1,3 +1,4 @@
+server.port=8081
 spring.application.name=congestion-service
 
 # postgreSQL ??
diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties
index 27952cf..a85e46c 100644
--- a/place-service/src/main/resources/application.properties
+++ b/place-service/src/main/resources/application.properties
@@ -1,3 +1,4 @@
+server.port=8082
 spring.application.name=place-service
 
 spring.config.import=optional:file:.env[.properties]

From 351a1282f409668b1cf69dc8178eb78391334a46 Mon Sep 17 00:00:00 2001
From: ByeongukYun 
Date: Tue, 29 Apr 2025 11:04:29 +0900
Subject: [PATCH 178/361] Update README.md

---
 README.md | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/README.md b/README.md
index de6e3f7..a0fb8c6 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,11 @@
 # Back-STARS
 SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다.
 
+Gateway : port 8080
+congestion : port 8081
+place : port 8082
+user : port 8083
+
 
로컬에서 postgreSQL 테스트 From 02be20b5db30d9b01d41c25722f6f97852019977 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 11:04:29 +0900 Subject: [PATCH 179/361] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index de6e3f7..a0fb8c6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # Back-STARS SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. +Gateway : port 8080 +congestion : port 8081 +place : port 8082 +user : port 8083 +
로컬에서 postgreSQL 테스트 From 76b397ca674b1b881e19085df08a55e3bb454448 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 11:06:28 +0900 Subject: [PATCH 180/361] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a0fb8c6..b8cef2c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Back-STARS SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. -Gateway : port 8080 -congestion : port 8081 -place : port 8082 -user : port 8083 +Gateway : port 8080
+congestion : port 8081
+place : port 8082
+user : port 8083
로컬에서 postgreSQL 테스트 From c7ff4821b760e3f94e68ce205db6a45a9e071754 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 11:06:28 +0900 Subject: [PATCH 181/361] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a0fb8c6..b8cef2c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Back-STARS SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. -Gateway : port 8080 -congestion : port 8081 -place : port 8082 -user : port 8083 +Gateway : port 8080
+congestion : port 8081
+place : port 8082
+user : port 8083
로컬에서 postgreSQL 테스트 From e043dd66d2d798343330b0f9b629f0d7827814e4 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 11:09:52 +0900 Subject: [PATCH 182/361] =?UTF-8?q?Readme.md=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b8cef2c..508be51 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # Back-STARS SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. -Gateway : port 8080
-congestion : port 8081
-place : port 8082
-user : port 8083 - +Gateway :    port 8080
+congestion : port 8081 - :8080/control/**
+place :      port 8082 - :8080/place/**
+user :       port 8083 - :8080/user/**
로컬에서 postgreSQL 테스트 From f049b87732db9a74b0e4503f3065f2dbb3a29a71 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 11:09:52 +0900 Subject: [PATCH 183/361] =?UTF-8?q?Readme.md=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b8cef2c..508be51 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # Back-STARS SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. -Gateway : port 8080
-congestion : port 8081
-place : port 8082
-user : port 8083 - +Gateway :    port 8080
+congestion : port 8081 - :8080/control/**
+place :      port 8082 - :8080/place/**
+user :       port 8083 - :8080/user/**
로컬에서 postgreSQL 테스트 From 4922b5b4873ca893a92702caf291da97bb020673 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 11:11:09 +0900 Subject: [PATCH 184/361] Update README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 508be51..94c86b3 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,10 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. Gateway :    port 8080
-congestion : port 8081 - :8080/control/**
-place :      port 8082 - :8080/place/**
-user :       port 8083 - :8080/user/**
+congestion : port 8081 - :8080/control/\*\*
+place :      port 8082 - :8080/place/\*\*
+user :       port 8083 - :8080/user/\*\* +
로컬에서 postgreSQL 테스트 From 37a7338b8c090ad24fdaeceb7f0ac09086ea9c15 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 11:11:09 +0900 Subject: [PATCH 185/361] Update README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 508be51..94c86b3 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,10 @@ SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. Gateway :    port 8080
-congestion : port 8081 - :8080/control/**
-place :      port 8082 - :8080/place/**
-user :       port 8083 - :8080/user/**
+congestion : port 8081 - :8080/control/\*\*
+place :      port 8082 - :8080/place/\*\*
+user :       port 8083 - :8080/user/\*\* +
로컬에서 postgreSQL 테스트 From ef868b4c513c642742da292934a704b29776cacb Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 11:13:14 +0900 Subject: [PATCH 186/361] =?UTF-8?q?README.md=20=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 94c86b3..72cf269 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Back-STARS SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. -Gateway :    port 8080
-congestion : port 8081 - :8080/control/\*\*
-place :      port 8082 - :8080/place/\*\*
-user :       port 8083 - :8080/user/\*\* +Gateway     : port 8080
+congestion : port 8081 - :8080/control/\*\*
+place          : port 8082 - :8080/place/\*\*
+user            : port 8083 - :8080/user/\*\*
로컬에서 postgreSQL 테스트 From 775361dbe6f367ebf177c2d60a0f4226a6c7920c Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 11:13:14 +0900 Subject: [PATCH 187/361] =?UTF-8?q?README.md=20=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 94c86b3..72cf269 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Back-STARS SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. -Gateway :    port 8080
-congestion : port 8081 - :8080/control/\*\*
-place :      port 8082 - :8080/place/\*\*
-user :       port 8083 - :8080/user/\*\* +Gateway     : port 8080
+congestion : port 8081 - :8080/control/\*\*
+place          : port 8082 - :8080/place/\*\*
+user            : port 8083 - :8080/user/\*\*
로컬에서 postgreSQL 테스트 From 168119458b7f3bda846b8601452f37dcbe1581c4 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Tue, 29 Apr 2025 13:39:53 +0900 Subject: [PATCH 188/361] =?UTF-8?q?Refactor:=20=ED=9A=8C=EC=9B=90=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/userservice/controller/AdminSignup.java | 12 ++++++++++++ .../userservice/repository/jpa/MemberRepository.java | 2 +- .../example/userservice/service/MemberService.java | 9 +++++++++ .../src/main/resources/application.properties | 2 +- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java index e069e82..2bcd045 100644 --- a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java +++ b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java @@ -77,6 +77,18 @@ public ResponseEntity> signup(@RequestBody MemberSign dto) { } } + //회원탈퇴 + @DeleteMapping("/signout/{userId}") + public ResponseEntity signout(@PathVariable String userId) { + try { + memberService.deleteMember(userId); + return ResponseEntity.ok("회원 탈퇴가 완료되었습니다."); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body("회원 탈퇴 실패: " + e.getMessage()); + } + } + + /** * 관리자 회원가입 API * @param dto 관리자 회원가입 정보 diff --git a/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java b/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java index cea05f5..69cb7dc 100644 --- a/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java +++ b/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java @@ -13,4 +13,4 @@ public interface MemberRepository extends JpaRepository { boolean existsByNickname(String nickname); Optional findByNickname(String nickname); Optional findByUserId(String userId); -} \ No newline at end of file +} diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java index 955c347..bf211c8 100644 --- a/user-service/src/main/java/com/example/userservice/service/MemberService.java +++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java @@ -123,4 +123,13 @@ public MemberDto.MemberResponse updateProfile(String userId, MemberDto.UpdateReq .created_at(updatedMember.getCreatedAt()) .build(); } + @Transactional + public void deleteMember(String userId) { + Member member = memberRepository.findByUserId(userId) + .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + + memberRepository.delete(member); + } + + } \ No newline at end of file diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties index d99579a..2757797 100644 --- a/user-service/src/main/resources/application.properties +++ b/user-service/src/main/resources/application.properties @@ -1,6 +1,6 @@ spring.application.name=user-service -server.port=8082 +server.port=8083 spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db spring.datasource.username=root From 994372bd6d8cbb6a51cf7923857659f93dd885f2 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Tue, 29 Apr 2025 13:39:53 +0900 Subject: [PATCH 189/361] =?UTF-8?q?Refactor:=20=ED=9A=8C=EC=9B=90=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/userservice/controller/AdminSignup.java | 12 ++++++++++++ .../userservice/repository/jpa/MemberRepository.java | 2 +- .../example/userservice/service/MemberService.java | 9 +++++++++ .../src/main/resources/application.properties | 2 +- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java index e069e82..2bcd045 100644 --- a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java +++ b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java @@ -77,6 +77,18 @@ public ResponseEntity> signup(@RequestBody MemberSign dto) { } } + //회원탈퇴 + @DeleteMapping("/signout/{userId}") + public ResponseEntity signout(@PathVariable String userId) { + try { + memberService.deleteMember(userId); + return ResponseEntity.ok("회원 탈퇴가 완료되었습니다."); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body("회원 탈퇴 실패: " + e.getMessage()); + } + } + + /** * 관리자 회원가입 API * @param dto 관리자 회원가입 정보 diff --git a/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java b/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java index cea05f5..69cb7dc 100644 --- a/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java +++ b/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java @@ -13,4 +13,4 @@ public interface MemberRepository extends JpaRepository { boolean existsByNickname(String nickname); Optional findByNickname(String nickname); Optional findByUserId(String userId); -} \ No newline at end of file +} diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java index 955c347..bf211c8 100644 --- a/user-service/src/main/java/com/example/userservice/service/MemberService.java +++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java @@ -123,4 +123,13 @@ public MemberDto.MemberResponse updateProfile(String userId, MemberDto.UpdateReq .created_at(updatedMember.getCreatedAt()) .build(); } + @Transactional + public void deleteMember(String userId) { + Member member = memberRepository.findByUserId(userId) + .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + + memberRepository.delete(member); + } + + } \ No newline at end of file diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties index d99579a..2757797 100644 --- a/user-service/src/main/resources/application.properties +++ b/user-service/src/main/resources/application.properties @@ -1,6 +1,6 @@ spring.application.name=user-service -server.port=8082 +server.port=8083 spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db spring.datasource.username=root From a23e5f85e7ff33b8d9303b2f13bf7d7e96cb10cd Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Tue, 29 Apr 2025 14:10:57 +0900 Subject: [PATCH 190/361] =?UTF-8?q?=EB=AA=A8=EB=93=A0=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminController.java | 32 +++++++++++++++++++ ...AdminSignup.java => SignupController.java} | 2 +- .../userservice/service/MemberService.java | 22 +++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 user-service/src/main/java/com/example/userservice/controller/AdminController.java rename user-service/src/main/java/com/example/userservice/controller/{AdminSignup.java => SignupController.java} (99%) diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminController.java b/user-service/src/main/java/com/example/userservice/controller/AdminController.java new file mode 100644 index 0000000..775cc81 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/controller/AdminController.java @@ -0,0 +1,32 @@ +package com.example.userservice.controller; + +import com.example.userservice.dto.MemberDto; +import com.example.userservice.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/admin") +public class AdminController { + + private final MemberService memberService; + + /** + * 전체 회원 목록 조회 API + * @return 모든 회원 목록 + */ + @GetMapping("/user/list") + @PreAuthorize("hasRole('ADMIN')") // ADMIN 권한이 있는 사용자만 접근 가능 + public ResponseEntity> getAllMembers() { + List members = memberService.getAllMembers(); + return ResponseEntity.ok(members); + } + +} \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java b/user-service/src/main/java/com/example/userservice/controller/SignupController.java similarity index 99% rename from user-service/src/main/java/com/example/userservice/controller/AdminSignup.java rename to user-service/src/main/java/com/example/userservice/controller/SignupController.java index 2bcd045..823474d 100644 --- a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java +++ b/user-service/src/main/java/com/example/userservice/controller/SignupController.java @@ -20,7 +20,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/auth") -public class AdminSignup { +public class SignupController { private final MemberService memberService; private final JwtUtil jwtUtil; private final TokenService tokenService; diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java index bf211c8..41b4126 100644 --- a/user-service/src/main/java/com/example/userservice/service/MemberService.java +++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java @@ -10,6 +10,8 @@ import org.springframework.stereotype.Service; import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -132,4 +134,24 @@ public void deleteMember(String userId) { } + + /** + * 모든 회원 목록 조회 + * @return 모든 회원 정보 목록 + */ + public List getAllMembers() { + List members = memberRepository.findAll(); + + return members.stream() + .map(member -> MemberDto.MemberResponse.builder() + .user_id(member.getUserId()) + .nickname(member.getNickname()) + .birth_year(member.getBirthYear()) + .mbti(member.getMbti()) + .gender(member.getGender()) + .role(member.getRole()) + .created_at(member.getCreatedAt()) + .build()) + .collect(Collectors.toList()); + } } \ No newline at end of file From 57f23fe26fff0384c96f5fe73eaa85d1a3c5434d Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Tue, 29 Apr 2025 14:10:57 +0900 Subject: [PATCH 191/361] =?UTF-8?q?=EB=AA=A8=EB=93=A0=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminController.java | 32 +++++++++++++++++++ ...AdminSignup.java => SignupController.java} | 2 +- .../userservice/service/MemberService.java | 22 +++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 user-service/src/main/java/com/example/userservice/controller/AdminController.java rename user-service/src/main/java/com/example/userservice/controller/{AdminSignup.java => SignupController.java} (99%) diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminController.java b/user-service/src/main/java/com/example/userservice/controller/AdminController.java new file mode 100644 index 0000000..775cc81 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/controller/AdminController.java @@ -0,0 +1,32 @@ +package com.example.userservice.controller; + +import com.example.userservice.dto.MemberDto; +import com.example.userservice.service.MemberService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/admin") +public class AdminController { + + private final MemberService memberService; + + /** + * 전체 회원 목록 조회 API + * @return 모든 회원 목록 + */ + @GetMapping("/user/list") + @PreAuthorize("hasRole('ADMIN')") // ADMIN 권한이 있는 사용자만 접근 가능 + public ResponseEntity> getAllMembers() { + List members = memberService.getAllMembers(); + return ResponseEntity.ok(members); + } + +} \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java b/user-service/src/main/java/com/example/userservice/controller/SignupController.java similarity index 99% rename from user-service/src/main/java/com/example/userservice/controller/AdminSignup.java rename to user-service/src/main/java/com/example/userservice/controller/SignupController.java index 2bcd045..823474d 100644 --- a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java +++ b/user-service/src/main/java/com/example/userservice/controller/SignupController.java @@ -20,7 +20,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/auth") -public class AdminSignup { +public class SignupController { private final MemberService memberService; private final JwtUtil jwtUtil; private final TokenService tokenService; diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java index bf211c8..41b4126 100644 --- a/user-service/src/main/java/com/example/userservice/service/MemberService.java +++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java @@ -10,6 +10,8 @@ import org.springframework.stereotype.Service; import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -132,4 +134,24 @@ public void deleteMember(String userId) { } + + /** + * 모든 회원 목록 조회 + * @return 모든 회원 정보 목록 + */ + public List getAllMembers() { + List members = memberRepository.findAll(); + + return members.stream() + .map(member -> MemberDto.MemberResponse.builder() + .user_id(member.getUserId()) + .nickname(member.getNickname()) + .birth_year(member.getBirthYear()) + .mbti(member.getMbti()) + .gender(member.getGender()) + .role(member.getRole()) + .created_at(member.getCreatedAt()) + .build()) + .collect(Collectors.toList()); + } } \ No newline at end of file From 0b82cdb494b8e9377e79efe72625a0aa6ee2f15a Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 14:20:47 +0900 Subject: [PATCH 192/361] =?UTF-8?q?CI/CD=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 111 ++++++++++++++++++ congestion-service/Dockerfile | 44 +++++++ external-info-service/Dockerfile | 44 +++++++ gateway/Dockerfile | 44 +++++++ place-service/Dockerfile | 64 ++++++++-- .../controller/AccommodationController.java | 6 +- .../userservice/config/RedisConfig.java | 2 +- .../src/main/resources/application.properties | 2 +- 8 files changed, 304 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 congestion-service/Dockerfile create mode 100644 external-info-service/Dockerfile create mode 100644 gateway/Dockerfile diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..73b4b61 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,111 @@ +name: Backend Upload ECR + +#on: +# workflow_dispatch: +# inputs: +# environment: +# description: '수동 배포' +# required: true +# default: 'all' +# type: choice +# options: +# - all +# - latest + +on: + push: + branches: [develop] + pull_request: + branches: [develop] + + +jobs: + upload-image: + runs-on: ubuntu-latest + env : + IMAGE_TAG: ${{ github.run_number }} + steps: +# - name: Set IMAGE_TAG based on input +# id: tag +# run: | +# if [ "${{ github.event.inputs.environment }}" = "all" ]; then +# echo "IMAGE_TAG=1.0.${{ github.run_number }}" >> $GITHUB_ENV +# else +# echo "IMAGE_TAG=latest" >> $GITHUB_ENV +# fi + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: 자바 설치 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: '17' + + - name: place-service 빌드 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + cd place-service + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/place-service:$IMAGE_TAG . + docker tag $ECR_REGISTRY/place-service:$IMAGE_TAG $ECR_REGISTRY/place-service:latest + + docker push $ECR_REGISTRY/place-service:$IMAGE_TAG + docker push $ECR_REGISTRY/place-service:latest + + - name: congestion-service 빌드 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + cd congestion-service + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/congestion-service:$IMAGE_TAG . + docker tag $ECR_REGISTRY/congestion-service:$IMAGE_TAG $ECR_REGISTRY/congestion-service:latest + + docker push $ECR_REGISTRY/congestion-service:$IMAGE_TAG + docker push $ECR_REGISTRY/congestion-service:latest + + - name: external-info-service 빌드 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + cd external-info-service + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/external-info-service:$IMAGE_TAG . + docker tag $ECR_REGISTRY/external-info-service:$IMAGE_TAG $ECR_REGISTRY/external-info-service:latest + + docker push $ECR_REGISTRY/external-info-service:$IMAGE_TAG + docker push $ECR_REGISTRY/external-info-service:latest + + + - name: kubeconfig 설정 + run: aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster + + - name: Deploy to EKS + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + kubectl set image deployment/external-info-service external-info-service=$ECR_REGISTRY/external-info-service:$IMAGE_TAG + kubectl rollout status deployment/external-info-service + + kubectl set image deployment/place-service place-service=$ECR_REGISTRY/place-service:$IMAGE_TAG + kubectl rollout status deployment/place-service + + kubectl set image deployment/congestion-service congestion-service=$ECR_REGISTRY/congestion-service:$IMAGE_TAG + kubectl rollout status deployment/congestion-service \ No newline at end of file diff --git a/congestion-service/Dockerfile b/congestion-service/Dockerfile new file mode 100644 index 0000000..cb414e8 --- /dev/null +++ b/congestion-service/Dockerfile @@ -0,0 +1,44 @@ +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 +WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + +COPY . . + +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 +WORKDIR /app + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./congestion.jar +# 포트지정 + +# env 파일 추가 +COPY .env .env + +EXPOSE 8082 + +# 서버가동 +CMD ["java", "-jar", "congestion.jar"] \ No newline at end of file diff --git a/external-info-service/Dockerfile b/external-info-service/Dockerfile new file mode 100644 index 0000000..e0f6066 --- /dev/null +++ b/external-info-service/Dockerfile @@ -0,0 +1,44 @@ +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 +WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + +COPY . . + +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 +WORKDIR /app + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./external.jar +# 포트지정 + +# env 파일 추가 +COPY .env .env + +EXPOSE 8081 + +# 서버가동 +CMD ["java", "-jar", "external.jar"] \ No newline at end of file diff --git a/gateway/Dockerfile b/gateway/Dockerfile new file mode 100644 index 0000000..568e28d --- /dev/null +++ b/gateway/Dockerfile @@ -0,0 +1,44 @@ +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 +WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + +COPY . . + +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 +WORKDIR /app + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./gateway.jar +# 포트지정 + +# env 파일 추가 +#COPY .env .env + +EXPOSE 8080 + +# 서버가동 +CMD ["java", "-jar", "gateway.jar"] \ No newline at end of file diff --git a/place-service/Dockerfile b/place-service/Dockerfile index 5508683..dfd3b8c 100644 --- a/place-service/Dockerfile +++ b/place-service/Dockerfile @@ -1,13 +1,63 @@ -# Gradle -FROM gradle:8.13.0-jdk17 AS build +## Gradle +#FROM gradle:8.13.0-jdk17 AS build +#WORKDIR /app +#COPY . . +#RUN gradle build -x test +# +#FROM eclipse-temurin:17-jdk +#WORKDIR /app +#COPY --from=build /app/build/libs/*.jar app.jar +#EXPOSE 8080 +#ENTRYPOINT ["java", "-jar", "app.jar"] +# +#ARG TOUR_API_KEY +# +#COPY .env .env + + +# 테스트용 + +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + COPY . . -RUN gradle build -x test -FROM eclipse-temurin:17-jdk +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 WORKDIR /app -COPY --from=build /app/build/libs/*.jar app.jar + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./place.jar +# 포트지정 + +# env 파일 추가 +COPY .env .env + EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "app.jar"] -COPY .env .env \ No newline at end of file +# 서버가동 +CMD ["java", "-jar", "place.jar"] \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java index 84594e4..8e07589 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java @@ -13,18 +13,16 @@ @RestController @RequestMapping("/main/accommodation") public class AccommodationController { - private final AccommodationService accommodationParseService; private final AccommodationService accommodationService; // 생성자 주입 - public AccommodationController(AccommodationService accommodationParseService, AccommodationService accommodationService) { - this.accommodationParseService = accommodationParseService; + public AccommodationController(AccommodationService accommodationService) { this.accommodationService = accommodationService; } @GetMapping("/list") public List getAccommodations() throws IOException { - return accommodationParseService.showAccommodations(); + return accommodationService.showAccommodations(); } @GetMapping("/{accommodation_id}") diff --git a/user-service/src/main/java/com/example/userservice/config/RedisConfig.java b/user-service/src/main/java/com/example/userservice/config/RedisConfig.java index 1a26232..1086b58 100644 --- a/user-service/src/main/java/com/example/userservice/config/RedisConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/RedisConfig.java @@ -5,6 +5,6 @@ @Configuration @EnableRedisRepositories(basePackages = "com.example.userservice.repository.redis") -public class RedisConfig { +public class RedisConfig { // 추가 설정 있으면 작성 } diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties index d99579a..2757797 100644 --- a/user-service/src/main/resources/application.properties +++ b/user-service/src/main/resources/application.properties @@ -1,6 +1,6 @@ spring.application.name=user-service -server.port=8082 +server.port=8083 spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db spring.datasource.username=root From 2efc767d63dbea842fc0e0cbe6fa438649d340bd Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 14:20:47 +0900 Subject: [PATCH 193/361] =?UTF-8?q?CI/CD=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 111 ++++++++++++++++++ congestion-service/Dockerfile | 44 +++++++ external-info-service/Dockerfile | 44 +++++++ gateway/Dockerfile | 44 +++++++ place-service/Dockerfile | 64 ++++++++-- .../controller/AccommodationController.java | 6 +- .../userservice/config/RedisConfig.java | 2 +- .../src/main/resources/application.properties | 2 +- 8 files changed, 304 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 congestion-service/Dockerfile create mode 100644 external-info-service/Dockerfile create mode 100644 gateway/Dockerfile diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..73b4b61 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,111 @@ +name: Backend Upload ECR + +#on: +# workflow_dispatch: +# inputs: +# environment: +# description: '수동 배포' +# required: true +# default: 'all' +# type: choice +# options: +# - all +# - latest + +on: + push: + branches: [develop] + pull_request: + branches: [develop] + + +jobs: + upload-image: + runs-on: ubuntu-latest + env : + IMAGE_TAG: ${{ github.run_number }} + steps: +# - name: Set IMAGE_TAG based on input +# id: tag +# run: | +# if [ "${{ github.event.inputs.environment }}" = "all" ]; then +# echo "IMAGE_TAG=1.0.${{ github.run_number }}" >> $GITHUB_ENV +# else +# echo "IMAGE_TAG=latest" >> $GITHUB_ENV +# fi + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: 자바 설치 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: '17' + + - name: place-service 빌드 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + cd place-service + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/place-service:$IMAGE_TAG . + docker tag $ECR_REGISTRY/place-service:$IMAGE_TAG $ECR_REGISTRY/place-service:latest + + docker push $ECR_REGISTRY/place-service:$IMAGE_TAG + docker push $ECR_REGISTRY/place-service:latest + + - name: congestion-service 빌드 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + cd congestion-service + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/congestion-service:$IMAGE_TAG . + docker tag $ECR_REGISTRY/congestion-service:$IMAGE_TAG $ECR_REGISTRY/congestion-service:latest + + docker push $ECR_REGISTRY/congestion-service:$IMAGE_TAG + docker push $ECR_REGISTRY/congestion-service:latest + + - name: external-info-service 빌드 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + cd external-info-service + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/external-info-service:$IMAGE_TAG . + docker tag $ECR_REGISTRY/external-info-service:$IMAGE_TAG $ECR_REGISTRY/external-info-service:latest + + docker push $ECR_REGISTRY/external-info-service:$IMAGE_TAG + docker push $ECR_REGISTRY/external-info-service:latest + + + - name: kubeconfig 설정 + run: aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster + + - name: Deploy to EKS + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + kubectl set image deployment/external-info-service external-info-service=$ECR_REGISTRY/external-info-service:$IMAGE_TAG + kubectl rollout status deployment/external-info-service + + kubectl set image deployment/place-service place-service=$ECR_REGISTRY/place-service:$IMAGE_TAG + kubectl rollout status deployment/place-service + + kubectl set image deployment/congestion-service congestion-service=$ECR_REGISTRY/congestion-service:$IMAGE_TAG + kubectl rollout status deployment/congestion-service \ No newline at end of file diff --git a/congestion-service/Dockerfile b/congestion-service/Dockerfile new file mode 100644 index 0000000..cb414e8 --- /dev/null +++ b/congestion-service/Dockerfile @@ -0,0 +1,44 @@ +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 +WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + +COPY . . + +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 +WORKDIR /app + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./congestion.jar +# 포트지정 + +# env 파일 추가 +COPY .env .env + +EXPOSE 8082 + +# 서버가동 +CMD ["java", "-jar", "congestion.jar"] \ No newline at end of file diff --git a/external-info-service/Dockerfile b/external-info-service/Dockerfile new file mode 100644 index 0000000..e0f6066 --- /dev/null +++ b/external-info-service/Dockerfile @@ -0,0 +1,44 @@ +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 +WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + +COPY . . + +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 +WORKDIR /app + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./external.jar +# 포트지정 + +# env 파일 추가 +COPY .env .env + +EXPOSE 8081 + +# 서버가동 +CMD ["java", "-jar", "external.jar"] \ No newline at end of file diff --git a/gateway/Dockerfile b/gateway/Dockerfile new file mode 100644 index 0000000..568e28d --- /dev/null +++ b/gateway/Dockerfile @@ -0,0 +1,44 @@ +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 +WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + +COPY . . + +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 +WORKDIR /app + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./gateway.jar +# 포트지정 + +# env 파일 추가 +#COPY .env .env + +EXPOSE 8080 + +# 서버가동 +CMD ["java", "-jar", "gateway.jar"] \ No newline at end of file diff --git a/place-service/Dockerfile b/place-service/Dockerfile index 5508683..dfd3b8c 100644 --- a/place-service/Dockerfile +++ b/place-service/Dockerfile @@ -1,13 +1,63 @@ -# Gradle -FROM gradle:8.13.0-jdk17 AS build +## Gradle +#FROM gradle:8.13.0-jdk17 AS build +#WORKDIR /app +#COPY . . +#RUN gradle build -x test +# +#FROM eclipse-temurin:17-jdk +#WORKDIR /app +#COPY --from=build /app/build/libs/*.jar app.jar +#EXPOSE 8080 +#ENTRYPOINT ["java", "-jar", "app.jar"] +# +#ARG TOUR_API_KEY +# +#COPY .env .env + + +# 테스트용 + +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + COPY . . -RUN gradle build -x test -FROM eclipse-temurin:17-jdk +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 WORKDIR /app -COPY --from=build /app/build/libs/*.jar app.jar + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./place.jar +# 포트지정 + +# env 파일 추가 +COPY .env .env + EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "app.jar"] -COPY .env .env \ No newline at end of file +# 서버가동 +CMD ["java", "-jar", "place.jar"] \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java index 84594e4..8e07589 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java @@ -13,18 +13,16 @@ @RestController @RequestMapping("/main/accommodation") public class AccommodationController { - private final AccommodationService accommodationParseService; private final AccommodationService accommodationService; // 생성자 주입 - public AccommodationController(AccommodationService accommodationParseService, AccommodationService accommodationService) { - this.accommodationParseService = accommodationParseService; + public AccommodationController(AccommodationService accommodationService) { this.accommodationService = accommodationService; } @GetMapping("/list") public List getAccommodations() throws IOException { - return accommodationParseService.showAccommodations(); + return accommodationService.showAccommodations(); } @GetMapping("/{accommodation_id}") diff --git a/user-service/src/main/java/com/example/userservice/config/RedisConfig.java b/user-service/src/main/java/com/example/userservice/config/RedisConfig.java index 1a26232..1086b58 100644 --- a/user-service/src/main/java/com/example/userservice/config/RedisConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/RedisConfig.java @@ -5,6 +5,6 @@ @Configuration @EnableRedisRepositories(basePackages = "com.example.userservice.repository.redis") -public class RedisConfig { +public class RedisConfig { // 추가 설정 있으면 작성 } diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties index d99579a..2757797 100644 --- a/user-service/src/main/resources/application.properties +++ b/user-service/src/main/resources/application.properties @@ -1,6 +1,6 @@ spring.application.name=user-service -server.port=8082 +server.port=8083 spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db spring.datasource.username=root From 91989791ca555a41bfdf8a74fda4e068d480c444 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Tue, 29 Apr 2025 14:20:53 +0900 Subject: [PATCH 194/361] =?UTF-8?q?Refactor:=20=EA=B4=80=EB=A6=AC=EC=A7=80?= =?UTF-8?q?=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../userservice/controller/AdminSignup.java | 49 ------------------- .../userservice/service/MemberService.java | 4 -- 2 files changed, 53 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java index 2bcd045..b7fa21b 100644 --- a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java +++ b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java @@ -88,53 +88,4 @@ public ResponseEntity signout(@PathVariable String userId) { } } - - /** - * 관리자 회원가입 API - * @param dto 관리자 회원가입 정보 - * @return 토큰 및 관리자 정보가 포함된 응답 - */ - @PostMapping("/admin/signup") - public ResponseEntity> adminSignup(@RequestBody MemberSign dto) { - try { - // 디버깅 로그 출력 - System.out.println("관리자 회원가입 요청: userId=" + dto.getUserId() + ", nickname=" + dto.getNickname()); - - // 관리자로 등록 - Member member = memberService.registerAdmin(dto); - - System.out.println("관리자 회원가입 성공: memberId=" + member.getMemberId() - + ", userId=" + member.getUserId() - + ", nickname=" + member.getNickname() - + ", role=" + member.getRole()); - - // 관리자 권한 설정 - List authorities = Collections.singletonList( - new SimpleGrantedAuthority(member.getRole()) - ); - - // 회원가입 성공하면 토큰 발급 - UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities); - String accessToken = jwtUtil.generateToken(userDetails); - String refreshToken = jwtUtil.generateRefreshToken(userDetails); - - // 리프레시 토큰 저장 - tokenService.saveRefreshToken(member.getMemberId(), refreshToken); - - // 응답 생성 - Map response = new HashMap<>(); - response.put("accessToken", accessToken); - response.put("refreshToken", refreshToken); - response.put("member_id", String.valueOf(member.getMemberId())); - response.put("user_id", member.getUserId()); - response.put("nickname", member.getNickname()); - response.put("role", member.getRole()); - - return ResponseEntity.ok(response); - } catch (IllegalStateException e) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "관리자 회원가입 실패: " + e.getMessage()); - return ResponseEntity.badRequest().body(errorResponse); - } - } } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java index bf211c8..56e0155 100644 --- a/user-service/src/main/java/com/example/userservice/service/MemberService.java +++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java @@ -22,10 +22,6 @@ public Member registerUser(MemberSign dto) { return registerMember(dto, "ROLE_USER"); } - // 관리자 회원가입 - public Member registerAdmin(MemberSign dto) { - return registerMember(dto, "ROLE_ADMIN"); - } // 공통 회원가입 로직 private Member registerMember(MemberSign dto, String defaultRole) { From c6d244a33d8411720a7feb017f8255e2968fb64b Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Tue, 29 Apr 2025 14:20:53 +0900 Subject: [PATCH 195/361] =?UTF-8?q?Refactor:=20=EA=B4=80=EB=A6=AC=EC=A7=80?= =?UTF-8?q?=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../userservice/controller/AdminSignup.java | 49 ------------------- .../userservice/service/MemberService.java | 4 -- 2 files changed, 53 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java index 2bcd045..b7fa21b 100644 --- a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java +++ b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java @@ -88,53 +88,4 @@ public ResponseEntity signout(@PathVariable String userId) { } } - - /** - * 관리자 회원가입 API - * @param dto 관리자 회원가입 정보 - * @return 토큰 및 관리자 정보가 포함된 응답 - */ - @PostMapping("/admin/signup") - public ResponseEntity> adminSignup(@RequestBody MemberSign dto) { - try { - // 디버깅 로그 출력 - System.out.println("관리자 회원가입 요청: userId=" + dto.getUserId() + ", nickname=" + dto.getNickname()); - - // 관리자로 등록 - Member member = memberService.registerAdmin(dto); - - System.out.println("관리자 회원가입 성공: memberId=" + member.getMemberId() - + ", userId=" + member.getUserId() - + ", nickname=" + member.getNickname() - + ", role=" + member.getRole()); - - // 관리자 권한 설정 - List authorities = Collections.singletonList( - new SimpleGrantedAuthority(member.getRole()) - ); - - // 회원가입 성공하면 토큰 발급 - UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities); - String accessToken = jwtUtil.generateToken(userDetails); - String refreshToken = jwtUtil.generateRefreshToken(userDetails); - - // 리프레시 토큰 저장 - tokenService.saveRefreshToken(member.getMemberId(), refreshToken); - - // 응답 생성 - Map response = new HashMap<>(); - response.put("accessToken", accessToken); - response.put("refreshToken", refreshToken); - response.put("member_id", String.valueOf(member.getMemberId())); - response.put("user_id", member.getUserId()); - response.put("nickname", member.getNickname()); - response.put("role", member.getRole()); - - return ResponseEntity.ok(response); - } catch (IllegalStateException e) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "관리자 회원가입 실패: " + e.getMessage()); - return ResponseEntity.badRequest().body(errorResponse); - } - } } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java index bf211c8..56e0155 100644 --- a/user-service/src/main/java/com/example/userservice/service/MemberService.java +++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java @@ -22,10 +22,6 @@ public Member registerUser(MemberSign dto) { return registerMember(dto, "ROLE_USER"); } - // 관리자 회원가입 - public Member registerAdmin(MemberSign dto) { - return registerMember(dto, "ROLE_ADMIN"); - } // 공통 회원가입 로직 private Member registerMember(MemberSign dto, String defaultRole) { From 10a446b31090ed4715bf7f70376b6fa7b4ecce9d Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 14:48:12 +0900 Subject: [PATCH 196/361] =?UTF-8?q?deploy.yml=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 24 +- external-info-service/.gitignore | 1 + .../service/ParkServiceOriginal.java | 52 --- .../service/WeatherServiceOriginal.java | 301 ------------------ 4 files changed, 21 insertions(+), 357 deletions(-) delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/ParkServiceOriginal.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 73b4b61..b731272 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -54,6 +54,19 @@ jobs: distribution: 'temurin' java-version: '17' + - name: gateway 빌드 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + cd gateway + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/gateway:$IMAGE_TAG . + docker tag $ECR_REGISTRY/gateway:$IMAGE_TAG $ECR_REGISTRY/gateway:latest + + docker push $ECR_REGISTRY/gateway:$IMAGE_TAG + docker push $ECR_REGISTRY/gateway:latest + - name: place-service 빌드 env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} @@ -101,11 +114,14 @@ jobs: env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} run: | - kubectl set image deployment/external-info-service external-info-service=$ECR_REGISTRY/external-info-service:$IMAGE_TAG + kubectl set image deployment/external-info-service external-info-service=$ECR_REGISTRY/external-info-service:latest kubectl rollout status deployment/external-info-service - kubectl set image deployment/place-service place-service=$ECR_REGISTRY/place-service:$IMAGE_TAG + kubectl set image deployment/place-service place-service=$ECR_REGISTRY/place-service:latest kubectl rollout status deployment/place-service - kubectl set image deployment/congestion-service congestion-service=$ECR_REGISTRY/congestion-service:$IMAGE_TAG - kubectl rollout status deployment/congestion-service \ No newline at end of file + kubectl set image deployment/congestion-service congestion-service=$ECR_REGISTRY/congestion-service:latest + kubectl rollout status deployment/congestion-service + + kubectl set image deployment/gateway gateway=$ECR_REGISTRY/gateway:latest + kubectl rollout status deployment/gateway \ No newline at end of file diff --git a/external-info-service/.gitignore b/external-info-service/.gitignore index c2065bc..55608a8 100644 --- a/external-info-service/.gitignore +++ b/external-info-service/.gitignore @@ -4,6 +4,7 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +.env ### STS ### .apt_generated diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkServiceOriginal.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkServiceOriginal.java deleted file mode 100644 index 3ee0688..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkServiceOriginal.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.externalinfoservice.service; - -import com.example.externalinfoservice.dto.ParkDto; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.Unmarshaller; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.io.StringReader; - -@Service -public class ParkServiceOriginal { - - @Value("${seoul.api.key}") - private String apiKey; - - private final RestTemplate restTemplate; - - public ParkServiceOriginal(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } - - public String getParkingData(String areaId) { - try { - String url = String.format( - "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", - apiKey, areaId - ); - - // XML 요청 - String xmlResponse = restTemplate.getForObject(url, String.class); - - // XML -> DTO - ParkDto parkDto = xmlToDto(xmlResponse); - - // DTO -> JSON - ObjectMapper objectMapper = new ObjectMapper(); - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(parkDto); - - } catch (Exception e) { - throw new RuntimeException("주차장 데이터 파싱 실패: " + e.getMessage(), e); - } - } - - private ParkDto xmlToDto(String xml) throws Exception { - JAXBContext context = JAXBContext.newInstance(ParkDto.class); - Unmarshaller unmarshaller = context.createUnmarshaller(); - return (ParkDto) unmarshaller.unmarshal(new StringReader(xml)); - } -} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java deleted file mode 100644 index 2b29623..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java +++ /dev/null @@ -1,301 +0,0 @@ -package com.example.externalinfoservice.service; - -import com.example.externalinfoservice.entity.Area; -import com.example.externalinfoservice.repository.AreaRepository; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import java.io.ByteArrayInputStream; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.stream.Collectors; - -@Service -public class WeatherServiceOriginal { - - @Value("${seoul.api.key}") - private String apiKey; - - private final RestTemplate restTemplate; - private final ObjectMapper objectMapper; - private final AreaRepository areaRepository; - - // 날씨 상태별 아이콘 URL 매핑 - private static final Map WEATHER_ICONS = new HashMap<>() {{ - put("구름 조금", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_halfsun.png"); - put("비", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_rain.png"); - put("눈", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_snow.png"); - put("구름 많음", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_cloudy.png"); - put("맑음", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sunny.png"); - }}; - - public WeatherServiceOriginal(RestTemplate restTemplate, - ObjectMapper objectMapper, - AreaRepository areaRepository) { - this.restTemplate = restTemplate; - this.objectMapper = objectMapper; - this.areaRepository = areaRepository; - } - - /** - * 특정 지역의 상세 날씨 정보를 제공합니다. - */ - public String getWeatherData(String placeCode) { - try { - // place-service 데이터베이스에서 지역 정보 조회 - Area area = areaRepository.findBySeoulId(placeCode) - .orElseThrow(() -> new RuntimeException("해당 지역 코드를 찾을 수 없습니다: " + placeCode)); - - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String apiUrl = String.format( - "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", - apiKey, placeCode - ); - String xmlResponse = restTemplate.getForObject(apiUrl, String.class); - - // XML 파싱을 위한 DocumentBuilder 생성 - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes())); - - // JSON 객체 생성 - ObjectNode weatherJson = objectMapper.createObjectNode(); - ObjectNode weatherSttsJson = objectMapper.createObjectNode(); - - // 지역 추가 정보 - weatherJson.put("areaName", area.getName()); - weatherJson.put("areaNameEng", area.getNameEng()); - weatherJson.put("category", area.getCategory()); - - // 위도, 경도 정보 추가 (선택적) - if (area.getLatitude() != null && area.getLongitude() != null) { - ObjectNode locationJson = objectMapper.createObjectNode(); - locationJson.put("latitude", area.getLatitude()); - locationJson.put("longitude", area.getLongitude()); - weatherJson.set("location", locationJson); - } - - // 원하는 필드들 - String[] desiredFields = { - "TEMP", "PRECIPITATION", "PRECPT_TYPE", - "PM10", "FCST24HOURS", "SKY_STTS" - }; - - // SKY_STTS 값을 저장할 변수 - String skyStatus = ""; - - // 각 필드별로 특별 처리 - for (String field : desiredFields) { - NodeList fieldNodes = document.getElementsByTagName(field); - - // 각 필드에 대해 값 추출 - if (fieldNodes.getLength() > 0) { - Node fieldNode = fieldNodes.item(0); - String nodeValue = fieldNode.getTextContent().trim(); - - if (field.equals("FCST24HOURS")) { - // FCST24HOURS는 배열로 처리 - ArrayNode forecastArray = objectMapper.createArrayNode(); - NodeList fcstChildren = fieldNode.getChildNodes(); - - for (int j = 0; j < fcstChildren.getLength(); j++) { - Node fcstChild = fcstChildren.item(j); - if (fcstChild.getNodeType() == Node.ELEMENT_NODE) { - Element fcstElement = (Element) fcstChild; - ObjectNode forecastItem = objectMapper.createObjectNode(); - NodeList itemChildren = fcstElement.getChildNodes(); - - for (int k = 0; k < itemChildren.getLength(); k++) { - Node itemChild = itemChildren.item(k); - if (itemChild.getNodeType() == Node.ELEMENT_NODE) { - Element itemElement = (Element) itemChild; - String itemNodeName = itemElement.getNodeName(); - String itemNodeValue = itemElement.getTextContent().trim(); - - if (!itemNodeValue.isEmpty()) { - forecastItem.put(convertToCamelCase(itemNodeName), itemNodeValue); - } - } - } - - if (!forecastItem.isEmpty()) { - forecastArray.add(forecastItem); - } - } - } - - weatherSttsJson.set(convertToCamelCase(field), forecastArray); - } else if (field.equals("SKY_STTS")) { - // SKY_STTS 값 저장 - skyStatus = nodeValue; - weatherSttsJson.put(convertToCamelCase(field), nodeValue); - } else if (!nodeValue.isEmpty()) { - weatherSttsJson.put(convertToCamelCase(field), nodeValue); - } - } - } - - // 날씨 상태(SKY_STTS)에 따라 아이콘 URL 매핑 - String iconUrl = WEATHER_ICONS.getOrDefault(skyStatus, "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); - - // 매핑된 아이콘 URL 추가 - weatherSttsJson.put("weatherIcon", iconUrl); - - // weatherStts 필드에 추출된 정보 저장 - weatherJson.set("weatherStts", weatherSttsJson); - - // JSON 변환 및 반환 - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(weatherJson); - - } catch (Exception e) { - e.printStackTrace(); // 전체 스택 트레이스 출력 - throw new RuntimeException("날씨 데이터 변환 실패", e); - } - } - - /** - * 모든 지역의 날씨 요약 정보를 JSON 형태로 반환합니다. - */ - public String getAllAreasWeatherSummary() { - try { - // 병렬 처리를 위한 스레드 풀 생성 - ExecutorService executor = Executors.newFixedThreadPool(5); - - // 결과를 담을 객체 생성 - ObjectNode resultJson = objectMapper.createObjectNode(); - resultJson.put("title", "서울시 주요 지역 날씨 정보"); - - // 전체 지역 날씨 정보를 담을 배열 생성 - ArrayNode areasWeather = objectMapper.createArrayNode(); - - // place-service 데이터베이스에서 모든 지역 정보 조회 - List areas = areaRepository.findAll(); - - // 모든 지역에 대해 CompletableFuture를 생성하여 병렬로 API 호출 - List> futures = areas.stream() - .map(area -> CompletableFuture.supplyAsync( - () -> getAreaWeatherSummary(area.getName(), area.getSeoulId()), - executor - )) - .collect(Collectors.toList()); - - // 모든 CompletableFuture가 완료될 때까지 기다리고 결과 수집 - CompletableFuture allFutures = CompletableFuture.allOf( - futures.toArray(new CompletableFuture[0])); - - // 결과 수집 및 JSON 배열에 추가 - allFutures.thenAccept(v -> - futures.forEach(future -> { - try { - ObjectNode node = future.get(); - areasWeather.add(node); - } catch (Exception e) { - // 개별 결과 처리 중 오류 발생 시 무시하고 계속 진행 - } - }) - ).get(); - - // 스레드 풀 종료 - executor.shutdown(); - - // 결과 JSON에 지역 날씨 정보 배열 추가 - resultJson.set("data", areasWeather); - - // JSON 변환 및 반환 - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(resultJson); - - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException("지역별 날씨 정보 조회 실패", e); - } - } - - /** - * 지역의 간략한 날씨 정보를 추출합니다. - */ - private ObjectNode getAreaWeatherSummary(String areaName, String areaCode) { - try { - // 날씨 정보 조회 API 호출 - String apiUrl = String.format( - "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", - apiKey, areaCode - ); - String xmlResponse = restTemplate.getForObject(apiUrl, String.class); - - // XML 파싱 - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes())); - - // 필요한 정보 추출 - String temperature = getNodeValueByTagName(document, "TEMP"); - String skyStatus = getNodeValueByTagName(document, "SKY_STTS"); - String precipitationType = getNodeValueByTagName(document, "PRECPT_TYPE"); - - // 날씨 아이콘 매핑 - String iconUrl = WEATHER_ICONS.getOrDefault(skyStatus, - "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); - - // JSON 객체 생성 (간소화된 형식) - ObjectNode areaWeather = objectMapper.createObjectNode(); - areaWeather.put("name", areaName); - areaWeather.put("TEMP", temperature); - areaWeather.put("PRECPT_TYPE", precipitationType); - areaWeather.put("weatherIcon", iconUrl); - - return areaWeather; - } catch (Exception e) { - // 오류 발생 시 기본 정보 반환 - ObjectNode defaultNode = objectMapper.createObjectNode(); - defaultNode.put("name", areaName); - defaultNode.put("TEMP", "N/A"); - defaultNode.put("PRECPT_TYPE", "정보 없음"); - defaultNode.put("weatherIcon", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); - return defaultNode; - } - } - - /** - * 문서에서 태그명으로 노드 값을 가져옵니다. - */ - private String getNodeValueByTagName(Document document, String tagName) { - NodeList nodes = document.getElementsByTagName(tagName); - if (nodes.getLength() > 0) { - return nodes.item(0).getTextContent().trim(); - } - return ""; - } - - // 언더스코어 이름을 camelCase로 변환 - private String convertToCamelCase(String input) { - if (input == null || input.isEmpty()) return input; - - StringBuilder result = new StringBuilder(); - boolean capitalizeNext = false; - - for (char c : input.toLowerCase().toCharArray()) { - if (c == '_') { - capitalizeNext = true; - } else if (capitalizeNext) { - result.append(Character.toUpperCase(c)); - capitalizeNext = false; - } else { - result.append(c); - } - } - - return result.toString(); - } -} \ No newline at end of file From fe9f275fe3ee46158263d8c17277a7b528b13fc7 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 14:48:12 +0900 Subject: [PATCH 197/361] =?UTF-8?q?deploy.yml=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 24 +- external-info-service/.gitignore | 1 + .../service/ParkServiceOriginal.java | 52 --- .../service/WeatherServiceOriginal.java | 301 ------------------ .../src/main/resources/application.properties | 6 +- 5 files changed, 23 insertions(+), 361 deletions(-) delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/ParkServiceOriginal.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 73b4b61..b731272 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -54,6 +54,19 @@ jobs: distribution: 'temurin' java-version: '17' + - name: gateway 빌드 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + cd gateway + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/gateway:$IMAGE_TAG . + docker tag $ECR_REGISTRY/gateway:$IMAGE_TAG $ECR_REGISTRY/gateway:latest + + docker push $ECR_REGISTRY/gateway:$IMAGE_TAG + docker push $ECR_REGISTRY/gateway:latest + - name: place-service 빌드 env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} @@ -101,11 +114,14 @@ jobs: env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} run: | - kubectl set image deployment/external-info-service external-info-service=$ECR_REGISTRY/external-info-service:$IMAGE_TAG + kubectl set image deployment/external-info-service external-info-service=$ECR_REGISTRY/external-info-service:latest kubectl rollout status deployment/external-info-service - kubectl set image deployment/place-service place-service=$ECR_REGISTRY/place-service:$IMAGE_TAG + kubectl set image deployment/place-service place-service=$ECR_REGISTRY/place-service:latest kubectl rollout status deployment/place-service - kubectl set image deployment/congestion-service congestion-service=$ECR_REGISTRY/congestion-service:$IMAGE_TAG - kubectl rollout status deployment/congestion-service \ No newline at end of file + kubectl set image deployment/congestion-service congestion-service=$ECR_REGISTRY/congestion-service:latest + kubectl rollout status deployment/congestion-service + + kubectl set image deployment/gateway gateway=$ECR_REGISTRY/gateway:latest + kubectl rollout status deployment/gateway \ No newline at end of file diff --git a/external-info-service/.gitignore b/external-info-service/.gitignore index c2065bc..55608a8 100644 --- a/external-info-service/.gitignore +++ b/external-info-service/.gitignore @@ -4,6 +4,7 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +.env ### STS ### .apt_generated diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkServiceOriginal.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkServiceOriginal.java deleted file mode 100644 index 3ee0688..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkServiceOriginal.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.externalinfoservice.service; - -import com.example.externalinfoservice.dto.ParkDto; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.Unmarshaller; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.io.StringReader; - -@Service -public class ParkServiceOriginal { - - @Value("${seoul.api.key}") - private String apiKey; - - private final RestTemplate restTemplate; - - public ParkServiceOriginal(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } - - public String getParkingData(String areaId) { - try { - String url = String.format( - "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", - apiKey, areaId - ); - - // XML 요청 - String xmlResponse = restTemplate.getForObject(url, String.class); - - // XML -> DTO - ParkDto parkDto = xmlToDto(xmlResponse); - - // DTO -> JSON - ObjectMapper objectMapper = new ObjectMapper(); - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(parkDto); - - } catch (Exception e) { - throw new RuntimeException("주차장 데이터 파싱 실패: " + e.getMessage(), e); - } - } - - private ParkDto xmlToDto(String xml) throws Exception { - JAXBContext context = JAXBContext.newInstance(ParkDto.class); - Unmarshaller unmarshaller = context.createUnmarshaller(); - return (ParkDto) unmarshaller.unmarshal(new StringReader(xml)); - } -} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java deleted file mode 100644 index 2b29623..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java +++ /dev/null @@ -1,301 +0,0 @@ -package com.example.externalinfoservice.service; - -import com.example.externalinfoservice.entity.Area; -import com.example.externalinfoservice.repository.AreaRepository; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import java.io.ByteArrayInputStream; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.stream.Collectors; - -@Service -public class WeatherServiceOriginal { - - @Value("${seoul.api.key}") - private String apiKey; - - private final RestTemplate restTemplate; - private final ObjectMapper objectMapper; - private final AreaRepository areaRepository; - - // 날씨 상태별 아이콘 URL 매핑 - private static final Map WEATHER_ICONS = new HashMap<>() {{ - put("구름 조금", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_halfsun.png"); - put("비", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_rain.png"); - put("눈", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_snow.png"); - put("구름 많음", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_cloudy.png"); - put("맑음", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sunny.png"); - }}; - - public WeatherServiceOriginal(RestTemplate restTemplate, - ObjectMapper objectMapper, - AreaRepository areaRepository) { - this.restTemplate = restTemplate; - this.objectMapper = objectMapper; - this.areaRepository = areaRepository; - } - - /** - * 특정 지역의 상세 날씨 정보를 제공합니다. - */ - public String getWeatherData(String placeCode) { - try { - // place-service 데이터베이스에서 지역 정보 조회 - Area area = areaRepository.findBySeoulId(placeCode) - .orElseThrow(() -> new RuntimeException("해당 지역 코드를 찾을 수 없습니다: " + placeCode)); - - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String apiUrl = String.format( - "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", - apiKey, placeCode - ); - String xmlResponse = restTemplate.getForObject(apiUrl, String.class); - - // XML 파싱을 위한 DocumentBuilder 생성 - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes())); - - // JSON 객체 생성 - ObjectNode weatherJson = objectMapper.createObjectNode(); - ObjectNode weatherSttsJson = objectMapper.createObjectNode(); - - // 지역 추가 정보 - weatherJson.put("areaName", area.getName()); - weatherJson.put("areaNameEng", area.getNameEng()); - weatherJson.put("category", area.getCategory()); - - // 위도, 경도 정보 추가 (선택적) - if (area.getLatitude() != null && area.getLongitude() != null) { - ObjectNode locationJson = objectMapper.createObjectNode(); - locationJson.put("latitude", area.getLatitude()); - locationJson.put("longitude", area.getLongitude()); - weatherJson.set("location", locationJson); - } - - // 원하는 필드들 - String[] desiredFields = { - "TEMP", "PRECIPITATION", "PRECPT_TYPE", - "PM10", "FCST24HOURS", "SKY_STTS" - }; - - // SKY_STTS 값을 저장할 변수 - String skyStatus = ""; - - // 각 필드별로 특별 처리 - for (String field : desiredFields) { - NodeList fieldNodes = document.getElementsByTagName(field); - - // 각 필드에 대해 값 추출 - if (fieldNodes.getLength() > 0) { - Node fieldNode = fieldNodes.item(0); - String nodeValue = fieldNode.getTextContent().trim(); - - if (field.equals("FCST24HOURS")) { - // FCST24HOURS는 배열로 처리 - ArrayNode forecastArray = objectMapper.createArrayNode(); - NodeList fcstChildren = fieldNode.getChildNodes(); - - for (int j = 0; j < fcstChildren.getLength(); j++) { - Node fcstChild = fcstChildren.item(j); - if (fcstChild.getNodeType() == Node.ELEMENT_NODE) { - Element fcstElement = (Element) fcstChild; - ObjectNode forecastItem = objectMapper.createObjectNode(); - NodeList itemChildren = fcstElement.getChildNodes(); - - for (int k = 0; k < itemChildren.getLength(); k++) { - Node itemChild = itemChildren.item(k); - if (itemChild.getNodeType() == Node.ELEMENT_NODE) { - Element itemElement = (Element) itemChild; - String itemNodeName = itemElement.getNodeName(); - String itemNodeValue = itemElement.getTextContent().trim(); - - if (!itemNodeValue.isEmpty()) { - forecastItem.put(convertToCamelCase(itemNodeName), itemNodeValue); - } - } - } - - if (!forecastItem.isEmpty()) { - forecastArray.add(forecastItem); - } - } - } - - weatherSttsJson.set(convertToCamelCase(field), forecastArray); - } else if (field.equals("SKY_STTS")) { - // SKY_STTS 값 저장 - skyStatus = nodeValue; - weatherSttsJson.put(convertToCamelCase(field), nodeValue); - } else if (!nodeValue.isEmpty()) { - weatherSttsJson.put(convertToCamelCase(field), nodeValue); - } - } - } - - // 날씨 상태(SKY_STTS)에 따라 아이콘 URL 매핑 - String iconUrl = WEATHER_ICONS.getOrDefault(skyStatus, "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); - - // 매핑된 아이콘 URL 추가 - weatherSttsJson.put("weatherIcon", iconUrl); - - // weatherStts 필드에 추출된 정보 저장 - weatherJson.set("weatherStts", weatherSttsJson); - - // JSON 변환 및 반환 - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(weatherJson); - - } catch (Exception e) { - e.printStackTrace(); // 전체 스택 트레이스 출력 - throw new RuntimeException("날씨 데이터 변환 실패", e); - } - } - - /** - * 모든 지역의 날씨 요약 정보를 JSON 형태로 반환합니다. - */ - public String getAllAreasWeatherSummary() { - try { - // 병렬 처리를 위한 스레드 풀 생성 - ExecutorService executor = Executors.newFixedThreadPool(5); - - // 결과를 담을 객체 생성 - ObjectNode resultJson = objectMapper.createObjectNode(); - resultJson.put("title", "서울시 주요 지역 날씨 정보"); - - // 전체 지역 날씨 정보를 담을 배열 생성 - ArrayNode areasWeather = objectMapper.createArrayNode(); - - // place-service 데이터베이스에서 모든 지역 정보 조회 - List areas = areaRepository.findAll(); - - // 모든 지역에 대해 CompletableFuture를 생성하여 병렬로 API 호출 - List> futures = areas.stream() - .map(area -> CompletableFuture.supplyAsync( - () -> getAreaWeatherSummary(area.getName(), area.getSeoulId()), - executor - )) - .collect(Collectors.toList()); - - // 모든 CompletableFuture가 완료될 때까지 기다리고 결과 수집 - CompletableFuture allFutures = CompletableFuture.allOf( - futures.toArray(new CompletableFuture[0])); - - // 결과 수집 및 JSON 배열에 추가 - allFutures.thenAccept(v -> - futures.forEach(future -> { - try { - ObjectNode node = future.get(); - areasWeather.add(node); - } catch (Exception e) { - // 개별 결과 처리 중 오류 발생 시 무시하고 계속 진행 - } - }) - ).get(); - - // 스레드 풀 종료 - executor.shutdown(); - - // 결과 JSON에 지역 날씨 정보 배열 추가 - resultJson.set("data", areasWeather); - - // JSON 변환 및 반환 - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(resultJson); - - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException("지역별 날씨 정보 조회 실패", e); - } - } - - /** - * 지역의 간략한 날씨 정보를 추출합니다. - */ - private ObjectNode getAreaWeatherSummary(String areaName, String areaCode) { - try { - // 날씨 정보 조회 API 호출 - String apiUrl = String.format( - "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", - apiKey, areaCode - ); - String xmlResponse = restTemplate.getForObject(apiUrl, String.class); - - // XML 파싱 - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes())); - - // 필요한 정보 추출 - String temperature = getNodeValueByTagName(document, "TEMP"); - String skyStatus = getNodeValueByTagName(document, "SKY_STTS"); - String precipitationType = getNodeValueByTagName(document, "PRECPT_TYPE"); - - // 날씨 아이콘 매핑 - String iconUrl = WEATHER_ICONS.getOrDefault(skyStatus, - "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); - - // JSON 객체 생성 (간소화된 형식) - ObjectNode areaWeather = objectMapper.createObjectNode(); - areaWeather.put("name", areaName); - areaWeather.put("TEMP", temperature); - areaWeather.put("PRECPT_TYPE", precipitationType); - areaWeather.put("weatherIcon", iconUrl); - - return areaWeather; - } catch (Exception e) { - // 오류 발생 시 기본 정보 반환 - ObjectNode defaultNode = objectMapper.createObjectNode(); - defaultNode.put("name", areaName); - defaultNode.put("TEMP", "N/A"); - defaultNode.put("PRECPT_TYPE", "정보 없음"); - defaultNode.put("weatherIcon", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); - return defaultNode; - } - } - - /** - * 문서에서 태그명으로 노드 값을 가져옵니다. - */ - private String getNodeValueByTagName(Document document, String tagName) { - NodeList nodes = document.getElementsByTagName(tagName); - if (nodes.getLength() > 0) { - return nodes.item(0).getTextContent().trim(); - } - return ""; - } - - // 언더스코어 이름을 camelCase로 변환 - private String convertToCamelCase(String input) { - if (input == null || input.isEmpty()) return input; - - StringBuilder result = new StringBuilder(); - boolean capitalizeNext = false; - - for (char c : input.toLowerCase().toCharArray()) { - if (c == '_') { - capitalizeNext = true; - } else if (capitalizeNext) { - result.append(Character.toUpperCase(c)); - capitalizeNext = false; - } else { - result.append(c); - } - } - - return result.toString(); - } -} \ No newline at end of file diff --git a/external-info-service/src/main/resources/application.properties b/external-info-service/src/main/resources/application.properties index d17219a..7c53a08 100644 --- a/external-info-service/src/main/resources/application.properties +++ b/external-info-service/src/main/resources/application.properties @@ -1,6 +1,5 @@ spring.application.name=external-info-service -<<<<<<< HEAD -======= + server.port=8081 # postgreSQL ?? @@ -18,5 +17,4 @@ spring.mvc.contentnegotiation.media-types.json=application/json spring.mvc.default-response-type=application/json -seoul.api.key=505a6255746b696d34344b46566174 ->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793 +seoul.api.key=${SEOUL_API_KEY} From 376ae8aaf8bbbf4c64025155dd4090f8ef4d75bb Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 14:54:58 +0900 Subject: [PATCH 198/361] =?UTF-8?q?=EC=98=A4=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 --- .../ExternalInfoServiceApplication.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java index 8514b59..b077f1c 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java @@ -2,13 +2,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -<<<<<<< HEAD - -@SpringBootApplication -public class ExternalInfoServiceApplication { - - public static void main(String[] args) { -======= import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @@ -16,9 +9,6 @@ public static void main(String[] args) { public class ExternalInfoServiceApplication { public static void main(String[] args) { - - ->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793 SpringApplication.run(ExternalInfoServiceApplication.class, args); } From 8135ce555ed34c1b5a44a624b0e9ecc32131f19b Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 14:54:58 +0900 Subject: [PATCH 199/361] =?UTF-8?q?=EC=98=A4=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 --- .../ExternalInfoServiceApplication.java | 10 ---------- .../src/main/resources/application.properties | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java index 8514b59..b077f1c 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java @@ -2,13 +2,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -<<<<<<< HEAD - -@SpringBootApplication -public class ExternalInfoServiceApplication { - - public static void main(String[] args) { -======= import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @@ -16,9 +9,6 @@ public static void main(String[] args) { public class ExternalInfoServiceApplication { public static void main(String[] args) { - - ->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793 SpringApplication.run(ExternalInfoServiceApplication.class, args); } diff --git a/external-info-service/src/main/resources/application.properties b/external-info-service/src/main/resources/application.properties index 7c53a08..3b12f08 100644 --- a/external-info-service/src/main/resources/application.properties +++ b/external-info-service/src/main/resources/application.properties @@ -17,4 +17,4 @@ spring.mvc.contentnegotiation.media-types.json=application/json spring.mvc.default-response-type=application/json -seoul.api.key=${SEOUL_API_KEY} +seoul.api.key=${SEOUL_API_KEY} \ No newline at end of file From 9ee75a987a885f618d579d2aca31c2f3acbcab02 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Tue, 29 Apr 2025 14:57:47 +0900 Subject: [PATCH 200/361] =?UTF-8?q?Feat:=20favorite=20=EC=A6=90=EA=B2=A8?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 +- .../userservice/UserServiceApplication.java | 2 + .../example/userservice/config/JpaConfig.java | 22 ++--- .../controller/AdminController.java | 10 +++ .../controller/FavoriteController.java | 45 ++++++++++ .../example/userservice/dto/FavoriteDto.java | 21 +++++ .../example/userservice/entity/Favorite.java | 27 ++++++ .../repository/FavoriteRepository.java | 16 ++++ .../userservice/service/FavoriteService.java | 84 +++++++++++++++++++ 9 files changed, 220 insertions(+), 15 deletions(-) create mode 100644 user-service/src/main/java/com/example/userservice/controller/FavoriteController.java create mode 100644 user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java create mode 100644 user-service/src/main/java/com/example/userservice/entity/Favorite.java create mode 100644 user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java create mode 100644 user-service/src/main/java/com/example/userservice/service/FavoriteService.java diff --git a/README.md b/README.md index 72cf269..76e1af5 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Back-STARS SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. -Gateway     : port 8080
-congestion : port 8081 - :8080/control/\*\*
-place          : port 8082 - :8080/place/\*\*
-user            : port 8083 - :8080/user/\*\* +> Gateway : port 8080
+> congestion : port 8081
+> place : port 8082
+> user : port 8083
로컬에서 postgreSQL 테스트 diff --git a/user-service/src/main/java/com/example/userservice/UserServiceApplication.java b/user-service/src/main/java/com/example/userservice/UserServiceApplication.java index 55a5c33..a94c590 100644 --- a/user-service/src/main/java/com/example/userservice/UserServiceApplication.java +++ b/user-service/src/main/java/com/example/userservice/UserServiceApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @SpringBootApplication +@EnableJpaRepositories(basePackages = "com.example.userservice.repository") public class UserServiceApplication { public static void main(String[] args) { diff --git a/user-service/src/main/java/com/example/userservice/config/JpaConfig.java b/user-service/src/main/java/com/example/userservice/config/JpaConfig.java index d35f648..4ceb53d 100644 --- a/user-service/src/main/java/com/example/userservice/config/JpaConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/JpaConfig.java @@ -1,11 +1,11 @@ -package com.example.userservice.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; - -@Configuration -@EnableJpaRepositories( - basePackages = "com.example.userservice.repository.jpa" -) -public class JpaConfig { -} +//package com.example.userservice.config; +// +//import org.springframework.context.annotation.Configuration; +//import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +// +//@Configuration +//@EnableJpaRepositories( +// basePackages = "com.example.userservice.repository.jpa" +//) +//public class JpaConfig { +//} diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminController.java b/user-service/src/main/java/com/example/userservice/controller/AdminController.java index 775cc81..4eb82e5 100644 --- a/user-service/src/main/java/com/example/userservice/controller/AdminController.java +++ b/user-service/src/main/java/com/example/userservice/controller/AdminController.java @@ -1,6 +1,9 @@ package com.example.userservice.controller; +import com.example.userservice.dto.FavoriteDto; import com.example.userservice.dto.MemberDto; +import com.example.userservice.entity.Favorite; +import com.example.userservice.service.FavoriteService; import com.example.userservice.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -17,6 +20,7 @@ public class AdminController { private final MemberService memberService; + private final FavoriteService favoriteService; /** * 전체 회원 목록 조회 API @@ -29,4 +33,10 @@ public ResponseEntity> getAllMembers() { return ResponseEntity.ok(members); } + @GetMapping("/favorite/list") + @PreAuthorize("hasRole('ADMIN')") // ADMIN 권한이 있는 사용자만 접근 가능 + public ResponseEntity> getAllFavorites() { + List favorites = favoriteService.getAllFavoriteData(); + return ResponseEntity.ok(favorites); + } } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java b/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java new file mode 100644 index 0000000..d01c1d0 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java @@ -0,0 +1,45 @@ +package com.example.userservice.controller; + +import com.example.userservice.dto.FavoriteDto; +import com.example.userservice.service.FavoriteService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/mypage/favorite") +@RequiredArgsConstructor +public class FavoriteController { + + private final FavoriteService favoriteService; + + @GetMapping("/list") + public List getList() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String userId = authentication.getName(); + + return favoriteService.getListData(userId); + } + + @PostMapping("/add") + public ResponseEntity addFavorite(@RequestBody FavoriteDto favoriteDto) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String userId = authentication.getName(); + + return ResponseEntity.ok(favoriteService.addFavoriteData(userId,favoriteDto)); + } + + @DeleteMapping("/delete/{type}/{id}") + public ResponseEntity deleteFavorite(@PathVariable String type, @PathVariable String id, HttpServletRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String userId = authentication.getName(); + favoriteService.deleteFavorite(userId, type, id); + + return ResponseEntity.ok().build(); + } +} diff --git a/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java b/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java new file mode 100644 index 0000000..0aef889 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java @@ -0,0 +1,21 @@ +package com.example.userservice.dto; + +import com.example.userservice.entity.Favorite; +import jakarta.persistence.Column; +import lombok.Data; + +@Data +public class FavoriteDto { + private Long favorite_id; + private String type; + private String place_id; + private String user_id; + + public FavoriteDto(Long favorite_id ,String type, String placeId, String userId) { + this.favorite_id = favorite_id; + this.type = type; + this.place_id = placeId; + this.user_id = userId; + } + +} diff --git a/user-service/src/main/java/com/example/userservice/entity/Favorite.java b/user-service/src/main/java/com/example/userservice/entity/Favorite.java new file mode 100644 index 0000000..4cad270 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/entity/Favorite.java @@ -0,0 +1,27 @@ +package com.example.userservice.entity; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Data +@NoArgsConstructor +@Table(name = "favorite") +public class Favorite { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "favorite_id", nullable = false) + private Long favoriteId; + + @Column(name = "type", nullable = false) + private String type; + + @Column(name = "place_id", nullable = false) + private String placeId; // 사용자 로그인 ID + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "user_id", nullable = false) + private Member member; + +} diff --git a/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java b/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java new file mode 100644 index 0000000..8f113d4 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java @@ -0,0 +1,16 @@ +package com.example.userservice.repository; + +import com.example.userservice.entity.Favorite; +import com.example.userservice.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface FavoriteRepository extends JpaRepository { + List findAllByMember_UserId(String userId); + + Optional findByMemberAndTypeAndPlaceId(Member member, String type, String id); +} diff --git a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java new file mode 100644 index 0000000..51b8e9b --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java @@ -0,0 +1,84 @@ +package com.example.userservice.service; + +import com.example.userservice.dto.FavoriteDto; +import com.example.userservice.entity.Favorite; +import com.example.userservice.entity.Member; +import com.example.userservice.repository.FavoriteRepository; +import com.example.userservice.repository.jpa.MemberRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FavoriteService { + + private final FavoriteRepository favoriteRepository; + private final MemberRepository memberRepository; + + public List getListData(String userId) { + try{ + List items = favoriteRepository.findAllByMember_UserId(userId); + return items.stream() + .map(item -> new FavoriteDto( + item.getFavoriteId(), + item.getType(), + item.getPlaceId(), + userId + )).toList(); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public FavoriteDto addFavoriteData(String userId, FavoriteDto favoriteDto) { + + // 1. 사용자 조회 + Member member = memberRepository.findByUserId(userId) + .orElseThrow(() -> new RuntimeException("사용자 없음")); + + // 2. Favorite 엔티티 생성 + Favorite favorite = new Favorite(); + favorite.setType(favoriteDto.getType()); + favorite.setPlaceId(favoriteDto.getPlace_id()); // id는 placeId 역할 + favorite.setMember(member); + + // 3. 저장 + Favorite saved = favoriteRepository.save(favorite); + + // 4. DTO로 변환 후 반환 + FavoriteDto result = new FavoriteDto(saved.getFavoriteId(),member.getUserId(), saved.getType(), saved.getPlaceId()); + + return result; + } + + @Transactional + public void deleteFavorite(String userId, String type, String id) { + Member member = memberRepository.findByUserId(userId) + .orElseThrow(() -> new RuntimeException("사용자 없음")); + + Favorite favorite = favoriteRepository.findByMemberAndTypeAndPlaceId(member, type, id) + .orElseThrow(() -> new RuntimeException("즐겨찾기 없음")); + + favoriteRepository.delete(favorite); + } + + public List getAllFavoriteData() { + try{ + List items = favoriteRepository.findAll(); + return items.stream() + .map(item -> new FavoriteDto( + item.getFavoriteId(), + item.getType(), + item.getPlaceId(), + item.getMember().getUserId() + )).toList(); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} From 85d4625a962cfb7c60b8e3dbd5637440dd702c4c Mon Sep 17 00:00:00 2001 From: ha-yun Date: Tue, 29 Apr 2025 14:57:47 +0900 Subject: [PATCH 201/361] =?UTF-8?q?Feat:=20favorite=20=EC=A6=90=EA=B2=A8?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 +- .../userservice/UserServiceApplication.java | 2 + .../example/userservice/config/JpaConfig.java | 22 ++--- .../controller/AdminController.java | 10 +++ .../controller/FavoriteController.java | 45 ++++++++++ .../example/userservice/dto/FavoriteDto.java | 21 +++++ .../example/userservice/entity/Favorite.java | 27 ++++++ .../repository/FavoriteRepository.java | 16 ++++ .../userservice/service/FavoriteService.java | 84 +++++++++++++++++++ 9 files changed, 220 insertions(+), 15 deletions(-) create mode 100644 user-service/src/main/java/com/example/userservice/controller/FavoriteController.java create mode 100644 user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java create mode 100644 user-service/src/main/java/com/example/userservice/entity/Favorite.java create mode 100644 user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java create mode 100644 user-service/src/main/java/com/example/userservice/service/FavoriteService.java diff --git a/README.md b/README.md index 72cf269..76e1af5 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Back-STARS SK쉴더스 루키즈 최종프로젝트 백엔드 리포지토리 입니다. -Gateway     : port 8080
-congestion : port 8081 - :8080/control/\*\*
-place          : port 8082 - :8080/place/\*\*
-user            : port 8083 - :8080/user/\*\* +> Gateway : port 8080
+> congestion : port 8081
+> place : port 8082
+> user : port 8083
로컬에서 postgreSQL 테스트 diff --git a/user-service/src/main/java/com/example/userservice/UserServiceApplication.java b/user-service/src/main/java/com/example/userservice/UserServiceApplication.java index 55a5c33..a94c590 100644 --- a/user-service/src/main/java/com/example/userservice/UserServiceApplication.java +++ b/user-service/src/main/java/com/example/userservice/UserServiceApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @SpringBootApplication +@EnableJpaRepositories(basePackages = "com.example.userservice.repository") public class UserServiceApplication { public static void main(String[] args) { diff --git a/user-service/src/main/java/com/example/userservice/config/JpaConfig.java b/user-service/src/main/java/com/example/userservice/config/JpaConfig.java index d35f648..4ceb53d 100644 --- a/user-service/src/main/java/com/example/userservice/config/JpaConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/JpaConfig.java @@ -1,11 +1,11 @@ -package com.example.userservice.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; - -@Configuration -@EnableJpaRepositories( - basePackages = "com.example.userservice.repository.jpa" -) -public class JpaConfig { -} +//package com.example.userservice.config; +// +//import org.springframework.context.annotation.Configuration; +//import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +// +//@Configuration +//@EnableJpaRepositories( +// basePackages = "com.example.userservice.repository.jpa" +//) +//public class JpaConfig { +//} diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminController.java b/user-service/src/main/java/com/example/userservice/controller/AdminController.java index 775cc81..4eb82e5 100644 --- a/user-service/src/main/java/com/example/userservice/controller/AdminController.java +++ b/user-service/src/main/java/com/example/userservice/controller/AdminController.java @@ -1,6 +1,9 @@ package com.example.userservice.controller; +import com.example.userservice.dto.FavoriteDto; import com.example.userservice.dto.MemberDto; +import com.example.userservice.entity.Favorite; +import com.example.userservice.service.FavoriteService; import com.example.userservice.service.MemberService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -17,6 +20,7 @@ public class AdminController { private final MemberService memberService; + private final FavoriteService favoriteService; /** * 전체 회원 목록 조회 API @@ -29,4 +33,10 @@ public ResponseEntity> getAllMembers() { return ResponseEntity.ok(members); } + @GetMapping("/favorite/list") + @PreAuthorize("hasRole('ADMIN')") // ADMIN 권한이 있는 사용자만 접근 가능 + public ResponseEntity> getAllFavorites() { + List favorites = favoriteService.getAllFavoriteData(); + return ResponseEntity.ok(favorites); + } } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java b/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java new file mode 100644 index 0000000..d01c1d0 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java @@ -0,0 +1,45 @@ +package com.example.userservice.controller; + +import com.example.userservice.dto.FavoriteDto; +import com.example.userservice.service.FavoriteService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/mypage/favorite") +@RequiredArgsConstructor +public class FavoriteController { + + private final FavoriteService favoriteService; + + @GetMapping("/list") + public List getList() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String userId = authentication.getName(); + + return favoriteService.getListData(userId); + } + + @PostMapping("/add") + public ResponseEntity addFavorite(@RequestBody FavoriteDto favoriteDto) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String userId = authentication.getName(); + + return ResponseEntity.ok(favoriteService.addFavoriteData(userId,favoriteDto)); + } + + @DeleteMapping("/delete/{type}/{id}") + public ResponseEntity deleteFavorite(@PathVariable String type, @PathVariable String id, HttpServletRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String userId = authentication.getName(); + favoriteService.deleteFavorite(userId, type, id); + + return ResponseEntity.ok().build(); + } +} diff --git a/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java b/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java new file mode 100644 index 0000000..0aef889 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java @@ -0,0 +1,21 @@ +package com.example.userservice.dto; + +import com.example.userservice.entity.Favorite; +import jakarta.persistence.Column; +import lombok.Data; + +@Data +public class FavoriteDto { + private Long favorite_id; + private String type; + private String place_id; + private String user_id; + + public FavoriteDto(Long favorite_id ,String type, String placeId, String userId) { + this.favorite_id = favorite_id; + this.type = type; + this.place_id = placeId; + this.user_id = userId; + } + +} diff --git a/user-service/src/main/java/com/example/userservice/entity/Favorite.java b/user-service/src/main/java/com/example/userservice/entity/Favorite.java new file mode 100644 index 0000000..4cad270 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/entity/Favorite.java @@ -0,0 +1,27 @@ +package com.example.userservice.entity; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Data +@NoArgsConstructor +@Table(name = "favorite") +public class Favorite { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "favorite_id", nullable = false) + private Long favoriteId; + + @Column(name = "type", nullable = false) + private String type; + + @Column(name = "place_id", nullable = false) + private String placeId; // 사용자 로그인 ID + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "user_id", nullable = false) + private Member member; + +} diff --git a/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java b/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java new file mode 100644 index 0000000..8f113d4 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java @@ -0,0 +1,16 @@ +package com.example.userservice.repository; + +import com.example.userservice.entity.Favorite; +import com.example.userservice.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface FavoriteRepository extends JpaRepository { + List findAllByMember_UserId(String userId); + + Optional findByMemberAndTypeAndPlaceId(Member member, String type, String id); +} diff --git a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java new file mode 100644 index 0000000..51b8e9b --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java @@ -0,0 +1,84 @@ +package com.example.userservice.service; + +import com.example.userservice.dto.FavoriteDto; +import com.example.userservice.entity.Favorite; +import com.example.userservice.entity.Member; +import com.example.userservice.repository.FavoriteRepository; +import com.example.userservice.repository.jpa.MemberRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FavoriteService { + + private final FavoriteRepository favoriteRepository; + private final MemberRepository memberRepository; + + public List getListData(String userId) { + try{ + List items = favoriteRepository.findAllByMember_UserId(userId); + return items.stream() + .map(item -> new FavoriteDto( + item.getFavoriteId(), + item.getType(), + item.getPlaceId(), + userId + )).toList(); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public FavoriteDto addFavoriteData(String userId, FavoriteDto favoriteDto) { + + // 1. 사용자 조회 + Member member = memberRepository.findByUserId(userId) + .orElseThrow(() -> new RuntimeException("사용자 없음")); + + // 2. Favorite 엔티티 생성 + Favorite favorite = new Favorite(); + favorite.setType(favoriteDto.getType()); + favorite.setPlaceId(favoriteDto.getPlace_id()); // id는 placeId 역할 + favorite.setMember(member); + + // 3. 저장 + Favorite saved = favoriteRepository.save(favorite); + + // 4. DTO로 변환 후 반환 + FavoriteDto result = new FavoriteDto(saved.getFavoriteId(),member.getUserId(), saved.getType(), saved.getPlaceId()); + + return result; + } + + @Transactional + public void deleteFavorite(String userId, String type, String id) { + Member member = memberRepository.findByUserId(userId) + .orElseThrow(() -> new RuntimeException("사용자 없음")); + + Favorite favorite = favoriteRepository.findByMemberAndTypeAndPlaceId(member, type, id) + .orElseThrow(() -> new RuntimeException("즐겨찾기 없음")); + + favoriteRepository.delete(favorite); + } + + public List getAllFavoriteData() { + try{ + List items = favoriteRepository.findAll(); + return items.stream() + .map(item -> new FavoriteDto( + item.getFavoriteId(), + item.getType(), + item.getPlaceId(), + item.getMember().getUserId() + )).toList(); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} From c13767f9af774a92bb27c5ddaadfb45c671a2db5 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 16:14:08 +0900 Subject: [PATCH 202/361] =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=ED=86=B5=ED=95=A9=20=ED=9B=84=20CI/CD=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- congestion-service/.gitignore | 1 + congestion-service/Dockerfile | 44 +++ .../src/main/resources/application.properties | 16 - external-info-service/.gitignore | 1 + external-info-service/Dockerfile | 44 +++ .../ExternalInfoServiceApplication.java | 10 - .../service/ParkServiceOriginal.java | 52 --- .../service/WeatherServiceOriginal.java | 301 ------------------ gateway/.gitignore | 1 + gateway/Dockerfile | 44 +++ place-service/.gitignore | 1 + place-service/Dockerfile | 64 +++- .../placeservice/entity/CulturalEvent.java | 4 +- .../src/main/resources/application.properties | 23 -- user-service/.gitignore | 1 + .../userservice/config/ApplicationConfig.java | 1 - .../src/main/resources/application.properties | 19 -- 17 files changed, 196 insertions(+), 431 deletions(-) create mode 100644 congestion-service/Dockerfile delete mode 100644 congestion-service/src/main/resources/application.properties create mode 100644 external-info-service/Dockerfile delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/ParkServiceOriginal.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java create mode 100644 gateway/Dockerfile delete mode 100644 place-service/src/main/resources/application.properties delete mode 100644 user-service/src/main/resources/application.properties diff --git a/congestion-service/.gitignore b/congestion-service/.gitignore index 55608a8..af391df 100644 --- a/congestion-service/.gitignore +++ b/congestion-service/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ .env +src/main/resources/application.properties ### STS ### .apt_generated diff --git a/congestion-service/Dockerfile b/congestion-service/Dockerfile new file mode 100644 index 0000000..cb414e8 --- /dev/null +++ b/congestion-service/Dockerfile @@ -0,0 +1,44 @@ +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 +WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + +COPY . . + +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 +WORKDIR /app + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./congestion.jar +# 포트지정 + +# env 파일 추가 +COPY .env .env + +EXPOSE 8082 + +# 서버가동 +CMD ["java", "-jar", "congestion.jar"] \ No newline at end of file diff --git a/congestion-service/src/main/resources/application.properties b/congestion-service/src/main/resources/application.properties deleted file mode 100644 index 0719530..0000000 --- a/congestion-service/src/main/resources/application.properties +++ /dev/null @@ -1,16 +0,0 @@ -server.port=8081 -spring.application.name=congestion-service - -# postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db -spring.datasource.username=root -spring.datasource.password=admin -spring.datasource.driver-class-name=org.postgresql.Driver - -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update - -spring.jpa.defer-datasource-initialization=true -# Hibernate SQL ?? ??? -spring.jpa.show-sql=true -spring.sql.init.mode=always \ No newline at end of file diff --git a/external-info-service/.gitignore b/external-info-service/.gitignore index c2065bc..8aca0b7 100644 --- a/external-info-service/.gitignore +++ b/external-info-service/.gitignore @@ -4,6 +4,7 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +src/main/resources/application.properties ### STS ### .apt_generated diff --git a/external-info-service/Dockerfile b/external-info-service/Dockerfile new file mode 100644 index 0000000..e0f6066 --- /dev/null +++ b/external-info-service/Dockerfile @@ -0,0 +1,44 @@ +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 +WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + +COPY . . + +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 +WORKDIR /app + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./external.jar +# 포트지정 + +# env 파일 추가 +COPY .env .env + +EXPOSE 8081 + +# 서버가동 +CMD ["java", "-jar", "external.jar"] \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java index 8514b59..b077f1c 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java @@ -2,13 +2,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -<<<<<<< HEAD - -@SpringBootApplication -public class ExternalInfoServiceApplication { - - public static void main(String[] args) { -======= import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @@ -16,9 +9,6 @@ public static void main(String[] args) { public class ExternalInfoServiceApplication { public static void main(String[] args) { - - ->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793 SpringApplication.run(ExternalInfoServiceApplication.class, args); } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkServiceOriginal.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkServiceOriginal.java deleted file mode 100644 index 3ee0688..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkServiceOriginal.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.externalinfoservice.service; - -import com.example.externalinfoservice.dto.ParkDto; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.Unmarshaller; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.io.StringReader; - -@Service -public class ParkServiceOriginal { - - @Value("${seoul.api.key}") - private String apiKey; - - private final RestTemplate restTemplate; - - public ParkServiceOriginal(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } - - public String getParkingData(String areaId) { - try { - String url = String.format( - "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", - apiKey, areaId - ); - - // XML 요청 - String xmlResponse = restTemplate.getForObject(url, String.class); - - // XML -> DTO - ParkDto parkDto = xmlToDto(xmlResponse); - - // DTO -> JSON - ObjectMapper objectMapper = new ObjectMapper(); - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(parkDto); - - } catch (Exception e) { - throw new RuntimeException("주차장 데이터 파싱 실패: " + e.getMessage(), e); - } - } - - private ParkDto xmlToDto(String xml) throws Exception { - JAXBContext context = JAXBContext.newInstance(ParkDto.class); - Unmarshaller unmarshaller = context.createUnmarshaller(); - return (ParkDto) unmarshaller.unmarshal(new StringReader(xml)); - } -} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java deleted file mode 100644 index 2b29623..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java +++ /dev/null @@ -1,301 +0,0 @@ -package com.example.externalinfoservice.service; - -import com.example.externalinfoservice.entity.Area; -import com.example.externalinfoservice.repository.AreaRepository; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import java.io.ByteArrayInputStream; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.stream.Collectors; - -@Service -public class WeatherServiceOriginal { - - @Value("${seoul.api.key}") - private String apiKey; - - private final RestTemplate restTemplate; - private final ObjectMapper objectMapper; - private final AreaRepository areaRepository; - - // 날씨 상태별 아이콘 URL 매핑 - private static final Map WEATHER_ICONS = new HashMap<>() {{ - put("구름 조금", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_halfsun.png"); - put("비", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_rain.png"); - put("눈", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_snow.png"); - put("구름 많음", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_cloudy.png"); - put("맑음", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sunny.png"); - }}; - - public WeatherServiceOriginal(RestTemplate restTemplate, - ObjectMapper objectMapper, - AreaRepository areaRepository) { - this.restTemplate = restTemplate; - this.objectMapper = objectMapper; - this.areaRepository = areaRepository; - } - - /** - * 특정 지역의 상세 날씨 정보를 제공합니다. - */ - public String getWeatherData(String placeCode) { - try { - // place-service 데이터베이스에서 지역 정보 조회 - Area area = areaRepository.findBySeoulId(placeCode) - .orElseThrow(() -> new RuntimeException("해당 지역 코드를 찾을 수 없습니다: " + placeCode)); - - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String apiUrl = String.format( - "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", - apiKey, placeCode - ); - String xmlResponse = restTemplate.getForObject(apiUrl, String.class); - - // XML 파싱을 위한 DocumentBuilder 생성 - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes())); - - // JSON 객체 생성 - ObjectNode weatherJson = objectMapper.createObjectNode(); - ObjectNode weatherSttsJson = objectMapper.createObjectNode(); - - // 지역 추가 정보 - weatherJson.put("areaName", area.getName()); - weatherJson.put("areaNameEng", area.getNameEng()); - weatherJson.put("category", area.getCategory()); - - // 위도, 경도 정보 추가 (선택적) - if (area.getLatitude() != null && area.getLongitude() != null) { - ObjectNode locationJson = objectMapper.createObjectNode(); - locationJson.put("latitude", area.getLatitude()); - locationJson.put("longitude", area.getLongitude()); - weatherJson.set("location", locationJson); - } - - // 원하는 필드들 - String[] desiredFields = { - "TEMP", "PRECIPITATION", "PRECPT_TYPE", - "PM10", "FCST24HOURS", "SKY_STTS" - }; - - // SKY_STTS 값을 저장할 변수 - String skyStatus = ""; - - // 각 필드별로 특별 처리 - for (String field : desiredFields) { - NodeList fieldNodes = document.getElementsByTagName(field); - - // 각 필드에 대해 값 추출 - if (fieldNodes.getLength() > 0) { - Node fieldNode = fieldNodes.item(0); - String nodeValue = fieldNode.getTextContent().trim(); - - if (field.equals("FCST24HOURS")) { - // FCST24HOURS는 배열로 처리 - ArrayNode forecastArray = objectMapper.createArrayNode(); - NodeList fcstChildren = fieldNode.getChildNodes(); - - for (int j = 0; j < fcstChildren.getLength(); j++) { - Node fcstChild = fcstChildren.item(j); - if (fcstChild.getNodeType() == Node.ELEMENT_NODE) { - Element fcstElement = (Element) fcstChild; - ObjectNode forecastItem = objectMapper.createObjectNode(); - NodeList itemChildren = fcstElement.getChildNodes(); - - for (int k = 0; k < itemChildren.getLength(); k++) { - Node itemChild = itemChildren.item(k); - if (itemChild.getNodeType() == Node.ELEMENT_NODE) { - Element itemElement = (Element) itemChild; - String itemNodeName = itemElement.getNodeName(); - String itemNodeValue = itemElement.getTextContent().trim(); - - if (!itemNodeValue.isEmpty()) { - forecastItem.put(convertToCamelCase(itemNodeName), itemNodeValue); - } - } - } - - if (!forecastItem.isEmpty()) { - forecastArray.add(forecastItem); - } - } - } - - weatherSttsJson.set(convertToCamelCase(field), forecastArray); - } else if (field.equals("SKY_STTS")) { - // SKY_STTS 값 저장 - skyStatus = nodeValue; - weatherSttsJson.put(convertToCamelCase(field), nodeValue); - } else if (!nodeValue.isEmpty()) { - weatherSttsJson.put(convertToCamelCase(field), nodeValue); - } - } - } - - // 날씨 상태(SKY_STTS)에 따라 아이콘 URL 매핑 - String iconUrl = WEATHER_ICONS.getOrDefault(skyStatus, "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); - - // 매핑된 아이콘 URL 추가 - weatherSttsJson.put("weatherIcon", iconUrl); - - // weatherStts 필드에 추출된 정보 저장 - weatherJson.set("weatherStts", weatherSttsJson); - - // JSON 변환 및 반환 - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(weatherJson); - - } catch (Exception e) { - e.printStackTrace(); // 전체 스택 트레이스 출력 - throw new RuntimeException("날씨 데이터 변환 실패", e); - } - } - - /** - * 모든 지역의 날씨 요약 정보를 JSON 형태로 반환합니다. - */ - public String getAllAreasWeatherSummary() { - try { - // 병렬 처리를 위한 스레드 풀 생성 - ExecutorService executor = Executors.newFixedThreadPool(5); - - // 결과를 담을 객체 생성 - ObjectNode resultJson = objectMapper.createObjectNode(); - resultJson.put("title", "서울시 주요 지역 날씨 정보"); - - // 전체 지역 날씨 정보를 담을 배열 생성 - ArrayNode areasWeather = objectMapper.createArrayNode(); - - // place-service 데이터베이스에서 모든 지역 정보 조회 - List areas = areaRepository.findAll(); - - // 모든 지역에 대해 CompletableFuture를 생성하여 병렬로 API 호출 - List> futures = areas.stream() - .map(area -> CompletableFuture.supplyAsync( - () -> getAreaWeatherSummary(area.getName(), area.getSeoulId()), - executor - )) - .collect(Collectors.toList()); - - // 모든 CompletableFuture가 완료될 때까지 기다리고 결과 수집 - CompletableFuture allFutures = CompletableFuture.allOf( - futures.toArray(new CompletableFuture[0])); - - // 결과 수집 및 JSON 배열에 추가 - allFutures.thenAccept(v -> - futures.forEach(future -> { - try { - ObjectNode node = future.get(); - areasWeather.add(node); - } catch (Exception e) { - // 개별 결과 처리 중 오류 발생 시 무시하고 계속 진행 - } - }) - ).get(); - - // 스레드 풀 종료 - executor.shutdown(); - - // 결과 JSON에 지역 날씨 정보 배열 추가 - resultJson.set("data", areasWeather); - - // JSON 변환 및 반환 - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(resultJson); - - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException("지역별 날씨 정보 조회 실패", e); - } - } - - /** - * 지역의 간략한 날씨 정보를 추출합니다. - */ - private ObjectNode getAreaWeatherSummary(String areaName, String areaCode) { - try { - // 날씨 정보 조회 API 호출 - String apiUrl = String.format( - "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", - apiKey, areaCode - ); - String xmlResponse = restTemplate.getForObject(apiUrl, String.class); - - // XML 파싱 - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes())); - - // 필요한 정보 추출 - String temperature = getNodeValueByTagName(document, "TEMP"); - String skyStatus = getNodeValueByTagName(document, "SKY_STTS"); - String precipitationType = getNodeValueByTagName(document, "PRECPT_TYPE"); - - // 날씨 아이콘 매핑 - String iconUrl = WEATHER_ICONS.getOrDefault(skyStatus, - "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); - - // JSON 객체 생성 (간소화된 형식) - ObjectNode areaWeather = objectMapper.createObjectNode(); - areaWeather.put("name", areaName); - areaWeather.put("TEMP", temperature); - areaWeather.put("PRECPT_TYPE", precipitationType); - areaWeather.put("weatherIcon", iconUrl); - - return areaWeather; - } catch (Exception e) { - // 오류 발생 시 기본 정보 반환 - ObjectNode defaultNode = objectMapper.createObjectNode(); - defaultNode.put("name", areaName); - defaultNode.put("TEMP", "N/A"); - defaultNode.put("PRECPT_TYPE", "정보 없음"); - defaultNode.put("weatherIcon", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); - return defaultNode; - } - } - - /** - * 문서에서 태그명으로 노드 값을 가져옵니다. - */ - private String getNodeValueByTagName(Document document, String tagName) { - NodeList nodes = document.getElementsByTagName(tagName); - if (nodes.getLength() > 0) { - return nodes.item(0).getTextContent().trim(); - } - return ""; - } - - // 언더스코어 이름을 camelCase로 변환 - private String convertToCamelCase(String input) { - if (input == null || input.isEmpty()) return input; - - StringBuilder result = new StringBuilder(); - boolean capitalizeNext = false; - - for (char c : input.toLowerCase().toCharArray()) { - if (c == '_') { - capitalizeNext = true; - } else if (capitalizeNext) { - result.append(Character.toUpperCase(c)); - capitalizeNext = false; - } else { - result.append(c); - } - } - - return result.toString(); - } -} \ No newline at end of file diff --git a/gateway/.gitignore b/gateway/.gitignore index c2065bc..8aca0b7 100644 --- a/gateway/.gitignore +++ b/gateway/.gitignore @@ -4,6 +4,7 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +src/main/resources/application.properties ### STS ### .apt_generated diff --git a/gateway/Dockerfile b/gateway/Dockerfile new file mode 100644 index 0000000..568e28d --- /dev/null +++ b/gateway/Dockerfile @@ -0,0 +1,44 @@ +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 +WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + +COPY . . + +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 +WORKDIR /app + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./gateway.jar +# 포트지정 + +# env 파일 추가 +#COPY .env .env + +EXPOSE 8080 + +# 서버가동 +CMD ["java", "-jar", "gateway.jar"] \ No newline at end of file diff --git a/place-service/.gitignore b/place-service/.gitignore index 844c37d..8778a85 100644 --- a/place-service/.gitignore +++ b/place-service/.gitignore @@ -4,6 +4,7 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +src/main/resources/application.properties .env .dockerignore diff --git a/place-service/Dockerfile b/place-service/Dockerfile index 5508683..dfd3b8c 100644 --- a/place-service/Dockerfile +++ b/place-service/Dockerfile @@ -1,13 +1,63 @@ -# Gradle -FROM gradle:8.13.0-jdk17 AS build +## Gradle +#FROM gradle:8.13.0-jdk17 AS build +#WORKDIR /app +#COPY . . +#RUN gradle build -x test +# +#FROM eclipse-temurin:17-jdk +#WORKDIR /app +#COPY --from=build /app/build/libs/*.jar app.jar +#EXPOSE 8080 +#ENTRYPOINT ["java", "-jar", "app.jar"] +# +#ARG TOUR_API_KEY +# +#COPY .env .env + + +# 테스트용 + +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + COPY . . -RUN gradle build -x test -FROM eclipse-temurin:17-jdk +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 WORKDIR /app -COPY --from=build /app/build/libs/*.jar app.jar + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./place.jar +# 포트지정 + +# env 파일 추가 +COPY .env .env + EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "app.jar"] -COPY .env .env \ No newline at end of file +# 서버가동 +CMD ["java", "-jar", "place.jar"] \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java index 4af7f5c..e6be07f 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java +++ b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; +import lombok.Data; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -10,8 +11,7 @@ import java.time.LocalDateTime; @Entity -@Getter -@Setter +@Data @NoArgsConstructor public class CulturalEvent { diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties deleted file mode 100644 index a85e46c..0000000 --- a/place-service/src/main/resources/application.properties +++ /dev/null @@ -1,23 +0,0 @@ -server.port=8082 -spring.application.name=place-service - -spring.config.import=optional:file:.env[.properties] -# postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db -spring.datasource.username=root -spring.datasource.password=admin -spring.datasource.driver-class-name=org.postgresql.Driver - -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.defer-datasource-initialization=true -spring.jpa.hibernate.ddl-auto=update -# spring.jpa.hibernate.ddl-auto=create -spring.jpa.show-sql=true - -#spring.sql.init.mode=always - -#API -kakao.api.key=${KAKAO_API_KEY} - -# ?? ?? -logging.level.com.example.placeservice=INFO \ No newline at end of file diff --git a/user-service/.gitignore b/user-service/.gitignore index 261a098..6964208 100644 --- a/user-service/.gitignore +++ b/user-service/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build .env +src/main/resources/application.properties ### STS ### .apt_generated diff --git a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java index 44f2998..27f8287 100644 --- a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java @@ -15,7 +15,6 @@ import org.springframework.security.crypto.password.PasswordEncoder; import java.util.ArrayList; -import java.util.Collections; import java.util.List; @Configuration diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties deleted file mode 100644 index 2757797..0000000 --- a/user-service/src/main/resources/application.properties +++ /dev/null @@ -1,19 +0,0 @@ -spring.application.name=user-service - -server.port=8083 - -spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db -spring.datasource.username=root -spring.datasource.password=admin -spring.datasource.driver-class-name=org.postgresql.Driver -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update - -spring.data.redis.host=localhost -spring.data.redis.port=6379 -spring.data.redis.password=1234 - - -jwt.secret=${JWT_SECRET_KEY} -jwt.expiration=86400000 -jwt.refresh-expiration=2592000000 \ No newline at end of file From f7641339e1f5f96abd0f7fc80bd3cd384092520a Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 16:14:08 +0900 Subject: [PATCH 203/361] =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=ED=86=B5=ED=95=A9=20=ED=9B=84=20CI/CD=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- congestion-service/.gitignore | 1 + congestion-service/Dockerfile | 44 +++ .../src/main/resources/application.properties | 16 - external-info-service/.gitignore | 1 + external-info-service/Dockerfile | 44 +++ .../ExternalInfoServiceApplication.java | 10 - .../service/ParkServiceOriginal.java | 52 --- .../service/WeatherServiceOriginal.java | 301 ------------------ .../src/main/resources/application.properties | 7 +- gateway/.gitignore | 1 + gateway/Dockerfile | 44 +++ place-service/.gitignore | 1 + place-service/Dockerfile | 64 +++- .../placeservice/entity/CulturalEvent.java | 4 +- .../src/main/resources/application.properties | 23 -- user-service/.gitignore | 1 + .../userservice/config/ApplicationConfig.java | 1 - .../src/main/resources/application.properties | 19 -- 18 files changed, 197 insertions(+), 437 deletions(-) create mode 100644 congestion-service/Dockerfile delete mode 100644 congestion-service/src/main/resources/application.properties create mode 100644 external-info-service/Dockerfile delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/ParkServiceOriginal.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java create mode 100644 gateway/Dockerfile delete mode 100644 place-service/src/main/resources/application.properties delete mode 100644 user-service/src/main/resources/application.properties diff --git a/congestion-service/.gitignore b/congestion-service/.gitignore index 55608a8..af391df 100644 --- a/congestion-service/.gitignore +++ b/congestion-service/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ .env +src/main/resources/application.properties ### STS ### .apt_generated diff --git a/congestion-service/Dockerfile b/congestion-service/Dockerfile new file mode 100644 index 0000000..cb414e8 --- /dev/null +++ b/congestion-service/Dockerfile @@ -0,0 +1,44 @@ +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 +WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + +COPY . . + +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 +WORKDIR /app + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./congestion.jar +# 포트지정 + +# env 파일 추가 +COPY .env .env + +EXPOSE 8082 + +# 서버가동 +CMD ["java", "-jar", "congestion.jar"] \ No newline at end of file diff --git a/congestion-service/src/main/resources/application.properties b/congestion-service/src/main/resources/application.properties deleted file mode 100644 index 0719530..0000000 --- a/congestion-service/src/main/resources/application.properties +++ /dev/null @@ -1,16 +0,0 @@ -server.port=8081 -spring.application.name=congestion-service - -# postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db -spring.datasource.username=root -spring.datasource.password=admin -spring.datasource.driver-class-name=org.postgresql.Driver - -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update - -spring.jpa.defer-datasource-initialization=true -# Hibernate SQL ?? ??? -spring.jpa.show-sql=true -spring.sql.init.mode=always \ No newline at end of file diff --git a/external-info-service/.gitignore b/external-info-service/.gitignore index c2065bc..8aca0b7 100644 --- a/external-info-service/.gitignore +++ b/external-info-service/.gitignore @@ -4,6 +4,7 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +src/main/resources/application.properties ### STS ### .apt_generated diff --git a/external-info-service/Dockerfile b/external-info-service/Dockerfile new file mode 100644 index 0000000..e0f6066 --- /dev/null +++ b/external-info-service/Dockerfile @@ -0,0 +1,44 @@ +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 +WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + +COPY . . + +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 +WORKDIR /app + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./external.jar +# 포트지정 + +# env 파일 추가 +COPY .env .env + +EXPOSE 8081 + +# 서버가동 +CMD ["java", "-jar", "external.jar"] \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java index 8514b59..b077f1c 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java @@ -2,13 +2,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -<<<<<<< HEAD - -@SpringBootApplication -public class ExternalInfoServiceApplication { - - public static void main(String[] args) { -======= import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @@ -16,9 +9,6 @@ public static void main(String[] args) { public class ExternalInfoServiceApplication { public static void main(String[] args) { - - ->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793 SpringApplication.run(ExternalInfoServiceApplication.class, args); } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkServiceOriginal.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkServiceOriginal.java deleted file mode 100644 index 3ee0688..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkServiceOriginal.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.externalinfoservice.service; - -import com.example.externalinfoservice.dto.ParkDto; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.xml.bind.JAXBContext; -import jakarta.xml.bind.Unmarshaller; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.io.StringReader; - -@Service -public class ParkServiceOriginal { - - @Value("${seoul.api.key}") - private String apiKey; - - private final RestTemplate restTemplate; - - public ParkServiceOriginal(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } - - public String getParkingData(String areaId) { - try { - String url = String.format( - "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", - apiKey, areaId - ); - - // XML 요청 - String xmlResponse = restTemplate.getForObject(url, String.class); - - // XML -> DTO - ParkDto parkDto = xmlToDto(xmlResponse); - - // DTO -> JSON - ObjectMapper objectMapper = new ObjectMapper(); - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(parkDto); - - } catch (Exception e) { - throw new RuntimeException("주차장 데이터 파싱 실패: " + e.getMessage(), e); - } - } - - private ParkDto xmlToDto(String xml) throws Exception { - JAXBContext context = JAXBContext.newInstance(ParkDto.class); - Unmarshaller unmarshaller = context.createUnmarshaller(); - return (ParkDto) unmarshaller.unmarshal(new StringReader(xml)); - } -} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java deleted file mode 100644 index 2b29623..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherServiceOriginal.java +++ /dev/null @@ -1,301 +0,0 @@ -package com.example.externalinfoservice.service; - -import com.example.externalinfoservice.entity.Area; -import com.example.externalinfoservice.repository.AreaRepository; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import java.io.ByteArrayInputStream; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.stream.Collectors; - -@Service -public class WeatherServiceOriginal { - - @Value("${seoul.api.key}") - private String apiKey; - - private final RestTemplate restTemplate; - private final ObjectMapper objectMapper; - private final AreaRepository areaRepository; - - // 날씨 상태별 아이콘 URL 매핑 - private static final Map WEATHER_ICONS = new HashMap<>() {{ - put("구름 조금", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_halfsun.png"); - put("비", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_rain.png"); - put("눈", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_snow.png"); - put("구름 많음", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_cloudy.png"); - put("맑음", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sunny.png"); - }}; - - public WeatherServiceOriginal(RestTemplate restTemplate, - ObjectMapper objectMapper, - AreaRepository areaRepository) { - this.restTemplate = restTemplate; - this.objectMapper = objectMapper; - this.areaRepository = areaRepository; - } - - /** - * 특정 지역의 상세 날씨 정보를 제공합니다. - */ - public String getWeatherData(String placeCode) { - try { - // place-service 데이터베이스에서 지역 정보 조회 - Area area = areaRepository.findBySeoulId(placeCode) - .orElseThrow(() -> new RuntimeException("해당 지역 코드를 찾을 수 없습니다: " + placeCode)); - - // 1. 외부 OpenAPI에서 XML 데이터 받아오기 - String apiUrl = String.format( - "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", - apiKey, placeCode - ); - String xmlResponse = restTemplate.getForObject(apiUrl, String.class); - - // XML 파싱을 위한 DocumentBuilder 생성 - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes())); - - // JSON 객체 생성 - ObjectNode weatherJson = objectMapper.createObjectNode(); - ObjectNode weatherSttsJson = objectMapper.createObjectNode(); - - // 지역 추가 정보 - weatherJson.put("areaName", area.getName()); - weatherJson.put("areaNameEng", area.getNameEng()); - weatherJson.put("category", area.getCategory()); - - // 위도, 경도 정보 추가 (선택적) - if (area.getLatitude() != null && area.getLongitude() != null) { - ObjectNode locationJson = objectMapper.createObjectNode(); - locationJson.put("latitude", area.getLatitude()); - locationJson.put("longitude", area.getLongitude()); - weatherJson.set("location", locationJson); - } - - // 원하는 필드들 - String[] desiredFields = { - "TEMP", "PRECIPITATION", "PRECPT_TYPE", - "PM10", "FCST24HOURS", "SKY_STTS" - }; - - // SKY_STTS 값을 저장할 변수 - String skyStatus = ""; - - // 각 필드별로 특별 처리 - for (String field : desiredFields) { - NodeList fieldNodes = document.getElementsByTagName(field); - - // 각 필드에 대해 값 추출 - if (fieldNodes.getLength() > 0) { - Node fieldNode = fieldNodes.item(0); - String nodeValue = fieldNode.getTextContent().trim(); - - if (field.equals("FCST24HOURS")) { - // FCST24HOURS는 배열로 처리 - ArrayNode forecastArray = objectMapper.createArrayNode(); - NodeList fcstChildren = fieldNode.getChildNodes(); - - for (int j = 0; j < fcstChildren.getLength(); j++) { - Node fcstChild = fcstChildren.item(j); - if (fcstChild.getNodeType() == Node.ELEMENT_NODE) { - Element fcstElement = (Element) fcstChild; - ObjectNode forecastItem = objectMapper.createObjectNode(); - NodeList itemChildren = fcstElement.getChildNodes(); - - for (int k = 0; k < itemChildren.getLength(); k++) { - Node itemChild = itemChildren.item(k); - if (itemChild.getNodeType() == Node.ELEMENT_NODE) { - Element itemElement = (Element) itemChild; - String itemNodeName = itemElement.getNodeName(); - String itemNodeValue = itemElement.getTextContent().trim(); - - if (!itemNodeValue.isEmpty()) { - forecastItem.put(convertToCamelCase(itemNodeName), itemNodeValue); - } - } - } - - if (!forecastItem.isEmpty()) { - forecastArray.add(forecastItem); - } - } - } - - weatherSttsJson.set(convertToCamelCase(field), forecastArray); - } else if (field.equals("SKY_STTS")) { - // SKY_STTS 값 저장 - skyStatus = nodeValue; - weatherSttsJson.put(convertToCamelCase(field), nodeValue); - } else if (!nodeValue.isEmpty()) { - weatherSttsJson.put(convertToCamelCase(field), nodeValue); - } - } - } - - // 날씨 상태(SKY_STTS)에 따라 아이콘 URL 매핑 - String iconUrl = WEATHER_ICONS.getOrDefault(skyStatus, "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); - - // 매핑된 아이콘 URL 추가 - weatherSttsJson.put("weatherIcon", iconUrl); - - // weatherStts 필드에 추출된 정보 저장 - weatherJson.set("weatherStts", weatherSttsJson); - - // JSON 변환 및 반환 - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(weatherJson); - - } catch (Exception e) { - e.printStackTrace(); // 전체 스택 트레이스 출력 - throw new RuntimeException("날씨 데이터 변환 실패", e); - } - } - - /** - * 모든 지역의 날씨 요약 정보를 JSON 형태로 반환합니다. - */ - public String getAllAreasWeatherSummary() { - try { - // 병렬 처리를 위한 스레드 풀 생성 - ExecutorService executor = Executors.newFixedThreadPool(5); - - // 결과를 담을 객체 생성 - ObjectNode resultJson = objectMapper.createObjectNode(); - resultJson.put("title", "서울시 주요 지역 날씨 정보"); - - // 전체 지역 날씨 정보를 담을 배열 생성 - ArrayNode areasWeather = objectMapper.createArrayNode(); - - // place-service 데이터베이스에서 모든 지역 정보 조회 - List areas = areaRepository.findAll(); - - // 모든 지역에 대해 CompletableFuture를 생성하여 병렬로 API 호출 - List> futures = areas.stream() - .map(area -> CompletableFuture.supplyAsync( - () -> getAreaWeatherSummary(area.getName(), area.getSeoulId()), - executor - )) - .collect(Collectors.toList()); - - // 모든 CompletableFuture가 완료될 때까지 기다리고 결과 수집 - CompletableFuture allFutures = CompletableFuture.allOf( - futures.toArray(new CompletableFuture[0])); - - // 결과 수집 및 JSON 배열에 추가 - allFutures.thenAccept(v -> - futures.forEach(future -> { - try { - ObjectNode node = future.get(); - areasWeather.add(node); - } catch (Exception e) { - // 개별 결과 처리 중 오류 발생 시 무시하고 계속 진행 - } - }) - ).get(); - - // 스레드 풀 종료 - executor.shutdown(); - - // 결과 JSON에 지역 날씨 정보 배열 추가 - resultJson.set("data", areasWeather); - - // JSON 변환 및 반환 - return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(resultJson); - - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException("지역별 날씨 정보 조회 실패", e); - } - } - - /** - * 지역의 간략한 날씨 정보를 추출합니다. - */ - private ObjectNode getAreaWeatherSummary(String areaName, String areaCode) { - try { - // 날씨 정보 조회 API 호출 - String apiUrl = String.format( - "http://openapi.seoul.go.kr:8088/%s/xml/citydata/1/5/%s", - apiKey, areaCode - ); - String xmlResponse = restTemplate.getForObject(apiUrl, String.class); - - // XML 파싱 - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(new ByteArrayInputStream(xmlResponse.getBytes())); - - // 필요한 정보 추출 - String temperature = getNodeValueByTagName(document, "TEMP"); - String skyStatus = getNodeValueByTagName(document, "SKY_STTS"); - String precipitationType = getNodeValueByTagName(document, "PRECPT_TYPE"); - - // 날씨 아이콘 매핑 - String iconUrl = WEATHER_ICONS.getOrDefault(skyStatus, - "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); - - // JSON 객체 생성 (간소화된 형식) - ObjectNode areaWeather = objectMapper.createObjectNode(); - areaWeather.put("name", areaName); - areaWeather.put("TEMP", temperature); - areaWeather.put("PRECPT_TYPE", precipitationType); - areaWeather.put("weatherIcon", iconUrl); - - return areaWeather; - } catch (Exception e) { - // 오류 발생 시 기본 정보 반환 - ObjectNode defaultNode = objectMapper.createObjectNode(); - defaultNode.put("name", areaName); - defaultNode.put("TEMP", "N/A"); - defaultNode.put("PRECPT_TYPE", "정보 없음"); - defaultNode.put("weatherIcon", "https://data.seoul.go.kr/SeoulRtd/images/icon/weather/ico_w_sun.png"); - return defaultNode; - } - } - - /** - * 문서에서 태그명으로 노드 값을 가져옵니다. - */ - private String getNodeValueByTagName(Document document, String tagName) { - NodeList nodes = document.getElementsByTagName(tagName); - if (nodes.getLength() > 0) { - return nodes.item(0).getTextContent().trim(); - } - return ""; - } - - // 언더스코어 이름을 camelCase로 변환 - private String convertToCamelCase(String input) { - if (input == null || input.isEmpty()) return input; - - StringBuilder result = new StringBuilder(); - boolean capitalizeNext = false; - - for (char c : input.toLowerCase().toCharArray()) { - if (c == '_') { - capitalizeNext = true; - } else if (capitalizeNext) { - result.append(Character.toUpperCase(c)); - capitalizeNext = false; - } else { - result.append(c); - } - } - - return result.toString(); - } -} \ No newline at end of file diff --git a/external-info-service/src/main/resources/application.properties b/external-info-service/src/main/resources/application.properties index d17219a..3d993c7 100644 --- a/external-info-service/src/main/resources/application.properties +++ b/external-info-service/src/main/resources/application.properties @@ -1,6 +1,5 @@ spring.application.name=external-info-service -<<<<<<< HEAD -======= + server.port=8081 # postgreSQL ?? @@ -16,7 +15,3 @@ spring.elasticsearch.uris= http://elasticsearch.seoultravel.life:9200 spring.mvc.contentnegotiation.favor-path-extension=false spring.mvc.contentnegotiation.media-types.json=application/json spring.mvc.default-response-type=application/json - - -seoul.api.key=505a6255746b696d34344b46566174 ->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793 diff --git a/gateway/.gitignore b/gateway/.gitignore index c2065bc..8aca0b7 100644 --- a/gateway/.gitignore +++ b/gateway/.gitignore @@ -4,6 +4,7 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +src/main/resources/application.properties ### STS ### .apt_generated diff --git a/gateway/Dockerfile b/gateway/Dockerfile new file mode 100644 index 0000000..568e28d --- /dev/null +++ b/gateway/Dockerfile @@ -0,0 +1,44 @@ +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 +WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + +COPY . . + +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 +WORKDIR /app + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./gateway.jar +# 포트지정 + +# env 파일 추가 +#COPY .env .env + +EXPOSE 8080 + +# 서버가동 +CMD ["java", "-jar", "gateway.jar"] \ No newline at end of file diff --git a/place-service/.gitignore b/place-service/.gitignore index 844c37d..8778a85 100644 --- a/place-service/.gitignore +++ b/place-service/.gitignore @@ -4,6 +4,7 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +src/main/resources/application.properties .env .dockerignore diff --git a/place-service/Dockerfile b/place-service/Dockerfile index 5508683..dfd3b8c 100644 --- a/place-service/Dockerfile +++ b/place-service/Dockerfile @@ -1,13 +1,63 @@ -# Gradle -FROM gradle:8.13.0-jdk17 AS build +## Gradle +#FROM gradle:8.13.0-jdk17 AS build +#WORKDIR /app +#COPY . . +#RUN gradle build -x test +# +#FROM eclipse-temurin:17-jdk +#WORKDIR /app +#COPY --from=build /app/build/libs/*.jar app.jar +#EXPOSE 8080 +#ENTRYPOINT ["java", "-jar", "app.jar"] +# +#ARG TOUR_API_KEY +# +#COPY .env .env + + +# 테스트용 + +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + COPY . . -RUN gradle build -x test -FROM eclipse-temurin:17-jdk +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 WORKDIR /app -COPY --from=build /app/build/libs/*.jar app.jar + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./place.jar +# 포트지정 + +# env 파일 추가 +COPY .env .env + EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "app.jar"] -COPY .env .env \ No newline at end of file +# 서버가동 +CMD ["java", "-jar", "place.jar"] \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java index 4af7f5c..e6be07f 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java +++ b/place-service/src/main/java/com/example/placeservice/entity/CulturalEvent.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; +import lombok.Data; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -10,8 +11,7 @@ import java.time.LocalDateTime; @Entity -@Getter -@Setter +@Data @NoArgsConstructor public class CulturalEvent { diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties deleted file mode 100644 index a85e46c..0000000 --- a/place-service/src/main/resources/application.properties +++ /dev/null @@ -1,23 +0,0 @@ -server.port=8082 -spring.application.name=place-service - -spring.config.import=optional:file:.env[.properties] -# postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db -spring.datasource.username=root -spring.datasource.password=admin -spring.datasource.driver-class-name=org.postgresql.Driver - -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.defer-datasource-initialization=true -spring.jpa.hibernate.ddl-auto=update -# spring.jpa.hibernate.ddl-auto=create -spring.jpa.show-sql=true - -#spring.sql.init.mode=always - -#API -kakao.api.key=${KAKAO_API_KEY} - -# ?? ?? -logging.level.com.example.placeservice=INFO \ No newline at end of file diff --git a/user-service/.gitignore b/user-service/.gitignore index 261a098..6964208 100644 --- a/user-service/.gitignore +++ b/user-service/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build .env +src/main/resources/application.properties ### STS ### .apt_generated diff --git a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java index 44f2998..27f8287 100644 --- a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java @@ -15,7 +15,6 @@ import org.springframework.security.crypto.password.PasswordEncoder; import java.util.ArrayList; -import java.util.Collections; import java.util.List; @Configuration diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties deleted file mode 100644 index 2757797..0000000 --- a/user-service/src/main/resources/application.properties +++ /dev/null @@ -1,19 +0,0 @@ -spring.application.name=user-service - -server.port=8083 - -spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db -spring.datasource.username=root -spring.datasource.password=admin -spring.datasource.driver-class-name=org.postgresql.Driver -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update - -spring.data.redis.host=localhost -spring.data.redis.port=6379 -spring.data.redis.password=1234 - - -jwt.secret=${JWT_SECRET_KEY} -jwt.expiration=86400000 -jwt.refresh-expiration=2592000000 \ No newline at end of file From 57b12893b503a64a715a8f34e7d0b152bfd6288f Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 16:16:11 +0900 Subject: [PATCH 204/361] =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20CI/CD=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 127 +++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..b731272 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,127 @@ +name: Backend Upload ECR + +#on: +# workflow_dispatch: +# inputs: +# environment: +# description: '수동 배포' +# required: true +# default: 'all' +# type: choice +# options: +# - all +# - latest + +on: + push: + branches: [develop] + pull_request: + branches: [develop] + + +jobs: + upload-image: + runs-on: ubuntu-latest + env : + IMAGE_TAG: ${{ github.run_number }} + steps: +# - name: Set IMAGE_TAG based on input +# id: tag +# run: | +# if [ "${{ github.event.inputs.environment }}" = "all" ]; then +# echo "IMAGE_TAG=1.0.${{ github.run_number }}" >> $GITHUB_ENV +# else +# echo "IMAGE_TAG=latest" >> $GITHUB_ENV +# fi + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: 자바 설치 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: '17' + + - name: gateway 빌드 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + cd gateway + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/gateway:$IMAGE_TAG . + docker tag $ECR_REGISTRY/gateway:$IMAGE_TAG $ECR_REGISTRY/gateway:latest + + docker push $ECR_REGISTRY/gateway:$IMAGE_TAG + docker push $ECR_REGISTRY/gateway:latest + + - name: place-service 빌드 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + cd place-service + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/place-service:$IMAGE_TAG . + docker tag $ECR_REGISTRY/place-service:$IMAGE_TAG $ECR_REGISTRY/place-service:latest + + docker push $ECR_REGISTRY/place-service:$IMAGE_TAG + docker push $ECR_REGISTRY/place-service:latest + + - name: congestion-service 빌드 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + cd congestion-service + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/congestion-service:$IMAGE_TAG . + docker tag $ECR_REGISTRY/congestion-service:$IMAGE_TAG $ECR_REGISTRY/congestion-service:latest + + docker push $ECR_REGISTRY/congestion-service:$IMAGE_TAG + docker push $ECR_REGISTRY/congestion-service:latest + + - name: external-info-service 빌드 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + cd external-info-service + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/external-info-service:$IMAGE_TAG . + docker tag $ECR_REGISTRY/external-info-service:$IMAGE_TAG $ECR_REGISTRY/external-info-service:latest + + docker push $ECR_REGISTRY/external-info-service:$IMAGE_TAG + docker push $ECR_REGISTRY/external-info-service:latest + + + - name: kubeconfig 설정 + run: aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster + + - name: Deploy to EKS + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + kubectl set image deployment/external-info-service external-info-service=$ECR_REGISTRY/external-info-service:latest + kubectl rollout status deployment/external-info-service + + kubectl set image deployment/place-service place-service=$ECR_REGISTRY/place-service:latest + kubectl rollout status deployment/place-service + + kubectl set image deployment/congestion-service congestion-service=$ECR_REGISTRY/congestion-service:latest + kubectl rollout status deployment/congestion-service + + kubectl set image deployment/gateway gateway=$ECR_REGISTRY/gateway:latest + kubectl rollout status deployment/gateway \ No newline at end of file From e08fe9b87eb852ab01e3c225eccdbaa79ee8f4f1 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 16:16:11 +0900 Subject: [PATCH 205/361] =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20CI/CD=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 127 +++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..b731272 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,127 @@ +name: Backend Upload ECR + +#on: +# workflow_dispatch: +# inputs: +# environment: +# description: '수동 배포' +# required: true +# default: 'all' +# type: choice +# options: +# - all +# - latest + +on: + push: + branches: [develop] + pull_request: + branches: [develop] + + +jobs: + upload-image: + runs-on: ubuntu-latest + env : + IMAGE_TAG: ${{ github.run_number }} + steps: +# - name: Set IMAGE_TAG based on input +# id: tag +# run: | +# if [ "${{ github.event.inputs.environment }}" = "all" ]; then +# echo "IMAGE_TAG=1.0.${{ github.run_number }}" >> $GITHUB_ENV +# else +# echo "IMAGE_TAG=latest" >> $GITHUB_ENV +# fi + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: 자바 설치 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: '17' + + - name: gateway 빌드 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + cd gateway + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/gateway:$IMAGE_TAG . + docker tag $ECR_REGISTRY/gateway:$IMAGE_TAG $ECR_REGISTRY/gateway:latest + + docker push $ECR_REGISTRY/gateway:$IMAGE_TAG + docker push $ECR_REGISTRY/gateway:latest + + - name: place-service 빌드 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + cd place-service + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/place-service:$IMAGE_TAG . + docker tag $ECR_REGISTRY/place-service:$IMAGE_TAG $ECR_REGISTRY/place-service:latest + + docker push $ECR_REGISTRY/place-service:$IMAGE_TAG + docker push $ECR_REGISTRY/place-service:latest + + - name: congestion-service 빌드 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + cd congestion-service + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/congestion-service:$IMAGE_TAG . + docker tag $ECR_REGISTRY/congestion-service:$IMAGE_TAG $ECR_REGISTRY/congestion-service:latest + + docker push $ECR_REGISTRY/congestion-service:$IMAGE_TAG + docker push $ECR_REGISTRY/congestion-service:latest + + - name: external-info-service 빌드 + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + cd external-info-service + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/external-info-service:$IMAGE_TAG . + docker tag $ECR_REGISTRY/external-info-service:$IMAGE_TAG $ECR_REGISTRY/external-info-service:latest + + docker push $ECR_REGISTRY/external-info-service:$IMAGE_TAG + docker push $ECR_REGISTRY/external-info-service:latest + + + - name: kubeconfig 설정 + run: aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster + + - name: Deploy to EKS + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + kubectl set image deployment/external-info-service external-info-service=$ECR_REGISTRY/external-info-service:latest + kubectl rollout status deployment/external-info-service + + kubectl set image deployment/place-service place-service=$ECR_REGISTRY/place-service:latest + kubectl rollout status deployment/place-service + + kubectl set image deployment/congestion-service congestion-service=$ECR_REGISTRY/congestion-service:latest + kubectl rollout status deployment/congestion-service + + kubectl set image deployment/gateway gateway=$ECR_REGISTRY/gateway:latest + kubectl rollout status deployment/gateway \ No newline at end of file From 1a82863c6eedef34c1610d51ff82f757fcc5d058 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 16:28:30 +0900 Subject: [PATCH 206/361] =?UTF-8?q?gitignore=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- external-info-service/.gitignore | 1 + user-service/.gitignore | 1 + 2 files changed, 2 insertions(+) diff --git a/external-info-service/.gitignore b/external-info-service/.gitignore index 55608a8..af391df 100644 --- a/external-info-service/.gitignore +++ b/external-info-service/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ .env +src/main/resources/application.properties ### STS ### .apt_generated diff --git a/user-service/.gitignore b/user-service/.gitignore index 261a098..6964208 100644 --- a/user-service/.gitignore +++ b/user-service/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build .env +src/main/resources/application.properties ### STS ### .apt_generated From 648ee2b0ec04abc11ed8b7380f5ec198763c472d Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 16:28:30 +0900 Subject: [PATCH 207/361] =?UTF-8?q?gitignore=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- external-info-service/.gitignore | 1 + user-service/.gitignore | 1 + 2 files changed, 2 insertions(+) diff --git a/external-info-service/.gitignore b/external-info-service/.gitignore index 55608a8..af391df 100644 --- a/external-info-service/.gitignore +++ b/external-info-service/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ .env +src/main/resources/application.properties ### STS ### .apt_generated diff --git a/user-service/.gitignore b/user-service/.gitignore index 261a098..6964208 100644 --- a/user-service/.gitignore +++ b/user-service/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build .env +src/main/resources/application.properties ### STS ### .apt_generated From de0e4eccbf6cde62b46074368018fdeffce75169 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 16:46:20 +0900 Subject: [PATCH 208/361] =?UTF-8?q?deploy.yml=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 196 ++++++++++++++++++++++++----------- 1 file changed, 133 insertions(+), 63 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b731272..7895ce6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,17 +1,5 @@ name: Backend Upload ECR -#on: -# workflow_dispatch: -# inputs: -# environment: -# description: '수동 배포' -# required: true -# default: 'all' -# type: choice -# options: -# - all -# - latest - on: push: branches: [develop] @@ -20,20 +8,12 @@ on: jobs: - upload-image: + + external-info-service-build : runs-on: ubuntu-latest - env : - IMAGE_TAG: ${{ github.run_number }} + env: + IMAGE_TAG: 1.${{ github.run_number }} steps: -# - name: Set IMAGE_TAG based on input -# id: tag -# run: | -# if [ "${{ github.event.inputs.environment }}" = "all" ]; then -# echo "IMAGE_TAG=1.0.${{ github.run_number }}" >> $GITHUB_ENV -# else -# echo "IMAGE_TAG=latest" >> $GITHUB_ENV -# fi - - name: Checkout repo uses: actions/checkout@v4 @@ -54,22 +34,61 @@ jobs: distribution: 'temurin' java-version: '17' - - name: gateway 빌드 - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + - name: kubeconfig 설정 run: | - cd gateway + aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster + echo "Step output: ${{ steps.set-ecr.outputs.registry }}" + + - name: Build & Push external-info-service + env: # ecr 업로드 할 때 필요한 변수들 선언 + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # ecr 경로 + run: | + cd external-info-service + echo "ECR_REGISTRY is: [$ECR_REGISTRY]" echo "${{ secrets.BACKEND_ENV}}" > .env gradle bootJar -x test - docker build -t $ECR_REGISTRY/gateway:$IMAGE_TAG . - docker tag $ECR_REGISTRY/gateway:$IMAGE_TAG $ECR_REGISTRY/gateway:latest + docker build -t $ECR_REGISTRY/external-info-service:$IMAGE_TAG . + docker tag $ECR_REGISTRY/external-info-service:$IMAGE_TAG $ECR_REGISTRY/external-info-service:latest + + docker push $ECR_REGISTRY/external-info-service:$IMAGE_TAG + docker push $ECR_REGISTRY/external-info-service:latest + + kubectl set image deployment/external-info-service external-info-service=$ECR_REGISTRY/external-info-service:$IMAGE_TAG + kubectl rollout status deployment/external-info-service - docker push $ECR_REGISTRY/gateway:$IMAGE_TAG - docker push $ECR_REGISTRY/gateway:latest + place-service-build: + runs-on: ubuntu-latest + env: + IMAGE_TAG: 1.${{ github.run_number }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: 자바 설치 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: '17' + + - name: kubeconfig 설정 + run: | + aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster + echo "Step output: ${{ steps.set-ecr.outputs.registry }}" - - name: place-service 빌드 - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + - name: Build & Push place-service + env: # ecr 업로드 할 때 필요한 변수들 선언 + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # ecr 경로 run: | cd place-service echo "${{ secrets.BACKEND_ENV}}" > .env @@ -79,10 +98,44 @@ jobs: docker push $ECR_REGISTRY/place-service:$IMAGE_TAG docker push $ECR_REGISTRY/place-service:latest + + echo "태그 확인: place-service=$ECR_REGISTRY/place-service:$IMAGE_TAG" + + kubectl set image deployment/place-service place-service=$ECR_REGISTRY/place-service:$IMAGE_TAG + kubectl rollout status deployment/place-service + + congestion-service-build: + runs-on: ubuntu-latest + env: + IMAGE_TAG: 1.${{ github.run_number }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: 자바 설치 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: '17' - - name: congestion-service 빌드 - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + - name: kubeconfig 설정 + run: | + aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster + echo "Step output: ${{ steps.set-ecr.outputs.registry }}" + - name: Build & Push congestion-service + env: # ecr 업로드 할 때 필요한 변수들 선언 + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # ecr 경로 run: | cd congestion-service echo "${{ secrets.BACKEND_ENV}}" > .env @@ -92,36 +145,53 @@ jobs: docker push $ECR_REGISTRY/congestion-service:$IMAGE_TAG docker push $ECR_REGISTRY/congestion-service:latest + + kubectl set image deployment/congestion-service congestion-service=$ECR_REGISTRY/congestion-service:$IMAGE_TAG + kubectl rollout status deployment/congestion-service + + gateway-build: + runs-on: ubuntu-latest + env: + IMAGE_TAG: 1.${{ github.run_number }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: 자바 설치 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: '17' - - name: external-info-service 빌드 - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + - name: kubeconfig 설정 run: | - cd external-info-service + aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster + echo "Step output: ${{ steps.set-ecr.outputs.registry }}" + - name: Build & Push gateway + env: # ecr 업로드 할 때 필요한 변수들 선언 + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # ecr 경로 + run: | + cd gateway echo "${{ secrets.BACKEND_ENV}}" > .env gradle bootJar -x test - docker build -t $ECR_REGISTRY/external-info-service:$IMAGE_TAG . - docker tag $ECR_REGISTRY/external-info-service:$IMAGE_TAG $ECR_REGISTRY/external-info-service:latest - - docker push $ECR_REGISTRY/external-info-service:$IMAGE_TAG - docker push $ECR_REGISTRY/external-info-service:latest - - - - name: kubeconfig 설정 - run: aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster + docker build -t $ECR_REGISTRY/gateway:$IMAGE_TAG . + docker tag $ECR_REGISTRY/gateway:$IMAGE_TAG $ECR_REGISTRY/gateway:latest - - name: Deploy to EKS - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - run: | - kubectl set image deployment/external-info-service external-info-service=$ECR_REGISTRY/external-info-service:latest - kubectl rollout status deployment/external-info-service - - kubectl set image deployment/place-service place-service=$ECR_REGISTRY/place-service:latest - kubectl rollout status deployment/place-service + docker push $ECR_REGISTRY/gateway:$IMAGE_TAG + docker push $ECR_REGISTRY/gateway:latest - kubectl set image deployment/congestion-service congestion-service=$ECR_REGISTRY/congestion-service:latest - kubectl rollout status deployment/congestion-service + echo "태그 확인: gateway=$ECR_REGISTRY/gateway:$IMAGE_TAG" - kubectl set image deployment/gateway gateway=$ECR_REGISTRY/gateway:latest + kubectl set image deployment/gateway gateway=$ECR_REGISTRY/gateway:$IMAGE_TAG kubectl rollout status deployment/gateway \ No newline at end of file From 331684b50ebf028e785e3d5361445d5f53081591 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 16:46:20 +0900 Subject: [PATCH 209/361] =?UTF-8?q?deploy.yml=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 196 ++++++++++++------ .../src/main/resources/application.properties | 20 -- 2 files changed, 133 insertions(+), 83 deletions(-) delete mode 100644 external-info-service/src/main/resources/application.properties diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b731272..7895ce6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,17 +1,5 @@ name: Backend Upload ECR -#on: -# workflow_dispatch: -# inputs: -# environment: -# description: '수동 배포' -# required: true -# default: 'all' -# type: choice -# options: -# - all -# - latest - on: push: branches: [develop] @@ -20,20 +8,12 @@ on: jobs: - upload-image: + + external-info-service-build : runs-on: ubuntu-latest - env : - IMAGE_TAG: ${{ github.run_number }} + env: + IMAGE_TAG: 1.${{ github.run_number }} steps: -# - name: Set IMAGE_TAG based on input -# id: tag -# run: | -# if [ "${{ github.event.inputs.environment }}" = "all" ]; then -# echo "IMAGE_TAG=1.0.${{ github.run_number }}" >> $GITHUB_ENV -# else -# echo "IMAGE_TAG=latest" >> $GITHUB_ENV -# fi - - name: Checkout repo uses: actions/checkout@v4 @@ -54,22 +34,61 @@ jobs: distribution: 'temurin' java-version: '17' - - name: gateway 빌드 - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + - name: kubeconfig 설정 run: | - cd gateway + aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster + echo "Step output: ${{ steps.set-ecr.outputs.registry }}" + + - name: Build & Push external-info-service + env: # ecr 업로드 할 때 필요한 변수들 선언 + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # ecr 경로 + run: | + cd external-info-service + echo "ECR_REGISTRY is: [$ECR_REGISTRY]" echo "${{ secrets.BACKEND_ENV}}" > .env gradle bootJar -x test - docker build -t $ECR_REGISTRY/gateway:$IMAGE_TAG . - docker tag $ECR_REGISTRY/gateway:$IMAGE_TAG $ECR_REGISTRY/gateway:latest + docker build -t $ECR_REGISTRY/external-info-service:$IMAGE_TAG . + docker tag $ECR_REGISTRY/external-info-service:$IMAGE_TAG $ECR_REGISTRY/external-info-service:latest + + docker push $ECR_REGISTRY/external-info-service:$IMAGE_TAG + docker push $ECR_REGISTRY/external-info-service:latest + + kubectl set image deployment/external-info-service external-info-service=$ECR_REGISTRY/external-info-service:$IMAGE_TAG + kubectl rollout status deployment/external-info-service - docker push $ECR_REGISTRY/gateway:$IMAGE_TAG - docker push $ECR_REGISTRY/gateway:latest + place-service-build: + runs-on: ubuntu-latest + env: + IMAGE_TAG: 1.${{ github.run_number }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: 자바 설치 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: '17' + + - name: kubeconfig 설정 + run: | + aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster + echo "Step output: ${{ steps.set-ecr.outputs.registry }}" - - name: place-service 빌드 - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + - name: Build & Push place-service + env: # ecr 업로드 할 때 필요한 변수들 선언 + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # ecr 경로 run: | cd place-service echo "${{ secrets.BACKEND_ENV}}" > .env @@ -79,10 +98,44 @@ jobs: docker push $ECR_REGISTRY/place-service:$IMAGE_TAG docker push $ECR_REGISTRY/place-service:latest + + echo "태그 확인: place-service=$ECR_REGISTRY/place-service:$IMAGE_TAG" + + kubectl set image deployment/place-service place-service=$ECR_REGISTRY/place-service:$IMAGE_TAG + kubectl rollout status deployment/place-service + + congestion-service-build: + runs-on: ubuntu-latest + env: + IMAGE_TAG: 1.${{ github.run_number }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: 자바 설치 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: '17' - - name: congestion-service 빌드 - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + - name: kubeconfig 설정 + run: | + aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster + echo "Step output: ${{ steps.set-ecr.outputs.registry }}" + - name: Build & Push congestion-service + env: # ecr 업로드 할 때 필요한 변수들 선언 + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # ecr 경로 run: | cd congestion-service echo "${{ secrets.BACKEND_ENV}}" > .env @@ -92,36 +145,53 @@ jobs: docker push $ECR_REGISTRY/congestion-service:$IMAGE_TAG docker push $ECR_REGISTRY/congestion-service:latest + + kubectl set image deployment/congestion-service congestion-service=$ECR_REGISTRY/congestion-service:$IMAGE_TAG + kubectl rollout status deployment/congestion-service + + gateway-build: + runs-on: ubuntu-latest + env: + IMAGE_TAG: 1.${{ github.run_number }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: 자바 설치 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: '17' - - name: external-info-service 빌드 - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + - name: kubeconfig 설정 run: | - cd external-info-service + aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster + echo "Step output: ${{ steps.set-ecr.outputs.registry }}" + - name: Build & Push gateway + env: # ecr 업로드 할 때 필요한 변수들 선언 + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # ecr 경로 + run: | + cd gateway echo "${{ secrets.BACKEND_ENV}}" > .env gradle bootJar -x test - docker build -t $ECR_REGISTRY/external-info-service:$IMAGE_TAG . - docker tag $ECR_REGISTRY/external-info-service:$IMAGE_TAG $ECR_REGISTRY/external-info-service:latest - - docker push $ECR_REGISTRY/external-info-service:$IMAGE_TAG - docker push $ECR_REGISTRY/external-info-service:latest - - - - name: kubeconfig 설정 - run: aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster + docker build -t $ECR_REGISTRY/gateway:$IMAGE_TAG . + docker tag $ECR_REGISTRY/gateway:$IMAGE_TAG $ECR_REGISTRY/gateway:latest - - name: Deploy to EKS - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - run: | - kubectl set image deployment/external-info-service external-info-service=$ECR_REGISTRY/external-info-service:latest - kubectl rollout status deployment/external-info-service - - kubectl set image deployment/place-service place-service=$ECR_REGISTRY/place-service:latest - kubectl rollout status deployment/place-service + docker push $ECR_REGISTRY/gateway:$IMAGE_TAG + docker push $ECR_REGISTRY/gateway:latest - kubectl set image deployment/congestion-service congestion-service=$ECR_REGISTRY/congestion-service:latest - kubectl rollout status deployment/congestion-service + echo "태그 확인: gateway=$ECR_REGISTRY/gateway:$IMAGE_TAG" - kubectl set image deployment/gateway gateway=$ECR_REGISTRY/gateway:latest + kubectl set image deployment/gateway gateway=$ECR_REGISTRY/gateway:$IMAGE_TAG kubectl rollout status deployment/gateway \ No newline at end of file diff --git a/external-info-service/src/main/resources/application.properties b/external-info-service/src/main/resources/application.properties deleted file mode 100644 index 3b12f08..0000000 --- a/external-info-service/src/main/resources/application.properties +++ /dev/null @@ -1,20 +0,0 @@ -spring.application.name=external-info-service - -server.port=8081 - -# postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db -spring.datasource.username=root -spring.datasource.password=admin -spring.datasource.driver-class-name=org.postgresql.Driver - -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update -spring.elasticsearch.uris= http://elasticsearch.seoultravel.life:9200 - -spring.mvc.contentnegotiation.favor-path-extension=false -spring.mvc.contentnegotiation.media-types.json=application/json -spring.mvc.default-response-type=application/json - - -seoul.api.key=${SEOUL_API_KEY} \ No newline at end of file From 888753b48a3df67e94bea17656673930bf45794a Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 16:49:29 +0900 Subject: [PATCH 210/361] =?UTF-8?q?application.properties=20=EC=B6=94?= =?UTF-8?q?=EC=A0=81=20=EC=A4=91=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.properties | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 user-service/src/main/resources/application.properties diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties deleted file mode 100644 index 2757797..0000000 --- a/user-service/src/main/resources/application.properties +++ /dev/null @@ -1,19 +0,0 @@ -spring.application.name=user-service - -server.port=8083 - -spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db -spring.datasource.username=root -spring.datasource.password=admin -spring.datasource.driver-class-name=org.postgresql.Driver -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update - -spring.data.redis.host=localhost -spring.data.redis.port=6379 -spring.data.redis.password=1234 - - -jwt.secret=${JWT_SECRET_KEY} -jwt.expiration=86400000 -jwt.refresh-expiration=2592000000 \ No newline at end of file From 7d6ff8d4042a28c2f07e7a490f7abc61e047d76f Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 29 Apr 2025 16:49:29 +0900 Subject: [PATCH 211/361] =?UTF-8?q?application.properties=20=EC=B6=94?= =?UTF-8?q?=EC=A0=81=20=EC=A4=91=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.properties | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 user-service/src/main/resources/application.properties diff --git a/user-service/src/main/resources/application.properties b/user-service/src/main/resources/application.properties deleted file mode 100644 index 2757797..0000000 --- a/user-service/src/main/resources/application.properties +++ /dev/null @@ -1,19 +0,0 @@ -spring.application.name=user-service - -server.port=8083 - -spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db -spring.datasource.username=root -spring.datasource.password=admin -spring.datasource.driver-class-name=org.postgresql.Driver -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update - -spring.data.redis.host=localhost -spring.data.redis.port=6379 -spring.data.redis.password=1234 - - -jwt.secret=${JWT_SECRET_KEY} -jwt.expiration=86400000 -jwt.refresh-expiration=2592000000 \ No newline at end of file From 20c7f02ff60f450c9c83e5a14ed35830e824b9e7 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 30 Apr 2025 09:28:44 +0900 Subject: [PATCH 212/361] =?UTF-8?q?Refactor:=20=EB=B3=80=EC=88=98=EB=AA=85?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/restaurant/RestaurantDetailResponse.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java b/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java index b16919e..8dd08ba 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java +++ b/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java @@ -9,14 +9,14 @@ @Builder public class RestaurantDetailResponse { - private String address_name; + private String address; private String category_group_code; private String category_group_name; // 고정 X, DB 값 private String category_name; // 고정 X, DB 값 private String distance; // 빈 값 유지 private String id; private String phone; - private String place_name; + private String name; private String place_url; private String road_address_name; private String lat; @@ -29,7 +29,7 @@ public class RestaurantDetailResponse { public static RestaurantDetailResponse fromEntity(Restaurant restaurant) { return RestaurantDetailResponse.builder() - .address_name(restaurant.getAddress()) + .address(restaurant.getAddress()) .category_group_code(restaurant.getCategory_code()) .category_group_name(restaurant.getCategoryGroupName()) // 고정X (DB) .category_name(restaurant.getCategoryName()) // 고정X (DB) @@ -37,7 +37,7 @@ public static RestaurantDetailResponse fromEntity(Restaurant restaurant) { .id(String.valueOf(restaurant.getRestaurantId())) .kakao_id(restaurant.getKakao_id()) .phone(restaurant.getPhone()) - .place_name(restaurant.getName()) + .name(restaurant.getName()) .place_url(restaurant.getKakaomap_url()) .categoryGroupName(restaurant.getCategoryGroupName()) // 추가 .categoryName(restaurant.getCategoryName()) // 추가 From f96400ee47f8d44c1842a31b07c3df8b32b99e40 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 30 Apr 2025 09:28:44 +0900 Subject: [PATCH 213/361] =?UTF-8?q?Refactor:=20=EB=B3=80=EC=88=98=EB=AA=85?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/restaurant/RestaurantDetailResponse.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java b/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java index b16919e..8dd08ba 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java +++ b/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java @@ -9,14 +9,14 @@ @Builder public class RestaurantDetailResponse { - private String address_name; + private String address; private String category_group_code; private String category_group_name; // 고정 X, DB 값 private String category_name; // 고정 X, DB 값 private String distance; // 빈 값 유지 private String id; private String phone; - private String place_name; + private String name; private String place_url; private String road_address_name; private String lat; @@ -29,7 +29,7 @@ public class RestaurantDetailResponse { public static RestaurantDetailResponse fromEntity(Restaurant restaurant) { return RestaurantDetailResponse.builder() - .address_name(restaurant.getAddress()) + .address(restaurant.getAddress()) .category_group_code(restaurant.getCategory_code()) .category_group_name(restaurant.getCategoryGroupName()) // 고정X (DB) .category_name(restaurant.getCategoryName()) // 고정X (DB) @@ -37,7 +37,7 @@ public static RestaurantDetailResponse fromEntity(Restaurant restaurant) { .id(String.valueOf(restaurant.getRestaurantId())) .kakao_id(restaurant.getKakao_id()) .phone(restaurant.getPhone()) - .place_name(restaurant.getName()) + .name(restaurant.getName()) .place_url(restaurant.getKakaomap_url()) .categoryGroupName(restaurant.getCategoryGroupName()) // 추가 .categoryName(restaurant.getCategoryName()) // 추가 From 2226dea6150d6bf216ce9499a5a229239cc6cf7d Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 30 Apr 2025 09:35:51 +0900 Subject: [PATCH 214/361] =?UTF-8?q?Accommodation=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AccommodationController.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java index 8e07589..33e3ce4 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java @@ -11,7 +11,7 @@ import java.util.List; @RestController -@RequestMapping("/main/accommodation") +@RequestMapping("/main") public class AccommodationController { private final AccommodationService accommodationService; @@ -20,22 +20,22 @@ public AccommodationController(AccommodationService accommodationService) { this.accommodationService = accommodationService; } - @GetMapping("/list") + @GetMapping("/accommodation/list") public List getAccommodations() throws IOException { return accommodationService.showAccommodations(); } - @GetMapping("/{accommodation_id}") + @GetMapping("/info/accommodation/{accommodation_id}") public AccommodationDto getAccommodationById(@PathVariable Long accommodation_id) throws IOException { return accommodationService.getAccommodationById(accommodation_id); } - @GetMapping("/gu/{gu}") + @GetMapping("/accommodation/gu/{gu}") public List getAccommodationByGu(@PathVariable String gu) throws IOException { return accommodationService.getAccommodationByGu(gu); } - @GetMapping("/type/{type}") + @GetMapping("/accommodation/type/{type}") public List getAccommodationByType(@PathVariable String type) throws IOException { return accommodationService.getAccommodationByType(type); } From b0fdc98ca9cf058a8dcdb6f2a6d7020d6c453206 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 30 Apr 2025 09:35:51 +0900 Subject: [PATCH 215/361] =?UTF-8?q?Accommodation=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AccommodationController.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java index 8e07589..33e3ce4 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java @@ -11,7 +11,7 @@ import java.util.List; @RestController -@RequestMapping("/main/accommodation") +@RequestMapping("/main") public class AccommodationController { private final AccommodationService accommodationService; @@ -20,22 +20,22 @@ public AccommodationController(AccommodationService accommodationService) { this.accommodationService = accommodationService; } - @GetMapping("/list") + @GetMapping("/accommodation/list") public List getAccommodations() throws IOException { return accommodationService.showAccommodations(); } - @GetMapping("/{accommodation_id}") + @GetMapping("/info/accommodation/{accommodation_id}") public AccommodationDto getAccommodationById(@PathVariable Long accommodation_id) throws IOException { return accommodationService.getAccommodationById(accommodation_id); } - @GetMapping("/gu/{gu}") + @GetMapping("/accommodation/gu/{gu}") public List getAccommodationByGu(@PathVariable String gu) throws IOException { return accommodationService.getAccommodationByGu(gu); } - @GetMapping("/type/{type}") + @GetMapping("/accommodation/type/{type}") public List getAccommodationByType(@PathVariable String type) throws IOException { return accommodationService.getAccommodationByType(type); } From 9066fc266e0628631e024c4f3e5e4f418ec01479 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Wed, 30 Apr 2025 10:10:14 +0900 Subject: [PATCH 216/361] =?UTF-8?q?=EC=B9=B4=ED=8E=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.properties | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/external-info-service/src/main/resources/application.properties b/external-info-service/src/main/resources/application.properties index d17219a..05948dd 100644 --- a/external-info-service/src/main/resources/application.properties +++ b/external-info-service/src/main/resources/application.properties @@ -1,6 +1,5 @@ spring.application.name=external-info-service -<<<<<<< HEAD -======= + server.port=8081 # postgreSQL ?? @@ -19,4 +18,3 @@ spring.mvc.default-response-type=application/json seoul.api.key=505a6255746b696d34344b46566174 ->>>>>>> 1230fbad88e7b4ae62a7080d04646e647a3a6793 From 7c8504ad17657a52414ba8e3c1825bc931193f3a Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 30 Apr 2025 10:21:21 +0900 Subject: [PATCH 217/361] =?UTF-8?q?accommodation=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=93=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AccommodationController.java | 6 ++++++ .../repository/AccommodationRepository.java | 4 ++++ .../service/AccommodationService.java | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java index 33e3ce4..afc337f 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.util.List; +import java.util.Map; @RestController @RequestMapping("/main") @@ -25,6 +26,11 @@ public List getAccommodations() throws IOException { return accommodationService.showAccommodations(); } + @GetMapping("/accommodation/list/{areaId}") + public Map getAccommodationByAreaId(@PathVariable Long areaId) throws IOException { + return accommodationService.getAccommodationByAreaId(areaId); + } + @GetMapping("/info/accommodation/{accommodation_id}") public AccommodationDto getAccommodationById(@PathVariable Long accommodation_id) throws IOException { return accommodationService.getAccommodationById(accommodation_id); diff --git a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java index eaedae0..9586746 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java @@ -2,6 +2,7 @@ import com.example.placeservice.entity.Accommodation; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import java.util.List; @@ -11,4 +12,7 @@ public interface AccommodationRepository extends JpaRepository findByGu(String gu); List findByType(String type); + + @Query(value = "SELECT * FROM Accommodation WHERE area_id = ?1", nativeQuery = true) + List findByAreaId(Long areaId); } diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java index e7067b5..75b7b78 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java @@ -24,7 +24,9 @@ import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Service public class AccommodationService { @@ -204,4 +206,21 @@ public List getAccommodationByType(String type) { } return accommodationDtos; } + + public Map getAccommodationByAreaId(Long areaId) { + List accommodations = accommodationRepository.findByAreaId(areaId); + List accommodationDtos = new ArrayList<>(); + + for (Accommodation accommodation : accommodations) { + AccommodationDto accommodationDto = AccommodationDto.fromEntity(accommodation); + accommodationDtos.add(accommodationDto); + } + + Map response = new HashMap<>(); + + response.put("type", "accommodation"); // 고정된 타입 문자열 + response.put("content", accommodationDtos); // 위에서 만든 DTO 리스트 + + return response; + } } \ No newline at end of file From 5222c32cbdb6d3adccadaf056057a1de154ba326 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 30 Apr 2025 10:21:21 +0900 Subject: [PATCH 218/361] =?UTF-8?q?accommodation=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=93=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AccommodationController.java | 6 ++++++ .../repository/AccommodationRepository.java | 4 ++++ .../service/AccommodationService.java | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java index 33e3ce4..afc337f 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AccommodationController.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.util.List; +import java.util.Map; @RestController @RequestMapping("/main") @@ -25,6 +26,11 @@ public List getAccommodations() throws IOException { return accommodationService.showAccommodations(); } + @GetMapping("/accommodation/list/{areaId}") + public Map getAccommodationByAreaId(@PathVariable Long areaId) throws IOException { + return accommodationService.getAccommodationByAreaId(areaId); + } + @GetMapping("/info/accommodation/{accommodation_id}") public AccommodationDto getAccommodationById(@PathVariable Long accommodation_id) throws IOException { return accommodationService.getAccommodationById(accommodation_id); diff --git a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java index eaedae0..9586746 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java @@ -2,6 +2,7 @@ import com.example.placeservice.entity.Accommodation; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import java.util.List; @@ -11,4 +12,7 @@ public interface AccommodationRepository extends JpaRepository findByGu(String gu); List findByType(String type); + + @Query(value = "SELECT * FROM Accommodation WHERE area_id = ?1", nativeQuery = true) + List findByAreaId(Long areaId); } diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java index e7067b5..75b7b78 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java @@ -24,7 +24,9 @@ import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Service public class AccommodationService { @@ -204,4 +206,21 @@ public List getAccommodationByType(String type) { } return accommodationDtos; } + + public Map getAccommodationByAreaId(Long areaId) { + List accommodations = accommodationRepository.findByAreaId(areaId); + List accommodationDtos = new ArrayList<>(); + + for (Accommodation accommodation : accommodations) { + AccommodationDto accommodationDto = AccommodationDto.fromEntity(accommodation); + accommodationDtos.add(accommodationDto); + } + + Map response = new HashMap<>(); + + response.put("type", "accommodation"); // 고정된 타입 문자열 + response.put("content", accommodationDtos); // 위에서 만든 DTO 리스트 + + return response; + } } \ No newline at end of file From b7d28aadacccaa589094455fc721d57dbf16f485 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 30 Apr 2025 10:23:40 +0900 Subject: [PATCH 219/361] =?UTF-8?q?Refactor:=20=EC=A7=80=EC=97=AD=EC=9D=B4?= =?UTF-8?q?=EB=A6=84,id=20=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/restaurant/RestaurantDetailResponse.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java b/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java index 8dd08ba..0c7e911 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java +++ b/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java @@ -9,11 +9,12 @@ @Builder public class RestaurantDetailResponse { + private Long area_id; + private String area_name; private String address; private String category_group_code; - private String category_group_name; // 고정 X, DB 값 - private String category_name; // 고정 X, DB 값 - private String distance; // 빈 값 유지 + private String category_group_name; + private String category_name; private String id; private String phone; private String name; @@ -21,19 +22,18 @@ public class RestaurantDetailResponse { private String road_address_name; private String lat; private String lon; - private String categoryGroupName; // 추가 - private String categoryName; // 추가 - + private String categoryGroupName; + private String categoryName; private String kakao_id; - public static RestaurantDetailResponse fromEntity(Restaurant restaurant) { return RestaurantDetailResponse.builder() + .area_id(restaurant.getArea().getAreaId()) + .area_name(restaurant.getArea().getName()) .address(restaurant.getAddress()) .category_group_code(restaurant.getCategory_code()) .category_group_name(restaurant.getCategoryGroupName()) // 고정X (DB) .category_name(restaurant.getCategoryName()) // 고정X (DB) - .distance("") // 그대로 비워두기 .id(String.valueOf(restaurant.getRestaurantId())) .kakao_id(restaurant.getKakao_id()) .phone(restaurant.getPhone()) From 5e1fa37595e5ff1ac000c7293488726ded2e821a Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 30 Apr 2025 10:23:40 +0900 Subject: [PATCH 220/361] =?UTF-8?q?Refactor:=20=EC=A7=80=EC=97=AD=EC=9D=B4?= =?UTF-8?q?=EB=A6=84,id=20=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/restaurant/RestaurantDetailResponse.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java b/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java index 8dd08ba..0c7e911 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java +++ b/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java @@ -9,11 +9,12 @@ @Builder public class RestaurantDetailResponse { + private Long area_id; + private String area_name; private String address; private String category_group_code; - private String category_group_name; // 고정 X, DB 값 - private String category_name; // 고정 X, DB 값 - private String distance; // 빈 값 유지 + private String category_group_name; + private String category_name; private String id; private String phone; private String name; @@ -21,19 +22,18 @@ public class RestaurantDetailResponse { private String road_address_name; private String lat; private String lon; - private String categoryGroupName; // 추가 - private String categoryName; // 추가 - + private String categoryGroupName; + private String categoryName; private String kakao_id; - public static RestaurantDetailResponse fromEntity(Restaurant restaurant) { return RestaurantDetailResponse.builder() + .area_id(restaurant.getArea().getAreaId()) + .area_name(restaurant.getArea().getName()) .address(restaurant.getAddress()) .category_group_code(restaurant.getCategory_code()) .category_group_name(restaurant.getCategoryGroupName()) // 고정X (DB) .category_name(restaurant.getCategoryName()) // 고정X (DB) - .distance("") // 그대로 비워두기 .id(String.valueOf(restaurant.getRestaurantId())) .kakao_id(restaurant.getKakao_id()) .phone(restaurant.getPhone()) From 5049b62a0066e25725be7095159951d201ae4978 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Wed, 30 Apr 2025 10:25:17 +0900 Subject: [PATCH 221/361] =?UTF-8?q?=EC=B9=B4=ED=8E=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CafeController.java | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java index 2b448b0..f66eaf6 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java @@ -41,8 +41,8 @@ public ResponseEntity>> getCafeList() { Map cafeMap = new LinkedHashMap<>(); cafeMap.put("id", cafe.getId().toString()); cafeMap.put("name", cafe.getName()); - cafeMap.put("x", cafe.getLon().toString()); - cafeMap.put("y", cafe.getLat().toString()); + cafeMap.put("lat", cafe.getLat().toString()); + cafeMap.put("lon", cafe.getLon().toString()); return cafeMap; }) .collect(Collectors.toList()); @@ -51,19 +51,28 @@ public ResponseEntity>> getCafeList() { } /** - * 지역 ID를 기준으로 카페 목록 조회 + * 지역 ID를 기준으로 카페 목록 조회 (간략 정보만 반환) */ @GetMapping("/cafe/list/{areaId}") - public ResponseEntity> getCafesByAreaId(@PathVariable Long areaId) { - log.info("지역 ID {}의 카페 목록 조회", areaId); - List cafes = cafeService.getCafesByAreaId(areaId); + public ResponseEntity>> getCafesByAreaId(@PathVariable Long areaId) { + log.info("지역 ID {}의 간략 카페 목록 조회", areaId); + List cafes = cafeRepository.findByAreaId(areaId); + log.info("지역 ID {}에서 총 {}개의 카페 찾음", areaId, cafes.size()); - // ID 기준으로 정렬 - List sortedCafes = cafes.stream() - .sorted(Comparator.comparing(CafeDto::getId)) + // ID 기준으로 정렬 및 간략 정보만 매핑 + List> simpleCafeList = cafes.stream() + .sorted(Comparator.comparing(Cafe::getId)) + .map(cafe -> { + Map cafeMap = new LinkedHashMap<>(); + cafeMap.put("id", cafe.getId().toString()); + cafeMap.put("name", cafe.getName()); + cafeMap.put("lat", cafe.getLat().toString()); + cafeMap.put("lon", cafe.getLon().toString()); + return cafeMap; + }) .collect(Collectors.toList()); - return ResponseEntity.ok(sortedCafes); + return ResponseEntity.ok(simpleCafeList); } @GetMapping("/info/cafe/{cafeId}") @@ -85,10 +94,14 @@ public ResponseEntity> getCafeInfo(@PathVariable Long cafeId cafeInfo.put("lat", cafe.getLat()); cafeInfo.put("lon", cafe.getLon()); - Map response = new LinkedHashMap<>(); - response.put("data", cafeInfo); + // 지역(Area) 정보 추가 + if (cafe.getArea() != null) { + cafeInfo.put("area_id", cafe.getArea().getAreaId()); + cafeInfo.put("area_name", cafe.getArea().getName()); + } - return ResponseEntity.ok(response); + // data 래퍼 없이 직접 반환 + return ResponseEntity.ok(cafeInfo); } return ResponseEntity.notFound().build(); From 1d949932dc5d3be3c5b4526ce713302ff5905d42 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Wed, 30 Apr 2025 10:25:17 +0900 Subject: [PATCH 222/361] =?UTF-8?q?=EC=B9=B4=ED=8E=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CafeController.java | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java index 2b448b0..f66eaf6 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java @@ -41,8 +41,8 @@ public ResponseEntity>> getCafeList() { Map cafeMap = new LinkedHashMap<>(); cafeMap.put("id", cafe.getId().toString()); cafeMap.put("name", cafe.getName()); - cafeMap.put("x", cafe.getLon().toString()); - cafeMap.put("y", cafe.getLat().toString()); + cafeMap.put("lat", cafe.getLat().toString()); + cafeMap.put("lon", cafe.getLon().toString()); return cafeMap; }) .collect(Collectors.toList()); @@ -51,19 +51,28 @@ public ResponseEntity>> getCafeList() { } /** - * 지역 ID를 기준으로 카페 목록 조회 + * 지역 ID를 기준으로 카페 목록 조회 (간략 정보만 반환) */ @GetMapping("/cafe/list/{areaId}") - public ResponseEntity> getCafesByAreaId(@PathVariable Long areaId) { - log.info("지역 ID {}의 카페 목록 조회", areaId); - List cafes = cafeService.getCafesByAreaId(areaId); + public ResponseEntity>> getCafesByAreaId(@PathVariable Long areaId) { + log.info("지역 ID {}의 간략 카페 목록 조회", areaId); + List cafes = cafeRepository.findByAreaId(areaId); + log.info("지역 ID {}에서 총 {}개의 카페 찾음", areaId, cafes.size()); - // ID 기준으로 정렬 - List sortedCafes = cafes.stream() - .sorted(Comparator.comparing(CafeDto::getId)) + // ID 기준으로 정렬 및 간략 정보만 매핑 + List> simpleCafeList = cafes.stream() + .sorted(Comparator.comparing(Cafe::getId)) + .map(cafe -> { + Map cafeMap = new LinkedHashMap<>(); + cafeMap.put("id", cafe.getId().toString()); + cafeMap.put("name", cafe.getName()); + cafeMap.put("lat", cafe.getLat().toString()); + cafeMap.put("lon", cafe.getLon().toString()); + return cafeMap; + }) .collect(Collectors.toList()); - return ResponseEntity.ok(sortedCafes); + return ResponseEntity.ok(simpleCafeList); } @GetMapping("/info/cafe/{cafeId}") @@ -85,10 +94,14 @@ public ResponseEntity> getCafeInfo(@PathVariable Long cafeId cafeInfo.put("lat", cafe.getLat()); cafeInfo.put("lon", cafe.getLon()); - Map response = new LinkedHashMap<>(); - response.put("data", cafeInfo); + // 지역(Area) 정보 추가 + if (cafe.getArea() != null) { + cafeInfo.put("area_id", cafe.getArea().getAreaId()); + cafeInfo.put("area_name", cafe.getArea().getName()); + } - return ResponseEntity.ok(response); + // data 래퍼 없이 직접 반환 + return ResponseEntity.ok(cafeInfo); } return ResponseEntity.notFound().build(); From 181132aafd22462205614cb6ac3a0bb74d64397f Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 30 Apr 2025 13:47:15 +0900 Subject: [PATCH 223/361] =?UTF-8?q?Refactor:=20=EC=A6=90=EA=B2=A8=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20API=20=EC=88=98=EC=A0=95=20&=20=EA=B4=80=EA=B4=91?= =?UTF-8?q?=EC=A7=80=20API=20dto=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/accommodation/AccommodationDto.java | 8 +- .../dto/attraction/AttractionInfoDto.java | 6 +- .../service/AttractionService.java | 9 +- .../example/userservice/config/AppConfig.java | 14 +++ .../controller/AdminController.java | 6 +- .../controller/FavoriteController.java | 11 +- .../example/userservice/dto/FavoriteDto.java | 6 +- .../repository/FavoriteRepository.java | 1 + .../userservice/service/FavoriteService.java | 111 ++++++++++++++---- 9 files changed, 136 insertions(+), 36 deletions(-) create mode 100644 user-service/src/main/java/com/example/userservice/config/AppConfig.java diff --git a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java index 997e257..fc783c7 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java @@ -12,9 +12,9 @@ @AllArgsConstructor public class AccommodationDto { - private Long accommodationId; - private Long areaId; - private String areaName; + private Long accommodation_id; + private Long area_id; + private String area_name; private String name; private String address; private BigDecimal lat; @@ -22,7 +22,7 @@ public class AccommodationDto { private String phone; private String gu; private String type; - private String kakaoUrl; + private String kakaomap_url; /** * Accommodation 엔티티를 AccommodationDto로 변환하는 정적 팩토리 메소드 diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java index 389d057..7899022 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java @@ -17,8 +17,10 @@ public class AttractionInfoDto { private String close_day; private String use_time; private String kakaomap_url; + private Long area_id; + private String area_name; - public AttractionInfoDto(Long attractionId, String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time, String kakaomapUrl) { + public AttractionInfoDto(Long attractionId, String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time, String kakaomapUrl, Long area_id, String area_name) { this.attraction_id = attractionId; this.seoul_attraction_id = seoul_attraction_id; this.name = name; @@ -30,6 +32,8 @@ public AttractionInfoDto(Long attractionId, String seoul_attraction_id, String n this.close_day = close_day; this.use_time = use_time; this.kakaomap_url = kakaomapUrl; + this.area_id = area_id; + this.area_name = area_name; } } diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index 04a593c..6de9b9b 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -162,6 +162,11 @@ public AttractionInfoDto getAttractionInfoData(long attractionId) { Attraction attraction = attractionRepository.findByAttractionId(attractionId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "해당 관광지 정보를 찾을 수 없습니다.")); + // 연결된 Area 정보 가져오기 + Area area = attraction.getArea(); // 여기에서 바로 가져올 수 있음 + String areaName = (area != null) ? area.getName() : null; + Long areaId = (area != null) ? area.getAreaId() : null; + return new AttractionInfoDto( attraction.getAttractionId(), attraction.getSeoulAttractionId(), @@ -173,7 +178,9 @@ public AttractionInfoDto getAttractionInfoData(long attractionId) { attraction.getHomepageUrl(), attraction.getCloseDay(), attraction.getUseTime(), - attraction.getKakaomapUrl() + attraction.getKakaomapUrl(), + areaId, + areaName ); } catch (RuntimeException e) { diff --git a/user-service/src/main/java/com/example/userservice/config/AppConfig.java b/user-service/src/main/java/com/example/userservice/config/AppConfig.java new file mode 100644 index 0000000..7ee7c5a --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/config/AppConfig.java @@ -0,0 +1,14 @@ +package com.example.userservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + +} diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminController.java b/user-service/src/main/java/com/example/userservice/controller/AdminController.java index 4eb82e5..8dfe416 100644 --- a/user-service/src/main/java/com/example/userservice/controller/AdminController.java +++ b/user-service/src/main/java/com/example/userservice/controller/AdminController.java @@ -2,7 +2,6 @@ import com.example.userservice.dto.FavoriteDto; import com.example.userservice.dto.MemberDto; -import com.example.userservice.entity.Favorite; import com.example.userservice.service.FavoriteService; import com.example.userservice.service.MemberService; import lombok.RequiredArgsConstructor; @@ -13,6 +12,7 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.Map; @RestController @RequiredArgsConstructor @@ -35,8 +35,8 @@ public ResponseEntity> getAllMembers() { @GetMapping("/favorite/list") @PreAuthorize("hasRole('ADMIN')") // ADMIN 권한이 있는 사용자만 접근 가능 - public ResponseEntity> getAllFavorites() { - List favorites = favoriteService.getAllFavoriteData(); + public ResponseEntity>> getAllFavorites() { + List> favorites = favoriteService.getAllFavoriteData(); return ResponseEntity.ok(favorites); } } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java b/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java index d01c1d0..1eeda9e 100644 --- a/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java +++ b/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java @@ -9,7 +9,9 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; +import java.util.HashMap; import java.util.List; +import java.util.Map; @RestController @RequestMapping("/mypage/favorite") @@ -27,19 +29,18 @@ public List getList() { } @PostMapping("/add") - public ResponseEntity addFavorite(@RequestBody FavoriteDto favoriteDto) { + public ResponseEntity> addFavorite(@RequestBody FavoriteDto favoriteDto) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String userId = authentication.getName(); - return ResponseEntity.ok(favoriteService.addFavoriteData(userId,favoriteDto)); + return favoriteService.addFavoriteData(userId,favoriteDto); } @DeleteMapping("/delete/{type}/{id}") - public ResponseEntity deleteFavorite(@PathVariable String type, @PathVariable String id, HttpServletRequest request) { + public ResponseEntity> deleteFavorite(@PathVariable String type, @PathVariable String id, HttpServletRequest request) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String userId = authentication.getName(); - favoriteService.deleteFavorite(userId, type, id); - return ResponseEntity.ok().build(); + return favoriteService.deleteFavorite(userId, type, id); } } diff --git a/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java b/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java index 0aef889..91e57a9 100644 --- a/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java +++ b/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java @@ -10,12 +10,16 @@ public class FavoriteDto { private String type; private String place_id; private String user_id; + private String name; + private String address; - public FavoriteDto(Long favorite_id ,String type, String placeId, String userId) { + public FavoriteDto(Long favorite_id ,String type, String placeId, String userId, String name, String address) { this.favorite_id = favorite_id; this.type = type; this.place_id = placeId; this.user_id = userId; + this.name = name; + this.address = address; } } diff --git a/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java b/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java index 8f113d4..6fbcaf3 100644 --- a/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java +++ b/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java @@ -13,4 +13,5 @@ public interface FavoriteRepository extends JpaRepository { List findAllByMember_UserId(String userId); Optional findByMemberAndTypeAndPlaceId(Member member, String type, String id); + } diff --git a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java index 51b8e9b..eadf605 100644 --- a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java +++ b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java @@ -5,11 +5,19 @@ import com.example.userservice.entity.Member; import com.example.userservice.repository.FavoriteRepository; import com.example.userservice.repository.jpa.MemberRepository; +import com.fasterxml.jackson.databind.JsonNode; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -17,29 +25,54 @@ public class FavoriteService { private final FavoriteRepository favoriteRepository; private final MemberRepository memberRepository; + private static final RestTemplate restTemplate = new RestTemplate(); public List getListData(String userId) { try{ List items = favoriteRepository.findAllByMember_UserId(userId); + return items.stream() - .map(item -> new FavoriteDto( + .map(item -> { + ResponseEntity response = restTemplate.exchange( + "http://localhost:8082/main/info/"+item.getType()+"/" + item.getPlaceId(), + HttpMethod.GET, + null, + JsonNode.class + ); + String name = response.getBody().get("name").asText(); + String address = response.getBody().get("address").asText(); + return new FavoriteDto( item.getFavoriteId(), item.getType(), item.getPlaceId(), - userId - )).toList(); + userId, + name, + address); + }).toList(); } catch (Exception e) { throw new RuntimeException(e); } } - public FavoriteDto addFavoriteData(String userId, FavoriteDto favoriteDto) { + public ResponseEntity> addFavoriteData(String userId, FavoriteDto favoriteDto) { + Map response = new HashMap<>(); // 1. 사용자 조회 Member member = memberRepository.findByUserId(userId) .orElseThrow(() -> new RuntimeException("사용자 없음")); + // 2. 이미 존재하는 즐겨찾기인지 확인 + Optional existingFavorite = favoriteRepository.findByMemberAndTypeAndPlaceId( + member, + favoriteDto.getType(), + favoriteDto.getPlace_id() + ); + if (existingFavorite.isPresent()) { + response.put("message", "이미 등록된 즐겨찾기입니다."); + return ResponseEntity.status(400).body(response); + } + // 2. Favorite 엔티티 생성 Favorite favorite = new Favorite(); favorite.setType(favoriteDto.getType()); @@ -49,33 +82,69 @@ public FavoriteDto addFavoriteData(String userId, FavoriteDto favoriteDto) { // 3. 저장 Favorite saved = favoriteRepository.save(favorite); - // 4. DTO로 변환 후 반환 - FavoriteDto result = new FavoriteDto(saved.getFavoriteId(),member.getUserId(), saved.getType(), saved.getPlaceId()); - - return result; + response.put("message", "즐겨찾기에 추가되었습니다."); + return ResponseEntity.ok(response); // 200 OK와 함께 메세지 전송 } @Transactional - public void deleteFavorite(String userId, String type, String id) { - Member member = memberRepository.findByUserId(userId) - .orElseThrow(() -> new RuntimeException("사용자 없음")); + public ResponseEntity> deleteFavorite(String userId, String type, String id) { + + Map response = new HashMap<>(); - Favorite favorite = favoriteRepository.findByMemberAndTypeAndPlaceId(member, type, id) - .orElseThrow(() -> new RuntimeException("즐겨찾기 없음")); + Member member = memberRepository.findByUserId(userId).orElse(null); + if (member == null) { + response.put("message", "사용자 없음"); + return ResponseEntity.status(400).body(response); + } + + Favorite favorite = favoriteRepository.findByMemberAndTypeAndPlaceId(member, type, id).orElse(null); + if (favorite == null) { + response.put("message", "즐겨찾기 없음"); + return ResponseEntity.status(400).body(response); + } favoriteRepository.delete(favorite); + response.put("message", "즐겨찾기 삭제 완료"); + return ResponseEntity.ok(response); } - public List getAllFavoriteData() { + public List> getAllFavoriteData() { try{ List items = favoriteRepository.findAll(); - return items.stream() - .map(item -> new FavoriteDto( - item.getFavoriteId(), - item.getType(), - item.getPlaceId(), - item.getMember().getUserId() - )).toList(); + List favoriteDtoList = items.stream() + .map(item ->{ + ResponseEntity response = restTemplate.exchange( + "http://localhost:8082/main/info/"+item.getType()+"/" + item.getPlaceId(), + HttpMethod.GET, + null, + JsonNode.class + ); + String name = response.getBody().get("name").asText(); + String address = response.getBody().get("address").asText(); + + return new FavoriteDto( + item.getFavoriteId(), + item.getType(), + item.getPlaceId(), + item.getMember().getUserId(), + name, + address); + }).toList(); + + Map> grouped = favoriteDtoList.stream() + .collect(Collectors.groupingBy(FavoriteDto::getUser_id)); + + // 원하는 형태로 변환 + List> result = grouped.entrySet().stream() + .map(entry -> { + Map map = new HashMap<>(); + map.put("user_id", entry.getKey()); + map.put("content", entry.getValue()); + return map; + }) + .toList(); + + return result; } catch (Exception e) { throw new RuntimeException(e); From aed07085487e31acb4317080a6fbf2377cff09ca Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 30 Apr 2025 13:47:15 +0900 Subject: [PATCH 224/361] =?UTF-8?q?Refactor:=20=EC=A6=90=EA=B2=A8=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20API=20=EC=88=98=EC=A0=95=20&=20=EA=B4=80=EA=B4=91?= =?UTF-8?q?=EC=A7=80=20API=20dto=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/accommodation/AccommodationDto.java | 8 +- .../dto/attraction/AttractionInfoDto.java | 6 +- .../service/AttractionService.java | 9 +- .../example/userservice/config/AppConfig.java | 14 +++ .../controller/AdminController.java | 6 +- .../controller/FavoriteController.java | 11 +- .../example/userservice/dto/FavoriteDto.java | 6 +- .../repository/FavoriteRepository.java | 1 + .../userservice/service/FavoriteService.java | 111 ++++++++++++++---- 9 files changed, 136 insertions(+), 36 deletions(-) create mode 100644 user-service/src/main/java/com/example/userservice/config/AppConfig.java diff --git a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java index 997e257..fc783c7 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java @@ -12,9 +12,9 @@ @AllArgsConstructor public class AccommodationDto { - private Long accommodationId; - private Long areaId; - private String areaName; + private Long accommodation_id; + private Long area_id; + private String area_name; private String name; private String address; private BigDecimal lat; @@ -22,7 +22,7 @@ public class AccommodationDto { private String phone; private String gu; private String type; - private String kakaoUrl; + private String kakaomap_url; /** * Accommodation 엔티티를 AccommodationDto로 변환하는 정적 팩토리 메소드 diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java index 389d057..7899022 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java @@ -17,8 +17,10 @@ public class AttractionInfoDto { private String close_day; private String use_time; private String kakaomap_url; + private Long area_id; + private String area_name; - public AttractionInfoDto(Long attractionId, String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time, String kakaomapUrl) { + public AttractionInfoDto(Long attractionId, String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time, String kakaomapUrl, Long area_id, String area_name) { this.attraction_id = attractionId; this.seoul_attraction_id = seoul_attraction_id; this.name = name; @@ -30,6 +32,8 @@ public AttractionInfoDto(Long attractionId, String seoul_attraction_id, String n this.close_day = close_day; this.use_time = use_time; this.kakaomap_url = kakaomapUrl; + this.area_id = area_id; + this.area_name = area_name; } } diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index 04a593c..6de9b9b 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -162,6 +162,11 @@ public AttractionInfoDto getAttractionInfoData(long attractionId) { Attraction attraction = attractionRepository.findByAttractionId(attractionId) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "해당 관광지 정보를 찾을 수 없습니다.")); + // 연결된 Area 정보 가져오기 + Area area = attraction.getArea(); // 여기에서 바로 가져올 수 있음 + String areaName = (area != null) ? area.getName() : null; + Long areaId = (area != null) ? area.getAreaId() : null; + return new AttractionInfoDto( attraction.getAttractionId(), attraction.getSeoulAttractionId(), @@ -173,7 +178,9 @@ public AttractionInfoDto getAttractionInfoData(long attractionId) { attraction.getHomepageUrl(), attraction.getCloseDay(), attraction.getUseTime(), - attraction.getKakaomapUrl() + attraction.getKakaomapUrl(), + areaId, + areaName ); } catch (RuntimeException e) { diff --git a/user-service/src/main/java/com/example/userservice/config/AppConfig.java b/user-service/src/main/java/com/example/userservice/config/AppConfig.java new file mode 100644 index 0000000..7ee7c5a --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/config/AppConfig.java @@ -0,0 +1,14 @@ +package com.example.userservice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + +} diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminController.java b/user-service/src/main/java/com/example/userservice/controller/AdminController.java index 4eb82e5..8dfe416 100644 --- a/user-service/src/main/java/com/example/userservice/controller/AdminController.java +++ b/user-service/src/main/java/com/example/userservice/controller/AdminController.java @@ -2,7 +2,6 @@ import com.example.userservice.dto.FavoriteDto; import com.example.userservice.dto.MemberDto; -import com.example.userservice.entity.Favorite; import com.example.userservice.service.FavoriteService; import com.example.userservice.service.MemberService; import lombok.RequiredArgsConstructor; @@ -13,6 +12,7 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.Map; @RestController @RequiredArgsConstructor @@ -35,8 +35,8 @@ public ResponseEntity> getAllMembers() { @GetMapping("/favorite/list") @PreAuthorize("hasRole('ADMIN')") // ADMIN 권한이 있는 사용자만 접근 가능 - public ResponseEntity> getAllFavorites() { - List favorites = favoriteService.getAllFavoriteData(); + public ResponseEntity>> getAllFavorites() { + List> favorites = favoriteService.getAllFavoriteData(); return ResponseEntity.ok(favorites); } } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java b/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java index d01c1d0..1eeda9e 100644 --- a/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java +++ b/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java @@ -9,7 +9,9 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; +import java.util.HashMap; import java.util.List; +import java.util.Map; @RestController @RequestMapping("/mypage/favorite") @@ -27,19 +29,18 @@ public List getList() { } @PostMapping("/add") - public ResponseEntity addFavorite(@RequestBody FavoriteDto favoriteDto) { + public ResponseEntity> addFavorite(@RequestBody FavoriteDto favoriteDto) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String userId = authentication.getName(); - return ResponseEntity.ok(favoriteService.addFavoriteData(userId,favoriteDto)); + return favoriteService.addFavoriteData(userId,favoriteDto); } @DeleteMapping("/delete/{type}/{id}") - public ResponseEntity deleteFavorite(@PathVariable String type, @PathVariable String id, HttpServletRequest request) { + public ResponseEntity> deleteFavorite(@PathVariable String type, @PathVariable String id, HttpServletRequest request) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String userId = authentication.getName(); - favoriteService.deleteFavorite(userId, type, id); - return ResponseEntity.ok().build(); + return favoriteService.deleteFavorite(userId, type, id); } } diff --git a/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java b/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java index 0aef889..91e57a9 100644 --- a/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java +++ b/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java @@ -10,12 +10,16 @@ public class FavoriteDto { private String type; private String place_id; private String user_id; + private String name; + private String address; - public FavoriteDto(Long favorite_id ,String type, String placeId, String userId) { + public FavoriteDto(Long favorite_id ,String type, String placeId, String userId, String name, String address) { this.favorite_id = favorite_id; this.type = type; this.place_id = placeId; this.user_id = userId; + this.name = name; + this.address = address; } } diff --git a/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java b/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java index 8f113d4..6fbcaf3 100644 --- a/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java +++ b/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java @@ -13,4 +13,5 @@ public interface FavoriteRepository extends JpaRepository { List findAllByMember_UserId(String userId); Optional findByMemberAndTypeAndPlaceId(Member member, String type, String id); + } diff --git a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java index 51b8e9b..eadf605 100644 --- a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java +++ b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java @@ -5,11 +5,19 @@ import com.example.userservice.entity.Member; import com.example.userservice.repository.FavoriteRepository; import com.example.userservice.repository.jpa.MemberRepository; +import com.fasterxml.jackson.databind.JsonNode; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -17,29 +25,54 @@ public class FavoriteService { private final FavoriteRepository favoriteRepository; private final MemberRepository memberRepository; + private static final RestTemplate restTemplate = new RestTemplate(); public List getListData(String userId) { try{ List items = favoriteRepository.findAllByMember_UserId(userId); + return items.stream() - .map(item -> new FavoriteDto( + .map(item -> { + ResponseEntity response = restTemplate.exchange( + "http://localhost:8082/main/info/"+item.getType()+"/" + item.getPlaceId(), + HttpMethod.GET, + null, + JsonNode.class + ); + String name = response.getBody().get("name").asText(); + String address = response.getBody().get("address").asText(); + return new FavoriteDto( item.getFavoriteId(), item.getType(), item.getPlaceId(), - userId - )).toList(); + userId, + name, + address); + }).toList(); } catch (Exception e) { throw new RuntimeException(e); } } - public FavoriteDto addFavoriteData(String userId, FavoriteDto favoriteDto) { + public ResponseEntity> addFavoriteData(String userId, FavoriteDto favoriteDto) { + Map response = new HashMap<>(); // 1. 사용자 조회 Member member = memberRepository.findByUserId(userId) .orElseThrow(() -> new RuntimeException("사용자 없음")); + // 2. 이미 존재하는 즐겨찾기인지 확인 + Optional existingFavorite = favoriteRepository.findByMemberAndTypeAndPlaceId( + member, + favoriteDto.getType(), + favoriteDto.getPlace_id() + ); + if (existingFavorite.isPresent()) { + response.put("message", "이미 등록된 즐겨찾기입니다."); + return ResponseEntity.status(400).body(response); + } + // 2. Favorite 엔티티 생성 Favorite favorite = new Favorite(); favorite.setType(favoriteDto.getType()); @@ -49,33 +82,69 @@ public FavoriteDto addFavoriteData(String userId, FavoriteDto favoriteDto) { // 3. 저장 Favorite saved = favoriteRepository.save(favorite); - // 4. DTO로 변환 후 반환 - FavoriteDto result = new FavoriteDto(saved.getFavoriteId(),member.getUserId(), saved.getType(), saved.getPlaceId()); - - return result; + response.put("message", "즐겨찾기에 추가되었습니다."); + return ResponseEntity.ok(response); // 200 OK와 함께 메세지 전송 } @Transactional - public void deleteFavorite(String userId, String type, String id) { - Member member = memberRepository.findByUserId(userId) - .orElseThrow(() -> new RuntimeException("사용자 없음")); + public ResponseEntity> deleteFavorite(String userId, String type, String id) { + + Map response = new HashMap<>(); - Favorite favorite = favoriteRepository.findByMemberAndTypeAndPlaceId(member, type, id) - .orElseThrow(() -> new RuntimeException("즐겨찾기 없음")); + Member member = memberRepository.findByUserId(userId).orElse(null); + if (member == null) { + response.put("message", "사용자 없음"); + return ResponseEntity.status(400).body(response); + } + + Favorite favorite = favoriteRepository.findByMemberAndTypeAndPlaceId(member, type, id).orElse(null); + if (favorite == null) { + response.put("message", "즐겨찾기 없음"); + return ResponseEntity.status(400).body(response); + } favoriteRepository.delete(favorite); + response.put("message", "즐겨찾기 삭제 완료"); + return ResponseEntity.ok(response); } - public List getAllFavoriteData() { + public List> getAllFavoriteData() { try{ List items = favoriteRepository.findAll(); - return items.stream() - .map(item -> new FavoriteDto( - item.getFavoriteId(), - item.getType(), - item.getPlaceId(), - item.getMember().getUserId() - )).toList(); + List favoriteDtoList = items.stream() + .map(item ->{ + ResponseEntity response = restTemplate.exchange( + "http://localhost:8082/main/info/"+item.getType()+"/" + item.getPlaceId(), + HttpMethod.GET, + null, + JsonNode.class + ); + String name = response.getBody().get("name").asText(); + String address = response.getBody().get("address").asText(); + + return new FavoriteDto( + item.getFavoriteId(), + item.getType(), + item.getPlaceId(), + item.getMember().getUserId(), + name, + address); + }).toList(); + + Map> grouped = favoriteDtoList.stream() + .collect(Collectors.groupingBy(FavoriteDto::getUser_id)); + + // 원하는 형태로 변환 + List> result = grouped.entrySet().stream() + .map(entry -> { + Map map = new HashMap<>(); + map.put("user_id", entry.getKey()); + map.put("content", entry.getValue()); + return map; + }) + .toList(); + + return result; } catch (Exception e) { throw new RuntimeException(e); From 5279847b338c89df44dde7772d9328e372f152e1 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 30 Apr 2025 13:49:39 +0900 Subject: [PATCH 225/361] =?UTF-8?q?gateway=20tokenrelay=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 50 ++++++++++++++++++- gateway/build.gradle | 5 ++ .../gateway/config/SecurityConfig.java | 5 ++ .../filter/JwtAuthenticationFilter.java | 4 ++ .../gateway/security/JwtTokenProvider.java | 4 ++ user-service/Dockerfile | 43 ++++++++++++++++ 6 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 gateway/src/main/java/com/example/gateway/config/SecurityConfig.java create mode 100644 gateway/src/main/java/com/example/gateway/filter/JwtAuthenticationFilter.java create mode 100644 gateway/src/main/java/com/example/gateway/security/JwtTokenProvider.java create mode 100644 user-service/Dockerfile diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7895ce6..12bc95e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -194,4 +194,52 @@ jobs: echo "태그 확인: gateway=$ECR_REGISTRY/gateway:$IMAGE_TAG" kubectl set image deployment/gateway gateway=$ECR_REGISTRY/gateway:$IMAGE_TAG - kubectl rollout status deployment/gateway \ No newline at end of file + kubectl rollout status deployment/gateway + + + user-service-build: + runs-on: ubuntu-latest + env: + IMAGE_TAG: 1.${{ github.run_number }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: 자바 설치 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: '17' + + - name: kubeconfig 설정 + run: | + aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster + echo "Step output: ${{ steps.set-ecr.outputs.registry }}" + - name: Build & Push gateway + env: # ecr 업로드 할 때 필요한 변수들 선언 + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # ecr 경로 + run: | + cd user-service + echo "${{ secrets.BACKEND_USER_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/user-service:$IMAGE_TAG . + docker tag $ECR_REGISTRY/user-service:$IMAGE_TAG $ECR_REGISTRY/user-service:latest + + ls -lh build/libs + + docker push $ECR_REGISTRY/user-service:$IMAGE_TAG + docker push $ECR_REGISTRY/user-service:latest + + kubectl set image deployment/user-service user-service=$ECR_REGISTRY/user-service:$IMAGE_TAG + kubectl rollout status deployment/user-service \ No newline at end of file diff --git a/gateway/build.gradle b/gateway/build.gradle index 809df21..e5bcefb 100644 --- a/gateway/build.gradle +++ b/gateway/build.gradle @@ -27,6 +27,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.cloud:spring-cloud-starter-gateway' implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/gateway/src/main/java/com/example/gateway/config/SecurityConfig.java b/gateway/src/main/java/com/example/gateway/config/SecurityConfig.java new file mode 100644 index 0000000..f90d1af --- /dev/null +++ b/gateway/src/main/java/com/example/gateway/config/SecurityConfig.java @@ -0,0 +1,5 @@ +package com.example.gateway.config; + +public class SecurityConfig { + +} diff --git a/gateway/src/main/java/com/example/gateway/filter/JwtAuthenticationFilter.java b/gateway/src/main/java/com/example/gateway/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..7711023 --- /dev/null +++ b/gateway/src/main/java/com/example/gateway/filter/JwtAuthenticationFilter.java @@ -0,0 +1,4 @@ +package com.example.gateway.filter; + +public class JwtAuthenticationFilter { +} diff --git a/gateway/src/main/java/com/example/gateway/security/JwtTokenProvider.java b/gateway/src/main/java/com/example/gateway/security/JwtTokenProvider.java new file mode 100644 index 0000000..83ac1fb --- /dev/null +++ b/gateway/src/main/java/com/example/gateway/security/JwtTokenProvider.java @@ -0,0 +1,4 @@ +package com.example.gateway.security; + +public class JwtTokenProvider { +} diff --git a/user-service/Dockerfile b/user-service/Dockerfile new file mode 100644 index 0000000..54d2456 --- /dev/null +++ b/user-service/Dockerfile @@ -0,0 +1,43 @@ +# 멀티스테이징 도커 파일 구성 + +# 소스코드를 빌드 +# 빌드의 결과물 jar -> 가동 +# 실습, 위의 목적을 달성하는 도커 파일 생성 + +# 스테이지 1-> jar를 생성(빌드), 과도기적 이미지, 용량이 커도 무방 +# 그레이들 빌드 도구와 자바가 세팅된 이미지로부터 이미지 구성 +FROM gradle:7.6-jdk AS builder + +# 워킹 디렉토리(리눅스 기반 설정) 지정 +WORKDIR /app + +# 호스트 OS에서 백엔드 원소스 전체 카피 + +COPY . . + +# 빌드 +RUN chmod +x ./gradlew +RUN ./gradlew clean build -x test --no-daemon + + +# 최종 산출물 : /app/build/libs/*SNAPSHOT.jar + +# 스테이지 2 -> jar를 가동 -> 경량!! -> 배포속도가 상승 +# JDK17이 설치된 경량 버전의 리눅스 준비 +FROM openjdk:17-jdk-slim + +# 컨테이너 내부에 작업디렉토리 지정 +WORKDIR /app + +# jar 복사 +# 스테이지1으로부터 특정위치에 존재하는 산출물을 현재 컨테이너의 작업디렉토리 루트에 복사 + +COPY --from=builder /app/build/libs/*SNAPSHOT.jar ./user.jar +# 포트지정 + +COPY .env .env + +EXPOSE 8083 + +# 서버가동 +CMD ["java", "-jar", "user.jar"] \ No newline at end of file From 2eb542173754b1fb967b49b4bb9538f5e07c86ba Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Tue, 29 Apr 2025 10:46:12 +0900 Subject: [PATCH 226/361] =?UTF-8?q?push=20and=20=ED=86=B5=ED=95=A9?= =?UTF-8?q?=EA=B2=80=EC=83=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PlaceSearchController.java | 29 +++++ .../placeservice/dto/PlaceDocument.java | 14 +++ .../placeservice/entity/Accommodation.java | 2 + .../repository/AccommodationRepository.java | 2 + .../repository/AttractionRepository.java | 5 + .../repository/CafeRepository.java | 3 + .../repository/CulturalEventRepository.java | 4 + .../repository/PlaceDocumentRepository.java | 22 ++++ .../repository/RestaurantRepository.java | 5 + .../service/PlaceDocumentMapper.java | 50 +++++++++ .../service/PlaceDocumentService.java | 85 ++++++++++++++ .../src/main/resources/application.properties | 4 +- user-service/.gitignore | 1 - .../userservice/config/ApplicationConfig.java | 25 +++-- .../userservice/config/SecurityConfig.java | 22 ++-- .../userservice/controller/AdminSignup.java | 104 +++++++++++++++--- .../controller/AuthController.java | 61 +++++++++- .../controller/MemberController.java | 73 ++++++++++++ .../com/example/userservice/dto/AuthDto.java | 38 +++++-- .../example/userservice/dto/MemberDto.java | 49 +++++++-- .../example/userservice/dto/MemberSign.java | 21 ++-- .../example/userservice/entity/Member.java | 24 ++-- .../userservice/entity/RefreshToken.java | 6 +- .../redis/RefreshTokenRepository.java | 5 +- .../security/JwtAuthenticationFilter.java | 17 ++- .../userservice/service/AuthService.java | 26 ++++- .../userservice/service/MemberService.java | 92 +++++++++++++++- .../userservice/service/TokenService.java | 30 ++++- 28 files changed, 724 insertions(+), 95 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/controller/PlaceSearchController.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/PlaceDocumentRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/PlaceDocumentService.java create mode 100644 user-service/src/main/java/com/example/userservice/controller/MemberController.java diff --git a/place-service/src/main/java/com/example/placeservice/controller/PlaceSearchController.java b/place-service/src/main/java/com/example/placeservice/controller/PlaceSearchController.java new file mode 100644 index 0000000..a358571 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/PlaceSearchController.java @@ -0,0 +1,29 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.dto.PlaceDocument; +import com.example.placeservice.service.PlaceDocumentService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/search") +public class PlaceSearchController { + + private final PlaceDocumentService placeSearchService; + + @GetMapping("/{keyword}") + public List searchByName(@PathVariable String keyword) { + return placeSearchService.searchPlacesByName(keyword); + } + + @GetMapping("/address/{keyword}") + public List searchByAddress(@PathVariable String keyword) { + return placeSearchService.searchPlacesByAddress(keyword); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java b/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java new file mode 100644 index 0000000..b91c9ba --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java @@ -0,0 +1,14 @@ +package com.example.placeservice.dto; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class PlaceDocument { + + // 공통 + private String name; + private String category; + private String address; +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java index 79d051a..9c1bc03 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java @@ -2,11 +2,13 @@ import jakarta.persistence.*; import lombok.Data; +import lombok.Getter; import java.math.BigDecimal; @Data @Entity +@Getter public class Accommodation { @Id @Column(name = "accommodation_id") diff --git a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java index eaedae0..cc93c89 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java @@ -11,4 +11,6 @@ public interface AccommodationRepository extends JpaRepository findByGu(String gu); List findByType(String type); + List findByNameContainingIgnoreCase(String keyword); + List findByAddressContainingIgnoreCase(String keyword); } diff --git a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java index f43538e..fcd46e0 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java @@ -1,12 +1,17 @@ package com.example.placeservice.repository; +import com.example.placeservice.entity.Accommodation; import com.example.placeservice.entity.Attraction; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface AttractionRepository extends JpaRepository { boolean existsBySeoulAttractionId(String seoul_attraction_id); Optional findByAttractionId(Long attraction_id); + List findByNameContainingIgnoreCase(String keyword); + + List findByAddressContainingIgnoreCase(String keyword); } diff --git a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java index 51c1a6d..400ffd1 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java @@ -1,5 +1,6 @@ package com.example.placeservice.repository; +import com.example.placeservice.entity.Accommodation; import com.example.placeservice.entity.Cafe; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; @@ -18,4 +19,6 @@ public interface CafeRepository extends JpaRepository { void deleteByAreaId(Long areaId); Optional findByKakaomapUrl(String placeCode); + List findByNameContainingIgnoreCase(String keyword); + List findByAddressContainingIgnoreCase(String keyword); } \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java index f238de4..95182d0 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java @@ -1,14 +1,18 @@ // CulturalEventRepository.java package com.example.placeservice.repository; +import com.example.placeservice.entity.Accommodation; import com.example.placeservice.entity.CulturalEvent; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; +import java.util.List; @Repository public interface CulturalEventRepository extends JpaRepository { boolean existsByTitleAndAddressAndStartDate(String title, String address, LocalDateTime startDate); + List findBytitleContainingIgnoreCase(String keyword); + List findByAddressContainingIgnoreCase(String keyword); } diff --git a/place-service/src/main/java/com/example/placeservice/repository/PlaceDocumentRepository.java b/place-service/src/main/java/com/example/placeservice/repository/PlaceDocumentRepository.java new file mode 100644 index 0000000..96a280f --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/PlaceDocumentRepository.java @@ -0,0 +1,22 @@ +package com.example.placeservice.repository; + +import com.example.placeservice.dto.PlaceDocument; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; + +@Repository +public class PlaceDocumentRepository { + + private final List placeDocuments = new ArrayList<>(); + + public List findAll() { + return placeDocuments; + } + + public void saveAll(List documents) { + placeDocuments.clear(); + placeDocuments.addAll(documents); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java index 6f04f0a..e7fc6c8 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java @@ -1,9 +1,14 @@ package com.example.placeservice.repository; +import com.example.placeservice.entity.Accommodation; import com.example.placeservice.entity.Restaurant; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface RestaurantRepository extends JpaRepository { // 기본 CRUD 사용 (findAll, findById, save 등) + List findByNameContainingIgnoreCase(String keyword); + List findByAddressContainingIgnoreCase(String keyword); } \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java new file mode 100644 index 0000000..7d7791f --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java @@ -0,0 +1,50 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.PlaceDocument; + +import com.example.placeservice.entity.*; +import org.springframework.stereotype.Component; + +@Component +public class PlaceDocumentMapper { + + public PlaceDocument toPlaceDocument(Accommodation item) { + return PlaceDocument.builder() + .name(item.getName()) + .address(item.getAddress()) + .category("accommodation") + .build(); + } + + public PlaceDocument toPlaceDocument(Cafe item) { + return PlaceDocument.builder() + .name(item.getName()) + .address(item.getAddress()) + .category("cafe") + .build(); + } + + public PlaceDocument toPlaceDocument(CulturalEvent item) { + return PlaceDocument.builder() + .name(item.getTitle()) + .address(item.getAddress()) + .category("culturalevent") + .build(); + } + + public PlaceDocument toPlaceDocument(Restaurant item) { + return PlaceDocument.builder() + .name(item.getName()) + .address(item.getAddress()) + .category("restaurant") + .build(); + } + + public PlaceDocument toPlaceDocument(Attraction item) { + return PlaceDocument.builder() + .name(item.getName()) + .address(item.getAddress()) + .category("attraction") + .build(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentService.java b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentService.java new file mode 100644 index 0000000..70abe76 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentService.java @@ -0,0 +1,85 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.accommodation.AccommodationDto; +import com.example.placeservice.dto.PlaceDocument; +import com.example.placeservice.service.PlaceDocumentMapper; +import com.example.placeservice.entity.*; +import com.example.placeservice.repository.*; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; + + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + + +@Service +@RequiredArgsConstructor +@Slf4j +public class PlaceDocumentService { + + private final AccommodationRepository accommodationRepository; + private final RestaurantRepository restaurantRepository; + private final AttractionRepository attractionRepository; + private final CulturalEventRepository culturalEventRepository; + private final CafeRepository cafeRepository; + + + private final PlaceDocumentRepository placeDocumentRepository; + private final PlaceDocumentMapper placeDocumentMapper; + + public List searchPlacesByName(String keyword) { + List results = new ArrayList<>(); + + // Accommodation, Attraction, Cafe, Restaurant, CulturalEvent에서 이름이 포함된 객체만 필터링 + results.addAll(accommodationRepository.findByNameContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + results.addAll(attractionRepository.findByNameContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + results.addAll(cafeRepository.findByNameContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + results.addAll(restaurantRepository.findByNameContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + results.addAll(culturalEventRepository.findBytitleContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + // 이름에만 keyword가 포함된 것만 남긴다. + return results.stream() + .filter(place -> place.getName().toLowerCase().contains(keyword.toLowerCase())) + .collect(Collectors.toList()); + } + + public List searchPlacesByAddress(String keyword) { + List results = new ArrayList<>(); + + // Accommodation, Attraction, Cafe, Restaurant, CulturalEvent에서 주소가 포함된 객체만 필터링 + results.addAll(accommodationRepository.findByAddressContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + results.addAll(attractionRepository.findByAddressContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + results.addAll(cafeRepository.findByAddressContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + results.addAll(restaurantRepository.findByAddressContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + results.addAll(culturalEventRepository.findByAddressContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + // 주소에만 keyword가 포함된 것만 남긴다. + return results.stream() + .filter(place -> place.getAddress().toLowerCase().contains(keyword.toLowerCase())) + .collect(Collectors.toList()); + } + + +} diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 27952cf..638be00 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -9,8 +9,8 @@ spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.defer-datasource-initialization=true -spring.jpa.hibernate.ddl-auto=update -# spring.jpa.hibernate.ddl-auto=create +#spring.jpa.hibernate.ddl-auto=update + spring.jpa.hibernate.ddl-auto=create spring.jpa.show-sql=true #spring.sql.init.mode=always diff --git a/user-service/.gitignore b/user-service/.gitignore index 1f22f61..261a098 100644 --- a/user-service/.gitignore +++ b/user-service/.gitignore @@ -37,5 +37,4 @@ out/ ### VS Code ### .vscode/ -.env diff --git a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java index 5e16036..44f2998 100644 --- a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java @@ -8,11 +8,16 @@ import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + @Configuration @RequiredArgsConstructor public class ApplicationConfig { @@ -22,12 +27,18 @@ public class ApplicationConfig { @Bean public UserDetailsService userDetailsService() { return username -> memberRepository.findByUserId(username) - .map(member -> new org.springframework.security.core.userdetails.User( - member.getUserId(), // userId를 사용하도록 변경 - member.getPassword(), - java.util.Collections.emptyList() - )) - .orElseThrow(() -> new UsernameNotFoundException("User not found")); + .map(member -> { + List authorities = new ArrayList<>(); + // 사용자 역할에 따른 권한 추가 + authorities.add(new SimpleGrantedAuthority(member.getRole())); + + return new org.springframework.security.core.userdetails.User( + member.getUserId(), // userId를 사용자 이름으로 사용 + member.getPassword(), // 암호화된 비밀번호 + authorities // 권한 목록 + ); + }) + .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다")); } @Bean @@ -45,6 +56,6 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration c @Bean public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); + return new BCryptPasswordEncoder(); // 비밀번호 암호화에 BCrypt 알고리즘 사용 } } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java index 0868b9b..9faf817 100644 --- a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -22,13 +23,22 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .csrf(csrf -> csrf.disable()) + .csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화 .authorizeHttpRequests(auth -> auth + // 공개 엔드포인트 - 인증 불필요 .requestMatchers("/auth/**").permitAll() - .requestMatchers("/admin/**").authenticated() + + // 관리자 전용 엔드포인트 + .requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN") + + // 사용자/관리자 접근 가능 엔드포인트 + .requestMatchers("/mypage/**").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN") + + // 그 외 모든 요청은 인증 필요 .anyRequest().authenticated() ) .sessionManagement(session -> session + // 세션 상태 없음 (JWT 사용하므로) .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authenticationProvider(authenticationProvider) @@ -36,10 +46,4 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } - - //비밀번호 암호화 적용 -// @Bean -// public BCryptPasswordEncoder passwordEncoder() { -// return new BCryptPasswordEncoder(); -// } -} +} \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java index 073cc06..e069e82 100644 --- a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java +++ b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java @@ -4,51 +4,125 @@ import com.example.userservice.entity.Member; import com.example.userservice.security.JwtUtil; import com.example.userservice.service.MemberService; +import com.example.userservice.service.TokenService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; -@Controller +@RestController @RequiredArgsConstructor @RequestMapping("/auth") public class AdminSignup { private final MemberService memberService; private final JwtUtil jwtUtil; + private final TokenService tokenService; + /** + * 일반 사용자 회원가입 API + * @param dto 회원가입 정보 (user_id, password, nickname, birth_year, mbti, gender) + * @return 토큰 및 사용자 정보가 포함된 응답 + */ @PostMapping("/signup") public ResponseEntity> signup(@RequestBody MemberSign dto) { try { // 디버깅 로그 출력 - System.out.println("회원가입 요청: userId=" + dto.getUserId() + ", nickname=" + dto.getNickname() + ", birth_year=" + dto.getBirthYear()); + System.out.println("회원가입 요청: userId=" + dto.getUserId() + ", nickname=" + dto.getNickname() + + ", birth_year=" + dto.getBirthYear() + + ", mbti=" + dto.getMbti() + + ", gender=" + dto.getGender()); - Member member = memberService.registerAdmin(dto); + // 일반 사용자로 등록 + Member member = memberService.registerUser(dto); + + System.out.println("회원가입 성공: memberId=" + member.getMemberId() + + ", userId=" + member.getUserId() + + ", nickname=" + member.getNickname() + + ", role=" + member.getRole()); - System.out.println("회원가입 성공: memberId=" + member.getMemberId() + ", userId=" + member.getUserId() + ", nickname=" + member.getNickname()); + // 사용자 권한 설정 + List authorities = Collections.singletonList( + new SimpleGrantedAuthority(member.getRole()) + ); - // 회원가입 성공하면 토큰 발급 - userId 사용 - UserDetails userDetails = new User(member.getUserId(), member.getPassword(), Collections.emptyList()); + // 회원가입 성공하면 토큰 발급 + UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities); String accessToken = jwtUtil.generateToken(userDetails); String refreshToken = jwtUtil.generateRefreshToken(userDetails); - Map tokens = new HashMap<>(); - tokens.put("accessToken", accessToken); - tokens.put("refreshToken", refreshToken); - tokens.put("member_id", String.valueOf(member.getMemberId())); - tokens.put("user_id", member.getUserId()); - tokens.put("nickname", member.getNickname()); + // 리프레시 토큰 저장 + tokenService.saveRefreshToken(member.getMemberId(), refreshToken); + + // 응답 생성 + Map response = new HashMap<>(); + response.put("accessToken", accessToken); + response.put("refreshToken", refreshToken); + response.put("member_id", String.valueOf(member.getMemberId())); + response.put("user_id", member.getUserId()); + response.put("nickname", member.getNickname()); + response.put("role", member.getRole()); - return ResponseEntity.ok(tokens); - } catch (IllegalArgumentException e) { + return ResponseEntity.ok(response); + } catch (IllegalStateException e) { Map errorResponse = new HashMap<>(); errorResponse.put("error", "회원가입 실패: " + e.getMessage()); return ResponseEntity.badRequest().body(errorResponse); } } + + /** + * 관리자 회원가입 API + * @param dto 관리자 회원가입 정보 + * @return 토큰 및 관리자 정보가 포함된 응답 + */ + @PostMapping("/admin/signup") + public ResponseEntity> adminSignup(@RequestBody MemberSign dto) { + try { + // 디버깅 로그 출력 + System.out.println("관리자 회원가입 요청: userId=" + dto.getUserId() + ", nickname=" + dto.getNickname()); + + // 관리자로 등록 + Member member = memberService.registerAdmin(dto); + + System.out.println("관리자 회원가입 성공: memberId=" + member.getMemberId() + + ", userId=" + member.getUserId() + + ", nickname=" + member.getNickname() + + ", role=" + member.getRole()); + + // 관리자 권한 설정 + List authorities = Collections.singletonList( + new SimpleGrantedAuthority(member.getRole()) + ); + + // 회원가입 성공하면 토큰 발급 + UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities); + String accessToken = jwtUtil.generateToken(userDetails); + String refreshToken = jwtUtil.generateRefreshToken(userDetails); + + // 리프레시 토큰 저장 + tokenService.saveRefreshToken(member.getMemberId(), refreshToken); + + // 응답 생성 + Map response = new HashMap<>(); + response.put("accessToken", accessToken); + response.put("refreshToken", refreshToken); + response.put("member_id", String.valueOf(member.getMemberId())); + response.put("user_id", member.getUserId()); + response.put("nickname", member.getNickname()); + response.put("role", member.getRole()); + + return ResponseEntity.ok(response); + } catch (IllegalStateException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put("error", "관리자 회원가입 실패: " + e.getMessage()); + return ResponseEntity.badRequest().body(errorResponse); + } + } } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/controller/AuthController.java b/user-service/src/main/java/com/example/userservice/controller/AuthController.java index 9defdce..cc38cbf 100644 --- a/user-service/src/main/java/com/example/userservice/controller/AuthController.java +++ b/user-service/src/main/java/com/example/userservice/controller/AuthController.java @@ -1,8 +1,13 @@ package com.example.userservice.controller; import com.example.userservice.dto.AuthDto; +import com.example.userservice.entity.Member; +import com.example.userservice.repository.jpa.MemberRepository; +import com.example.userservice.security.JwtUtil; import com.example.userservice.service.AuthService; import com.example.userservice.service.CookieUtil; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -18,7 +23,15 @@ public class AuthController { private final AuthService authService; private final CookieUtil cookieUtil; + private final JwtUtil jwtUtil; + private final MemberRepository memberRepository; + /** + * 로그인 API + * @param request 로그인 요청 (user_id, password) + * @param response HTTP 응답 (쿠키 설정용) + * @return 로그인 성공 시 토큰 및 사용자 정보 + */ @PostMapping("/login") public ResponseEntity login( @RequestBody AuthDto.LoginRequest request, @@ -36,16 +49,58 @@ public ResponseEntity login( return ResponseEntity.ok(loginResponse); } + /** + * 로그아웃 API + * @param request HTTP 요청 (토큰 추출용) + * @param response HTTP 응답 (쿠키 삭제용) + * @return 로그아웃 성공 여부 및 메시지 + */ @PostMapping("/logout") public ResponseEntity logout( - @RequestBody AuthDto.LogoutRequest request, + HttpServletRequest request, HttpServletResponse response) { // 쿠키 삭제 cookieUtil.addCookie(response, "access_token", "", 0); cookieUtil.addCookie(response, "refresh_token", "", 0); - // authService의 logout 메서드 호출 - return ResponseEntity.ok(authService.logout(request)); + // 헤더에서 토큰 추출 + String authHeader = request.getHeader("Authorization"); + String jwt = null; + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + jwt = authHeader.substring(7); + } else { + // 쿠키에서 토큰 찾기 (선택적) + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if ("access_token".equals(cookie.getName())) { + jwt = cookie.getValue(); + break; + } + } + } + } + + if (jwt != null) { + // 토큰에서 사용자 정보 추출 + String userId = jwtUtil.extractUsername(jwt); + // 사용자 ID로 Member 조회 + Member member = memberRepository.findByUserId(userId) + .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다")); + + // 로그아웃 처리 + return ResponseEntity.ok(authService.logout( + AuthDto.LogoutRequest.builder() + .memberId(String.valueOf(member.getMemberId())) + .build())); + } + + return ResponseEntity.badRequest().body( + AuthDto.LogoutResponse.builder() + .success(false) + .message("유효한 토큰이 없습니다.") + .build()); } } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/controller/MemberController.java b/user-service/src/main/java/com/example/userservice/controller/MemberController.java new file mode 100644 index 0000000..d51b3e1 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/controller/MemberController.java @@ -0,0 +1,73 @@ +package com.example.userservice.controller; + +import com.example.userservice.dto.MemberDto; +import com.example.userservice.security.JwtUtil; +import com.example.userservice.service.MemberService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +public class MemberController { + + private final MemberService memberService; + private final JwtUtil jwtUtil; + + /** + * 사용자 프로필 조회 API + * @param request HTTP 요청 (토큰에서 사용자 ID 추출용) + * @return 사용자 프로필 정보 + */ + @GetMapping("/mypage/profile") + public ResponseEntity getProfile(HttpServletRequest request) { + // 현재 인증된 사용자의 userId 추출 + String userId = extractUserIdFromRequest(request); + + // 서비스를 통해 프로필 정보 조회 + MemberDto.ProfileResponse profile = memberService.getProfile(userId); + + return ResponseEntity.ok(profile); + } + + /** + * 사용자 프로필 수정 API + * @param request HTTP 요청 (토큰에서 사용자 ID 추출용) + * @param updateRequest 프로필 수정 정보 (닉네임, 생년월일, MBTI, 성별, 비밀번호 등) + * @return 업데이트된 사용자 정보 + */ + @PostMapping("/mypage/profile/edit") + public ResponseEntity updateProfile( + HttpServletRequest request, + @RequestBody MemberDto.UpdateRequest updateRequest) { + + // 현재 인증된 사용자의 userId 추출 + String userId = extractUserIdFromRequest(request); + + // 서비스를 통해 프로필 정보 업데이트 + MemberDto.MemberResponse updatedProfile = memberService.updateProfile(userId, updateRequest); + + return ResponseEntity.ok(updatedProfile); + } + + /** + * JWT 토큰에서 userId 추출 헬퍼 메서드 + * @param request HTTP 요청 + * @return 추출된 사용자 ID + */ + private String extractUserIdFromRequest(HttpServletRequest request) { + String authHeader = request.getHeader("Authorization"); + String jwt = null; + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + jwt = authHeader.substring(7); + } + + if (jwt != null) { + return jwtUtil.extractUsername(jwt); + } + + throw new RuntimeException("인증 토큰이 유효하지 않습니다."); + } +} \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/dto/AuthDto.java b/user-service/src/main/java/com/example/userservice/dto/AuthDto.java index dfa8fc8..54cac85 100644 --- a/user-service/src/main/java/com/example/userservice/dto/AuthDto.java +++ b/user-service/src/main/java/com/example/userservice/dto/AuthDto.java @@ -7,53 +7,69 @@ public class AuthDto { + /** + * 로그인 요청 DTO + */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public static class LoginRequest { private String user_id; // 로그인 시 사용자가 입력하는 ID - private String password; + private String password; // 비밀번호 } + /** + * 로그인 응답 DTO + */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public static class LoginResponse { - private String accessToken; - private String refreshToken; - private String member_id; // 내부 관리용 ID - private String user_id; // 로그인용 ID - private String nickname; + private String accessToken; // 액세스 토큰 + private String refreshToken; // 리프레시 토큰 + private String member_id; // 내부 관리용 ID + private String user_id; // 로그인용 ID + private String nickname; // 닉네임 + private String role; // 사용자 역할 (ROLE_USER or ROLE_ADMIN) } + /** + * 로그아웃 요청 DTO + */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public static class LogoutRequest { - private String memberId; + private String memberId; // 회원 ID public String getMemberId() { return memberId; } } + /** + * 로그아웃 응답 DTO + */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public static class LogoutResponse { - private boolean success; - private String message; + private boolean success; // 성공 여부 + private String message; // 메시지 } + /** + * 토큰 갱신 요청 DTO + */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public static class RefreshTokenRequest { - private String refreshToken; + private String refreshToken; // 리프레시 토큰 } -} +} \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java index 3648c46..8b6e619 100644 --- a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java +++ b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java @@ -9,36 +9,61 @@ public class MemberDto { + // 회원 정보 응답 DTO @Data @Builder @AllArgsConstructor @NoArgsConstructor public static class MemberResponse { - private String user_id; - private String nickname; - private Short birth_year; - private LocalDateTime created_at; + private String user_id; // 로그인 ID + private String nickname; // 닉네임 + private Short birth_year; // 출생연도 + private String mbti; // MBTI 유형 + private String gender; // 성별 + private String role; // 역할 (ROLE_USER, ROLE_ADMIN) + private LocalDateTime created_at; // 계정 생성일 } + // 프로필 조회 응답 DTO + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class ProfileResponse { + private String nickname; // 닉네임 + private Short birth_year; // 출생연도 + private String mbti; // MBTI 유형 + private String gender; // 성별 + private String role; // 역할 (ROLE_USER, ROLE_ADMIN) + private LocalDateTime created_at; // 계정 생성일 + } + + // 프로필 업데이트 요청 DTO @Data @Builder @AllArgsConstructor @NoArgsConstructor public static class UpdateRequest { - private String nickname; - private Short birth_year; - private String current_password; - private String new_password; + private String nickname; // 변경할 닉네임 + private Short birth_year; // 변경할 출생연도 + private String mbti; // 변경할 MBTI + private String gender; // 변경할 성별 + private String current_password; // 현재 비밀번호 (비밀번호 변경 시 필요) + private String new_password; // 새 비밀번호 (비밀번호 변경 시 필요) } + // 회원 등록 요청 DTO @Data @Builder @AllArgsConstructor @NoArgsConstructor public static class RegisterRequest { - private String user_id; - private String password; - private String nickname; - private Short birth_year; + private String user_id; // 로그인 ID + private String password; // 비밀번호 + private String nickname; // 닉네임 + private Short birth_year; // 출생연도 + private String mbti; // MBTI 유형 + private String gender; // 성별 + private String role; // 역할 (옵션) } } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java index 058318e..c9f4e94 100644 --- a/user-service/src/main/java/com/example/userservice/dto/MemberSign.java +++ b/user-service/src/main/java/com/example/userservice/dto/MemberSign.java @@ -7,13 +7,20 @@ @Getter @Setter public class MemberSign { - @JsonProperty("user_id") // JSON 필드 이름 매핑 - private String userId; + @JsonProperty("user_id") // JSON 필드와 매핑 + private String userId; // 로그인에 사용할 ID - private String password; - private String nickname; - private String createTime; + private String password; // 비밀번호 - @JsonProperty("birth_year") // JSON 필드 이름 매핑 - private Short birthYear; + private String nickname; // 닉네임 + + @JsonProperty("birth_year") // JSON 필드와 매핑 + private Short birthYear; // 출생연도 + + private String mbti; // MBTI 유형 + + private String gender; // 성별 + + // 관리자 회원가입용 옵션 필드 + private String role; // 사용자 권한 (기본값 ROLE_USER, ROLE_ADMIN 가능) } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/entity/Member.java b/user-service/src/main/java/com/example/userservice/entity/Member.java index 3815518..5df9b7c 100644 --- a/user-service/src/main/java/com/example/userservice/entity/Member.java +++ b/user-service/src/main/java/com/example/userservice/entity/Member.java @@ -19,21 +19,29 @@ public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "member_id", nullable = false) - private Long memberId; + private Long memberId; // 회원 고유 ID (시스템 내부용) @Column(name = "user_id", nullable = false, unique = true) - private String userId; - + private String userId; // 사용자 로그인 ID @Column(nullable = false) - private String password; + private String password; // 비밀번호 @Column - private String nickname; + private String nickname; // 닉네임 @Column(name = "birth_year") - private Short birthYear; + private Short birthYear; // 출생연도 + + @Column(name = "mbti") + private String mbti; // MBTI 유형 + + @Column(name = "gender") + private String gender; // 성별 @Column(name = "created_at") - private LocalDateTime createdAt; -} + private LocalDateTime createdAt; // 계정 생성일 + + @Column(name = "role", nullable = false) + private String role; // 사용자 권한 ("ROLE_ADMIN" 또는 "ROLE_USER") +} \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java index 6612a36..fe4a75d 100644 --- a/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java +++ b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java @@ -14,6 +14,6 @@ @RedisHash(value = "refresh_token", timeToLive = 60 * 60 * 24 * 7) // 7일 저장 public class RefreshToken { @Id - private String id; // memberId를 String으로 저장 - private String refreshToken; -} + private String id; // memberId를 String으로 저장 (Redis 키) + private String refreshToken; // 리프레시 토큰 값 +} \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java b/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java index f871368..a680309 100644 --- a/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java +++ b/user-service/src/main/java/com/example/userservice/repository/redis/RefreshTokenRepository.java @@ -1,13 +1,12 @@ package com.example.userservice.repository.redis; import com.example.userservice.entity.RefreshToken; -import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository -public interface RefreshTokenRepository extends JpaRepository { - Optional findByUserId(Long userId); +public interface RefreshTokenRepository extends CrudRepository { Optional findByRefreshToken(String refreshToken); } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java index b917cea..7f40fe9 100644 --- a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java +++ b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java @@ -26,7 +26,12 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final UserDetailsService userDetailsService; // 필터링하지 않을 URL 패턴 목록 - private final List excludedPaths = Arrays.asList("/auth/signup", "/auth/login", "/auth/logout"); + private final List excludedPaths = Arrays.asList( + "/auth/signup", + "/auth/admin/signup", + "/auth/login", + "/auth/logout" + ); @Override protected void doFilterInternal( @@ -36,7 +41,6 @@ protected void doFilterInternal( ) throws ServletException, IOException { // 인증이 필요없는 경로는 필터를 적용하지 않음 - String requestPath = request.getServletPath(); if (shouldNotFilter(request)) { filterChain.doFilter(request, response); return; @@ -48,7 +52,7 @@ protected void doFilterInternal( // Authorization 헤더가 있으면 해당 토큰 사용 if (authHeader != null && authHeader.startsWith("Bearer ")) { - jwt = authHeader.substring(7); + jwt = authHeader.substring(7); // "Bearer " 이후의 문자열 추출 } // 아니면 쿠키에서 토큰 찾기 else { @@ -70,21 +74,26 @@ protected void doFilterInternal( } try { + // 토큰에서 사용자 이름(userId) 추출 final String username = jwtUtil.extractUsername(jwt); + // 보안 컨텍스트에 인증 정보가 없으면 새로 설정 if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + // 토큰이 유효한지 확인 if (jwtUtil.validateToken(jwt, userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, - userDetails.getAuthorities() + userDetails.getAuthorities() // 사용자 권한 포함 ); authToken.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); SecurityContextHolder.getContext().setAuthentication(authToken); + + logger.info("사용자 인증 성공: " + username + " (권한: " + userDetails.getAuthorities() + ")"); } } } catch (Exception e) { diff --git a/user-service/src/main/java/com/example/userservice/service/AuthService.java b/user-service/src/main/java/com/example/userservice/service/AuthService.java index 184877f..347ef7d 100644 --- a/user-service/src/main/java/com/example/userservice/service/AuthService.java +++ b/user-service/src/main/java/com/example/userservice/service/AuthService.java @@ -7,11 +7,13 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import java.util.Collections; +import java.util.List; @Service @RequiredArgsConstructor @@ -22,7 +24,11 @@ public class AuthService { private final AuthenticationManager authenticationManager; private final TokenService tokenService; - // 로그인 + /** + * 로그인 처리 + * @param request 로그인 요청 정보 (user_id, password) + * @return 토큰과 사용자 정보가 포함된 응답 + */ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) { System.out.println("로그인 시도: " + request.getUser_id()); @@ -30,7 +36,7 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) { Member member = memberRepository.findByUserId(request.getUser_id()) .orElseThrow(() -> { System.out.println("사용자를 찾을 수 없음: " + request.getUser_id()); - return new RuntimeException("User not found"); + return new RuntimeException("사용자를 찾을 수 없습니다"); }); System.out.println("사용자 찾음: ID=" + member.getMemberId() + ", UserID=" + member.getUserId()); @@ -49,8 +55,13 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) { throw e; } + // 사용자 권한 설정 + List authorities = Collections.singletonList( + new SimpleGrantedAuthority(member.getRole()) + ); + // 3. JWT 토큰 발급 - UserDetails userDetails = new User(member.getUserId(), member.getPassword(), Collections.emptyList()); + UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities); String accessToken = jwtUtil.generateToken(userDetails); String refreshToken = jwtUtil.generateRefreshToken(userDetails); @@ -64,10 +75,15 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) { .member_id(String.valueOf(member.getMemberId())) .user_id(member.getUserId()) .nickname(member.getNickname()) + .role(member.getRole()) .build(); } - // 로그아웃 + /** + * 로그아웃 처리 + * @param request 로그아웃 요청 정보 (memberId) + * @return 로그아웃 성공 여부와 메시지 + */ public AuthDto.LogoutResponse logout(AuthDto.LogoutRequest request) { // 1. 사용자의 Redis Refresh Token 삭제 Long memberId = Long.parseLong(request.getMemberId()); @@ -79,4 +95,4 @@ public AuthDto.LogoutResponse logout(AuthDto.LogoutRequest request) { .message("로그아웃 완료: RefreshToken 삭제 완료") .build(); } -} +} \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java index cba58f6..955c347 100644 --- a/user-service/src/main/java/com/example/userservice/service/MemberService.java +++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java @@ -1,8 +1,10 @@ package com.example.userservice.service; +import com.example.userservice.dto.MemberDto; import com.example.userservice.dto.MemberSign; import com.example.userservice.entity.Member; import com.example.userservice.repository.jpa.MemberRepository; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -15,24 +17,110 @@ public class MemberService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; + // 일반 사용자 회원가입 (기본 권한은 ROLE_USER) + public Member registerUser(MemberSign dto) { + return registerMember(dto, "ROLE_USER"); + } + + // 관리자 회원가입 public Member registerAdmin(MemberSign dto) { + return registerMember(dto, "ROLE_ADMIN"); + } + + // 공통 회원가입 로직 + private Member registerMember(MemberSign dto, String defaultRole) { // userId 중복 체크 if (memberRepository.existsByUserId(dto.getUserId())) { throw new IllegalStateException("이미 존재하는 사용자 ID입니다."); } + // 닉네임 중복 체크 if (memberRepository.existsByNickname(dto.getNickname())) { throw new IllegalStateException("이미 존재하는 닉네임입니다."); } + // DTO에서 역할이 지정되었고 관리자로 등록하는 경우 해당 역할 사용, 아니면 기본 역할 사용 + String role = dto.getRole() != null && defaultRole.equals("ROLE_ADMIN") + ? dto.getRole() + : defaultRole; + + // 회원 엔티티 생성 Member member = Member.builder() - .userId(dto.getUserId()) // userId 설정 - .password(passwordEncoder.encode(dto.getPassword())) + .userId(dto.getUserId()) + .password(passwordEncoder.encode(dto.getPassword())) // 비밀번호 암호화 .nickname(dto.getNickname()) .birthYear(dto.getBirthYear()) + .mbti(dto.getMbti()) + .gender(dto.getGender()) + .role(role) .createdAt(LocalDateTime.now()) .build(); + // 저장 후 반환 return memberRepository.save(member); } + + // 회원 정보 조회 + public MemberDto.ProfileResponse getProfile(String userId) { + Member member = memberRepository.findByUserId(userId) + .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); + + return MemberDto.ProfileResponse.builder() + .nickname(member.getNickname()) + .birth_year(member.getBirthYear()) + .mbti(member.getMbti()) + .gender(member.getGender()) + .role(member.getRole()) + .created_at(member.getCreatedAt()) + .build(); + } + + // 회원 정보 수정 + @Transactional + public MemberDto.MemberResponse updateProfile(String userId, MemberDto.UpdateRequest request) { + Member member = memberRepository.findByUserId(userId) + .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); + + // 비밀번호 변경이 요청된 경우 + if (request.getCurrent_password() != null && request.getNew_password() != null) { + // 현재 비밀번호 확인 + if (!passwordEncoder.matches(request.getCurrent_password(), member.getPassword())) { + throw new RuntimeException("현재 비밀번호가 일치하지 않습니다."); + } + + // 새 비밀번호로 업데이트 (암호화) + member.setPassword(passwordEncoder.encode(request.getNew_password())); + } + + // 기타 정보 업데이트 + if (request.getNickname() != null) { + member.setNickname(request.getNickname()); + } + + if (request.getBirth_year() != null) { + member.setBirthYear(request.getBirth_year()); + } + + if (request.getMbti() != null) { + member.setMbti(request.getMbti()); + } + + if (request.getGender() != null) { + member.setGender(request.getGender()); + } + + // 저장 + Member updatedMember = memberRepository.save(member); + + // 응답 DTO로 변환 + return MemberDto.MemberResponse.builder() + .user_id(updatedMember.getUserId()) + .nickname(updatedMember.getNickname()) + .birth_year(updatedMember.getBirthYear()) + .mbti(updatedMember.getMbti()) + .gender(updatedMember.getGender()) + .role(updatedMember.getRole()) + .created_at(updatedMember.getCreatedAt()) + .build(); + } } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/service/TokenService.java b/user-service/src/main/java/com/example/userservice/service/TokenService.java index dd8ca0c..3d6070f 100644 --- a/user-service/src/main/java/com/example/userservice/service/TokenService.java +++ b/user-service/src/main/java/com/example/userservice/service/TokenService.java @@ -6,6 +6,7 @@ import com.example.userservice.repository.redis.RefreshTokenRepository; import com.example.userservice.security.JwtUtil; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; @@ -21,6 +22,11 @@ public class TokenService { private final MemberRepository memberRepository; private final RefreshTokenRepository refreshTokenRepository; + /** + * 리프레시 토큰을 사용하여 새 액세스 토큰 생성 + * @param refreshToken 리프레시 토큰 + * @return 새 액세스 토큰 + */ public String createNewAccessToken(String refreshToken) { // 리프레시 토큰 유효성 검사 if (!jwtUtil.validateToken(refreshToken)) { @@ -38,11 +44,24 @@ public String createNewAccessToken(String refreshToken) { Member member = memberRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + // 사용자 권한 설정 + SimpleGrantedAuthority authority = new SimpleGrantedAuthority(member.getRole()); + // 새 액세스 토큰 생성 - UserDetails userDetails = new User(member.getUserId(), member.getPassword(), Collections.emptyList()); + UserDetails userDetails = new User( + member.getUserId(), + member.getPassword(), + Collections.singletonList(authority) + ); + return jwtUtil.generateToken(userDetails); } + /** + * 리프레시 토큰 저장 + * @param memberId 회원 ID + * @param refreshToken 리프레시 토큰 + */ public void saveRefreshToken(Long memberId, String refreshToken) { RefreshToken token = RefreshToken.builder() .id(String.valueOf(memberId)) // memberId를 String으로 변환해서 저장 @@ -52,7 +71,12 @@ public void saveRefreshToken(Long memberId, String refreshToken) { refreshTokenRepository.save(token); } + /** + * 리프레시 토큰 삭제 + * @param memberId 회원 ID + */ public void deleteRefreshToken(Long memberId) { - refreshTokenRepository.deleteById(Long.valueOf(String.valueOf(memberId))); // 여기서도 String 변환 필요! + // memberId를 String으로 변환하여 deleteById 호출 + refreshTokenRepository.deleteById(String.valueOf(memberId)); } -} +} \ No newline at end of file From c54beb90382b15db95fa68c7a30c6982de1a420e Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Wed, 30 Apr 2025 13:57:17 +0900 Subject: [PATCH 227/361] =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AreaPlaceController.java | 175 ++++++++++++++++++ .../repository/AttractionRepository.java | 7 + .../repository/CulturalEventRepository.java | 6 + .../repository/RestaurantRepository.java | 7 + 4 files changed, 195 insertions(+) create mode 100644 place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java diff --git a/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java b/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java new file mode 100644 index 0000000..2bc0ead --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java @@ -0,0 +1,175 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.entity.*; +import com.example.placeservice.repository.AccommodationRepository; +import com.example.placeservice.repository.AreaRepository; +import com.example.placeservice.repository.AttractionRepository; +import com.example.placeservice.repository.CafeRepository; +import com.example.placeservice.repository.CulturalEventRepository; +import com.example.placeservice.repository.RestaurantRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.*; + +@Slf4j +@RestController +@RequestMapping("/main") +@RequiredArgsConstructor +public class AreaPlaceController { + + private final AreaRepository areaRepository; + private final CafeRepository cafeRepository; + private final RestaurantRepository restaurantRepository; + private final AttractionRepository attractionRepository; + private final AccommodationRepository accommodationRepository; + private final CulturalEventRepository culturalEventRepository; + + /** + * 지역 ID를 기준으로 모든 장소 정보(카페, 음식점, 관광지, 숙박, 행사)를 한번에 조회 + */ + @GetMapping("/place/list/{areaId}") + public ResponseEntity>> getAllPlacesByAreaId(@PathVariable Long areaId) { + log.info("지역 ID {}의 모든 장소 정보 통합 조회", areaId); + + // 지역 정보 조회 + Optional areaOptional = areaRepository.findById(areaId); + if (!areaOptional.isPresent()) { + return ResponseEntity.notFound().build(); + } + + // 배열 형태로 응답 변경 + List> response = new ArrayList<>(); + + // 카페 정보 + Map cafeResponse = new LinkedHashMap<>(); + cafeResponse.put("type", "cafe"); + + List> cafeList = new ArrayList<>(); + List cafes = cafeRepository.findByAreaId(areaId); + + for (Cafe cafe : cafes) { + Map cafeMap = new LinkedHashMap<>(); + cafeMap.put("id", cafe.getId()); + cafeMap.put("name", cafe.getName()); + cafeMap.put("address", cafe.getAddress()); + cafeMap.put("phone", cafe.getPhone()); + cafeMap.put("category_code", cafe.getCategoryCode()); + cafeMap.put("kakaomap_url", cafe.getKakaomapUrl()); + cafeMap.put("lat", cafe.getLat()); + cafeMap.put("lon", cafe.getLon()); + cafeList.add(cafeMap); + } + + cafeResponse.put("content", cafeList); + response.add(cafeResponse); // 배열에 추가 + + // 음식점 정보 + Map restaurantResponse = new LinkedHashMap<>(); + restaurantResponse.put("type", "restaurant"); + + List> restaurantList = new ArrayList<>(); + List restaurants = restaurantRepository.findByAreaAreaId(areaId); + + for (Restaurant restaurant : restaurants) { + Map restaurantMap = new LinkedHashMap<>(); + restaurantMap.put("id", restaurant.getRestaurantId()); + restaurantMap.put("name", restaurant.getName()); + restaurantMap.put("address", restaurant.getAddress()); + restaurantMap.put("phone", restaurant.getPhone()); + restaurantMap.put("category_code", restaurant.getCategory_code()); + restaurantMap.put("category_group_name", restaurant.getCategoryGroupName()); + restaurantMap.put("category_name", restaurant.getCategoryName()); + restaurantMap.put("kakaomap_url", restaurant.getKakaomap_url()); + restaurantMap.put("lat", restaurant.getLat()); + restaurantMap.put("lon", restaurant.getLon()); + restaurantList.add(restaurantMap); + } + + restaurantResponse.put("content", restaurantList); + response.add(restaurantResponse); // 배열에 추가 + + // 관광지 정보 + Map attractionResponse = new LinkedHashMap<>(); + attractionResponse.put("type", "attraction"); + + List> attractionList = new ArrayList<>(); + List attractions = attractionRepository.findByAreaAreaId(areaId); + + for (Attraction attraction : attractions) { + Map attractionMap = new LinkedHashMap<>(); + attractionMap.put("id", attraction.getAttractionId()); + attractionMap.put("seoul_attraction_id", attraction.getSeoulAttractionId()); + attractionMap.put("name", attraction.getName()); + attractionMap.put("address", attraction.getAddress()); + attractionMap.put("phone", attraction.getPhone()); + attractionMap.put("homepage_url", attraction.getHomepageUrl()); + attractionMap.put("close_day", attraction.getCloseDay()); + attractionMap.put("use_time", attraction.getUseTime()); + attractionMap.put("kakaomap_url", attraction.getKakaomapUrl()); + attractionMap.put("lat", attraction.getLat()); + attractionMap.put("lon", attraction.getLon()); + attractionList.add(attractionMap); + } + + attractionResponse.put("content", attractionList); + response.add(attractionResponse); // 배열에 추가 + + // 숙박시설 정보 + Map accommodationResponse = new LinkedHashMap<>(); + accommodationResponse.put("type", "accommodation"); + + List> accommodationList = new ArrayList<>(); + List accommodations = accommodationRepository.findByAreaId(areaId); + + for (Accommodation accommodation : accommodations) { + Map accommodationMap = new LinkedHashMap<>(); + accommodationMap.put("id", accommodation.getAccommodationId()); + accommodationMap.put("name", accommodation.getName()); + accommodationMap.put("address", accommodation.getAddress()); + accommodationMap.put("phone", accommodation.getPhone()); + accommodationMap.put("gu", accommodation.getGu()); + accommodationMap.put("type", accommodation.getType()); + accommodationMap.put("kakaomap_url", accommodation.getKakaomapUrl()); + accommodationMap.put("lat", accommodation.getLat()); + accommodationMap.put("lon", accommodation.getLon()); + accommodationList.add(accommodationMap); + } + + accommodationResponse.put("content", accommodationList); + response.add(accommodationResponse); // 배열에 추가 + + // 문화행사 정보 + Map eventResponse = new LinkedHashMap<>(); + eventResponse.put("type", "cultural_event"); + + List> eventList = new ArrayList<>(); + List events = culturalEventRepository.findByAreaAreaId(areaId); + + for (CulturalEvent event : events) { + Map eventMap = new LinkedHashMap<>(); + eventMap.put("id", event.getEventId()); + eventMap.put("category", event.getCategory()); + eventMap.put("title", event.getTitle()); + eventMap.put("address", event.getAddress()); + eventMap.put("target", event.getTarget()); + eventMap.put("event_fee", event.getEventFee()); + eventMap.put("event_img", event.getEventImg()); + eventMap.put("start_date", event.getStartDate()); + eventMap.put("end_date", event.getEndDate()); + eventMap.put("lat", event.getLat()); + eventMap.put("lon", event.getLon()); + eventList.add(eventMap); + } + + eventResponse.put("content", eventList); + response.add(eventResponse); // 배열에 추가 + + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java index f43538e..925983f 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java @@ -2,11 +2,18 @@ import com.example.placeservice.entity.Attraction; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import java.util.List; import java.util.Optional; public interface AttractionRepository extends JpaRepository { boolean existsBySeoulAttractionId(String seoul_attraction_id); Optional findByAttractionId(Long attraction_id); + + // 지역 ID로 관광지 목록 조회 + @Query("SELECT a FROM Attraction a WHERE a.area.areaId = :areaId") + List findByAreaAreaId(@Param("areaId") Long areaId); } diff --git a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java index f238de4..97b1a82 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java @@ -3,12 +3,18 @@ import com.example.placeservice.entity.CulturalEvent; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; +import java.util.List; @Repository public interface CulturalEventRepository extends JpaRepository { boolean existsByTitleAndAddressAndStartDate(String title, String address, LocalDateTime startDate); + // 지역 ID로 문화 행사 목록 조회 + @Query("SELECT e FROM CulturalEvent e WHERE e.area.areaId = :areaId") + List findByAreaAreaId(@Param("areaId") Long areaId); } diff --git a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java index 6f04f0a..441b570 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java @@ -2,8 +2,15 @@ import com.example.placeservice.entity.Restaurant; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface RestaurantRepository extends JpaRepository { // 기본 CRUD 사용 (findAll, findById, save 등) + // 지역 ID로 음식점 목록 조회 + @Query("SELECT r FROM Restaurant r WHERE r.area.areaId = :areaId") + List findByAreaAreaId(@Param("areaId") Long areaId); } \ No newline at end of file From dc42591ecea1a81fd324ad4e217c015dfe87e497 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Wed, 30 Apr 2025 13:57:17 +0900 Subject: [PATCH 228/361] =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AreaPlaceController.java | 175 ++++++++++++++++++ .../repository/AttractionRepository.java | 7 + .../repository/CulturalEventRepository.java | 6 + .../repository/RestaurantRepository.java | 7 + 4 files changed, 195 insertions(+) create mode 100644 place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java diff --git a/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java b/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java new file mode 100644 index 0000000..2bc0ead --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java @@ -0,0 +1,175 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.entity.*; +import com.example.placeservice.repository.AccommodationRepository; +import com.example.placeservice.repository.AreaRepository; +import com.example.placeservice.repository.AttractionRepository; +import com.example.placeservice.repository.CafeRepository; +import com.example.placeservice.repository.CulturalEventRepository; +import com.example.placeservice.repository.RestaurantRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.*; + +@Slf4j +@RestController +@RequestMapping("/main") +@RequiredArgsConstructor +public class AreaPlaceController { + + private final AreaRepository areaRepository; + private final CafeRepository cafeRepository; + private final RestaurantRepository restaurantRepository; + private final AttractionRepository attractionRepository; + private final AccommodationRepository accommodationRepository; + private final CulturalEventRepository culturalEventRepository; + + /** + * 지역 ID를 기준으로 모든 장소 정보(카페, 음식점, 관광지, 숙박, 행사)를 한번에 조회 + */ + @GetMapping("/place/list/{areaId}") + public ResponseEntity>> getAllPlacesByAreaId(@PathVariable Long areaId) { + log.info("지역 ID {}의 모든 장소 정보 통합 조회", areaId); + + // 지역 정보 조회 + Optional areaOptional = areaRepository.findById(areaId); + if (!areaOptional.isPresent()) { + return ResponseEntity.notFound().build(); + } + + // 배열 형태로 응답 변경 + List> response = new ArrayList<>(); + + // 카페 정보 + Map cafeResponse = new LinkedHashMap<>(); + cafeResponse.put("type", "cafe"); + + List> cafeList = new ArrayList<>(); + List cafes = cafeRepository.findByAreaId(areaId); + + for (Cafe cafe : cafes) { + Map cafeMap = new LinkedHashMap<>(); + cafeMap.put("id", cafe.getId()); + cafeMap.put("name", cafe.getName()); + cafeMap.put("address", cafe.getAddress()); + cafeMap.put("phone", cafe.getPhone()); + cafeMap.put("category_code", cafe.getCategoryCode()); + cafeMap.put("kakaomap_url", cafe.getKakaomapUrl()); + cafeMap.put("lat", cafe.getLat()); + cafeMap.put("lon", cafe.getLon()); + cafeList.add(cafeMap); + } + + cafeResponse.put("content", cafeList); + response.add(cafeResponse); // 배열에 추가 + + // 음식점 정보 + Map restaurantResponse = new LinkedHashMap<>(); + restaurantResponse.put("type", "restaurant"); + + List> restaurantList = new ArrayList<>(); + List restaurants = restaurantRepository.findByAreaAreaId(areaId); + + for (Restaurant restaurant : restaurants) { + Map restaurantMap = new LinkedHashMap<>(); + restaurantMap.put("id", restaurant.getRestaurantId()); + restaurantMap.put("name", restaurant.getName()); + restaurantMap.put("address", restaurant.getAddress()); + restaurantMap.put("phone", restaurant.getPhone()); + restaurantMap.put("category_code", restaurant.getCategory_code()); + restaurantMap.put("category_group_name", restaurant.getCategoryGroupName()); + restaurantMap.put("category_name", restaurant.getCategoryName()); + restaurantMap.put("kakaomap_url", restaurant.getKakaomap_url()); + restaurantMap.put("lat", restaurant.getLat()); + restaurantMap.put("lon", restaurant.getLon()); + restaurantList.add(restaurantMap); + } + + restaurantResponse.put("content", restaurantList); + response.add(restaurantResponse); // 배열에 추가 + + // 관광지 정보 + Map attractionResponse = new LinkedHashMap<>(); + attractionResponse.put("type", "attraction"); + + List> attractionList = new ArrayList<>(); + List attractions = attractionRepository.findByAreaAreaId(areaId); + + for (Attraction attraction : attractions) { + Map attractionMap = new LinkedHashMap<>(); + attractionMap.put("id", attraction.getAttractionId()); + attractionMap.put("seoul_attraction_id", attraction.getSeoulAttractionId()); + attractionMap.put("name", attraction.getName()); + attractionMap.put("address", attraction.getAddress()); + attractionMap.put("phone", attraction.getPhone()); + attractionMap.put("homepage_url", attraction.getHomepageUrl()); + attractionMap.put("close_day", attraction.getCloseDay()); + attractionMap.put("use_time", attraction.getUseTime()); + attractionMap.put("kakaomap_url", attraction.getKakaomapUrl()); + attractionMap.put("lat", attraction.getLat()); + attractionMap.put("lon", attraction.getLon()); + attractionList.add(attractionMap); + } + + attractionResponse.put("content", attractionList); + response.add(attractionResponse); // 배열에 추가 + + // 숙박시설 정보 + Map accommodationResponse = new LinkedHashMap<>(); + accommodationResponse.put("type", "accommodation"); + + List> accommodationList = new ArrayList<>(); + List accommodations = accommodationRepository.findByAreaId(areaId); + + for (Accommodation accommodation : accommodations) { + Map accommodationMap = new LinkedHashMap<>(); + accommodationMap.put("id", accommodation.getAccommodationId()); + accommodationMap.put("name", accommodation.getName()); + accommodationMap.put("address", accommodation.getAddress()); + accommodationMap.put("phone", accommodation.getPhone()); + accommodationMap.put("gu", accommodation.getGu()); + accommodationMap.put("type", accommodation.getType()); + accommodationMap.put("kakaomap_url", accommodation.getKakaomapUrl()); + accommodationMap.put("lat", accommodation.getLat()); + accommodationMap.put("lon", accommodation.getLon()); + accommodationList.add(accommodationMap); + } + + accommodationResponse.put("content", accommodationList); + response.add(accommodationResponse); // 배열에 추가 + + // 문화행사 정보 + Map eventResponse = new LinkedHashMap<>(); + eventResponse.put("type", "cultural_event"); + + List> eventList = new ArrayList<>(); + List events = culturalEventRepository.findByAreaAreaId(areaId); + + for (CulturalEvent event : events) { + Map eventMap = new LinkedHashMap<>(); + eventMap.put("id", event.getEventId()); + eventMap.put("category", event.getCategory()); + eventMap.put("title", event.getTitle()); + eventMap.put("address", event.getAddress()); + eventMap.put("target", event.getTarget()); + eventMap.put("event_fee", event.getEventFee()); + eventMap.put("event_img", event.getEventImg()); + eventMap.put("start_date", event.getStartDate()); + eventMap.put("end_date", event.getEndDate()); + eventMap.put("lat", event.getLat()); + eventMap.put("lon", event.getLon()); + eventList.add(eventMap); + } + + eventResponse.put("content", eventList); + response.add(eventResponse); // 배열에 추가 + + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java index f43538e..925983f 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java @@ -2,11 +2,18 @@ import com.example.placeservice.entity.Attraction; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import java.util.List; import java.util.Optional; public interface AttractionRepository extends JpaRepository { boolean existsBySeoulAttractionId(String seoul_attraction_id); Optional findByAttractionId(Long attraction_id); + + // 지역 ID로 관광지 목록 조회 + @Query("SELECT a FROM Attraction a WHERE a.area.areaId = :areaId") + List findByAreaAreaId(@Param("areaId") Long areaId); } diff --git a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java index f238de4..97b1a82 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java @@ -3,12 +3,18 @@ import com.example.placeservice.entity.CulturalEvent; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; +import java.util.List; @Repository public interface CulturalEventRepository extends JpaRepository { boolean existsByTitleAndAddressAndStartDate(String title, String address, LocalDateTime startDate); + // 지역 ID로 문화 행사 목록 조회 + @Query("SELECT e FROM CulturalEvent e WHERE e.area.areaId = :areaId") + List findByAreaAreaId(@Param("areaId") Long areaId); } diff --git a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java index 6f04f0a..441b570 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java @@ -2,8 +2,15 @@ import com.example.placeservice.entity.Restaurant; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface RestaurantRepository extends JpaRepository { // 기본 CRUD 사용 (findAll, findById, save 등) + // 지역 ID로 음식점 목록 조회 + @Query("SELECT r FROM Restaurant r WHERE r.area.areaId = :areaId") + List findByAreaAreaId(@Param("areaId") Long areaId); } \ No newline at end of file From 954a674d8e03562a6344a5b35bc8fbcb038e3bdd Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 30 Apr 2025 14:21:52 +0900 Subject: [PATCH 229/361] =?UTF-8?q?application.properties=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 --- .../src/main/resources/application.properties | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 external-info-service/src/main/resources/application.properties diff --git a/external-info-service/src/main/resources/application.properties b/external-info-service/src/main/resources/application.properties new file mode 100644 index 0000000..05948dd --- /dev/null +++ b/external-info-service/src/main/resources/application.properties @@ -0,0 +1,20 @@ +spring.application.name=external-info-service + +server.port=8081 + +# postgreSQL ?? +spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db +spring.datasource.username=root +spring.datasource.password=admin +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update +spring.elasticsearch.uris= http://elasticsearch.seoultravel.life:9200 + +spring.mvc.contentnegotiation.favor-path-extension=false +spring.mvc.contentnegotiation.media-types.json=application/json +spring.mvc.default-response-type=application/json + + +seoul.api.key=505a6255746b696d34344b46566174 From 6ad78de9d5014fb093226ff945bc1e75789d5242 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 30 Apr 2025 15:17:14 +0900 Subject: [PATCH 230/361] Merge: searchplace & Feature#54 --- .../controller/PlaceSearchController.java | 29 +++++++ .../placeservice/dto/PlaceDocument.java | 14 +++ .../placeservice/entity/Accommodation.java | 2 + .../repository/AccommodationRepository.java | 5 ++ .../repository/AttractionRepository.java | 5 ++ .../repository/CafeRepository.java | 3 + .../repository/CulturalEventRepository.java | 4 + .../repository/PlaceDocumentRepository.java | 22 +++++ .../repository/RestaurantRepository.java | 5 ++ .../service/PlaceDocumentMapper.java | 50 +++++++++++ .../service/PlaceDocumentService.java | 85 +++++++++++++++++++ .../src/main/resources/application.properties | 22 +++++ 12 files changed, 246 insertions(+) create mode 100644 place-service/src/main/java/com/example/placeservice/controller/PlaceSearchController.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java create mode 100644 place-service/src/main/java/com/example/placeservice/repository/PlaceDocumentRepository.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/PlaceDocumentService.java create mode 100644 place-service/src/main/resources/application.properties diff --git a/place-service/src/main/java/com/example/placeservice/controller/PlaceSearchController.java b/place-service/src/main/java/com/example/placeservice/controller/PlaceSearchController.java new file mode 100644 index 0000000..a358571 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/PlaceSearchController.java @@ -0,0 +1,29 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.dto.PlaceDocument; +import com.example.placeservice.service.PlaceDocumentService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/search") +public class PlaceSearchController { + + private final PlaceDocumentService placeSearchService; + + @GetMapping("/{keyword}") + public List searchByName(@PathVariable String keyword) { + return placeSearchService.searchPlacesByName(keyword); + } + + @GetMapping("/address/{keyword}") + public List searchByAddress(@PathVariable String keyword) { + return placeSearchService.searchPlacesByAddress(keyword); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java b/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java new file mode 100644 index 0000000..b91c9ba --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java @@ -0,0 +1,14 @@ +package com.example.placeservice.dto; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class PlaceDocument { + + // 공통 + private String name; + private String category; + private String address; +} diff --git a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java index 79d051a..9c1bc03 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java @@ -2,11 +2,13 @@ import jakarta.persistence.*; import lombok.Data; +import lombok.Getter; import java.math.BigDecimal; @Data @Entity +@Getter public class Accommodation { @Id @Column(name = "accommodation_id") diff --git a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java index 9586746..20208fa 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java @@ -12,7 +12,12 @@ public interface AccommodationRepository extends JpaRepository findByGu(String gu); List findByType(String type); +<<<<<<< HEAD @Query(value = "SELECT * FROM Accommodation WHERE area_id = ?1", nativeQuery = true) List findByAreaId(Long areaId); +======= + List findByNameContainingIgnoreCase(String keyword); + List findByAddressContainingIgnoreCase(String keyword); +>>>>>>> 2eb5421 (push and 통합검색) } diff --git a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java index f43538e..fcd46e0 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AttractionRepository.java @@ -1,12 +1,17 @@ package com.example.placeservice.repository; +import com.example.placeservice.entity.Accommodation; import com.example.placeservice.entity.Attraction; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface AttractionRepository extends JpaRepository { boolean existsBySeoulAttractionId(String seoul_attraction_id); Optional findByAttractionId(Long attraction_id); + List findByNameContainingIgnoreCase(String keyword); + + List findByAddressContainingIgnoreCase(String keyword); } diff --git a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java index 51c1a6d..400ffd1 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java @@ -1,5 +1,6 @@ package com.example.placeservice.repository; +import com.example.placeservice.entity.Accommodation; import com.example.placeservice.entity.Cafe; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; @@ -18,4 +19,6 @@ public interface CafeRepository extends JpaRepository { void deleteByAreaId(Long areaId); Optional findByKakaomapUrl(String placeCode); + List findByNameContainingIgnoreCase(String keyword); + List findByAddressContainingIgnoreCase(String keyword); } \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java index f238de4..95182d0 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java @@ -1,14 +1,18 @@ // CulturalEventRepository.java package com.example.placeservice.repository; +import com.example.placeservice.entity.Accommodation; import com.example.placeservice.entity.CulturalEvent; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; +import java.util.List; @Repository public interface CulturalEventRepository extends JpaRepository { boolean existsByTitleAndAddressAndStartDate(String title, String address, LocalDateTime startDate); + List findBytitleContainingIgnoreCase(String keyword); + List findByAddressContainingIgnoreCase(String keyword); } diff --git a/place-service/src/main/java/com/example/placeservice/repository/PlaceDocumentRepository.java b/place-service/src/main/java/com/example/placeservice/repository/PlaceDocumentRepository.java new file mode 100644 index 0000000..96a280f --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/repository/PlaceDocumentRepository.java @@ -0,0 +1,22 @@ +package com.example.placeservice.repository; + +import com.example.placeservice.dto.PlaceDocument; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; + +@Repository +public class PlaceDocumentRepository { + + private final List placeDocuments = new ArrayList<>(); + + public List findAll() { + return placeDocuments; + } + + public void saveAll(List documents) { + placeDocuments.clear(); + placeDocuments.addAll(documents); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java index 6f04f0a..e7fc6c8 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/RestaurantRepository.java @@ -1,9 +1,14 @@ package com.example.placeservice.repository; +import com.example.placeservice.entity.Accommodation; import com.example.placeservice.entity.Restaurant; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface RestaurantRepository extends JpaRepository { // 기본 CRUD 사용 (findAll, findById, save 등) + List findByNameContainingIgnoreCase(String keyword); + List findByAddressContainingIgnoreCase(String keyword); } \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java new file mode 100644 index 0000000..7d7791f --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java @@ -0,0 +1,50 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.PlaceDocument; + +import com.example.placeservice.entity.*; +import org.springframework.stereotype.Component; + +@Component +public class PlaceDocumentMapper { + + public PlaceDocument toPlaceDocument(Accommodation item) { + return PlaceDocument.builder() + .name(item.getName()) + .address(item.getAddress()) + .category("accommodation") + .build(); + } + + public PlaceDocument toPlaceDocument(Cafe item) { + return PlaceDocument.builder() + .name(item.getName()) + .address(item.getAddress()) + .category("cafe") + .build(); + } + + public PlaceDocument toPlaceDocument(CulturalEvent item) { + return PlaceDocument.builder() + .name(item.getTitle()) + .address(item.getAddress()) + .category("culturalevent") + .build(); + } + + public PlaceDocument toPlaceDocument(Restaurant item) { + return PlaceDocument.builder() + .name(item.getName()) + .address(item.getAddress()) + .category("restaurant") + .build(); + } + + public PlaceDocument toPlaceDocument(Attraction item) { + return PlaceDocument.builder() + .name(item.getName()) + .address(item.getAddress()) + .category("attraction") + .build(); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentService.java b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentService.java new file mode 100644 index 0000000..70abe76 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentService.java @@ -0,0 +1,85 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.accommodation.AccommodationDto; +import com.example.placeservice.dto.PlaceDocument; +import com.example.placeservice.service.PlaceDocumentMapper; +import com.example.placeservice.entity.*; +import com.example.placeservice.repository.*; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; + + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + + +@Service +@RequiredArgsConstructor +@Slf4j +public class PlaceDocumentService { + + private final AccommodationRepository accommodationRepository; + private final RestaurantRepository restaurantRepository; + private final AttractionRepository attractionRepository; + private final CulturalEventRepository culturalEventRepository; + private final CafeRepository cafeRepository; + + + private final PlaceDocumentRepository placeDocumentRepository; + private final PlaceDocumentMapper placeDocumentMapper; + + public List searchPlacesByName(String keyword) { + List results = new ArrayList<>(); + + // Accommodation, Attraction, Cafe, Restaurant, CulturalEvent에서 이름이 포함된 객체만 필터링 + results.addAll(accommodationRepository.findByNameContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + results.addAll(attractionRepository.findByNameContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + results.addAll(cafeRepository.findByNameContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + results.addAll(restaurantRepository.findByNameContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + results.addAll(culturalEventRepository.findBytitleContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + // 이름에만 keyword가 포함된 것만 남긴다. + return results.stream() + .filter(place -> place.getName().toLowerCase().contains(keyword.toLowerCase())) + .collect(Collectors.toList()); + } + + public List searchPlacesByAddress(String keyword) { + List results = new ArrayList<>(); + + // Accommodation, Attraction, Cafe, Restaurant, CulturalEvent에서 주소가 포함된 객체만 필터링 + results.addAll(accommodationRepository.findByAddressContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + results.addAll(attractionRepository.findByAddressContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + results.addAll(cafeRepository.findByAddressContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + results.addAll(restaurantRepository.findByAddressContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + results.addAll(culturalEventRepository.findByAddressContainingIgnoreCase(keyword) + .stream().map(placeDocumentMapper::toPlaceDocument).toList()); + + // 주소에만 keyword가 포함된 것만 남긴다. + return results.stream() + .filter(place -> place.getAddress().toLowerCase().contains(keyword.toLowerCase())) + .collect(Collectors.toList()); + } + + +} diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties new file mode 100644 index 0000000..638be00 --- /dev/null +++ b/place-service/src/main/resources/application.properties @@ -0,0 +1,22 @@ +spring.application.name=place-service + +spring.config.import=optional:file:.env[.properties] +# postgreSQL ?? +spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db +spring.datasource.username=root +spring.datasource.password=admin +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.defer-datasource-initialization=true +#spring.jpa.hibernate.ddl-auto=update + spring.jpa.hibernate.ddl-auto=create +spring.jpa.show-sql=true + +#spring.sql.init.mode=always + +#API +kakao.api.key=${KAKAO_API_KEY} + +# ?? ?? +logging.level.com.example.placeservice=INFO \ No newline at end of file From 04105922ae247bca5c77836bb153d910d84d351b Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 30 Apr 2025 15:18:59 +0900 Subject: [PATCH 231/361] =?UTF-8?q?Merge:=20=EC=98=A4=EB=A5=98=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeservice/repository/AccommodationRepository.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java index 20208fa..9586746 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java @@ -12,12 +12,7 @@ public interface AccommodationRepository extends JpaRepository findByGu(String gu); List findByType(String type); -<<<<<<< HEAD @Query(value = "SELECT * FROM Accommodation WHERE area_id = ?1", nativeQuery = true) List findByAreaId(Long areaId); -======= - List findByNameContainingIgnoreCase(String keyword); - List findByAddressContainingIgnoreCase(String keyword); ->>>>>>> 2eb5421 (push and 통합검색) } From 0fe91eca01c0219817590f29e27008ba45ce569f Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 30 Apr 2025 15:33:42 +0900 Subject: [PATCH 232/361] =?UTF-8?q?Merge:=20=EB=B9=A0=EC=A7=84=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeservice/repository/AccommodationRepository.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java index 9586746..02bae26 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/AccommodationRepository.java @@ -15,4 +15,7 @@ public interface AccommodationRepository extends JpaRepository findByAreaId(Long areaId); + + List findByNameContainingIgnoreCase(String keyword); + List findByAddressContainingIgnoreCase(String keyword); } From ebf54c1bf332d55411ad3b603521b09e7ca70afe Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 30 Apr 2025 16:32:38 +0900 Subject: [PATCH 233/361] =?UTF-8?q?Merge=20branch=20'Feature#26-=EC=9E=A5?= =?UTF-8?q?=EC=86=8C=EB=8D=B0=EC=9D=B4=ED=84=B0=EA=B0=80=EA=B3=B5'=20of=20?= =?UTF-8?q?https://github.com/SentryRookies/Back-STARS=20into=20Feature#26?= =?UTF-8?q?-=EC=9E=A5=EC=86=8C=EB=8D=B0=EC=9D=B4=ED=84=B0=EA=B0=80?= =?UTF-8?q?=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/restaurant/RestaurantDetailResponse.java | 8 ++++---- .../dto/restaurant/RestaurantListResponse.java | 8 ++++---- .../userservice/controller/SignupController.java | 11 ++++++++++- .../example/userservice/service/MemberService.java | 3 +-- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java b/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java index 0c7e911..86b30ca 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java +++ b/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantDetailResponse.java @@ -15,9 +15,9 @@ public class RestaurantDetailResponse { private String category_group_code; private String category_group_name; private String category_name; - private String id; + private String restaurant_id; private String phone; - private String name; + private String restaurant_name; private String place_url; private String road_address_name; private String lat; @@ -34,10 +34,10 @@ public static RestaurantDetailResponse fromEntity(Restaurant restaurant) { .category_group_code(restaurant.getCategory_code()) .category_group_name(restaurant.getCategoryGroupName()) // 고정X (DB) .category_name(restaurant.getCategoryName()) // 고정X (DB) - .id(String.valueOf(restaurant.getRestaurantId())) + .restaurant_id(String.valueOf(restaurant.getRestaurantId())) .kakao_id(restaurant.getKakao_id()) .phone(restaurant.getPhone()) - .name(restaurant.getName()) + .restaurant_name(restaurant.getName()) .place_url(restaurant.getKakaomap_url()) .categoryGroupName(restaurant.getCategoryGroupName()) // 추가 .categoryName(restaurant.getCategoryName()) // 추가 diff --git a/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantListResponse.java b/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantListResponse.java index 8681d7e..b7a2b73 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantListResponse.java +++ b/place-service/src/main/java/com/example/placeservice/dto/restaurant/RestaurantListResponse.java @@ -7,15 +7,15 @@ @Getter @Builder public class RestaurantListResponse { - private Long id; - private String name; + private Long restaurant_id; + private String restaurant_name; private String lat; private String lon; public static RestaurantListResponse fromEntity(Restaurant restaurant) { return RestaurantListResponse.builder() - .id(restaurant.getRestaurantId()) - .name(restaurant.getName()) + .restaurant_id(restaurant.getRestaurantId()) + .restaurant_name(restaurant.getName()) .lat(restaurant.getLat().toString()) .lon(restaurant.getLon().toString()) .build(); diff --git a/user-service/src/main/java/com/example/userservice/controller/SignupController.java b/user-service/src/main/java/com/example/userservice/controller/SignupController.java index 49f8999..403e7c8 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SignupController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SignupController.java @@ -7,6 +7,7 @@ import com.example.userservice.service.TokenService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; @@ -79,7 +80,15 @@ public ResponseEntity> signup(@RequestBody MemberSign dto) { //회원탈퇴 @DeleteMapping("/signout/{userId}") - public ResponseEntity signout(@PathVariable String userId) { + public ResponseEntity signout( + @PathVariable String userId, + @AuthenticationPrincipal UserDetails userDetails) { + + // 본인 확인 + if (!userDetails.getUsername().equals(userId)) { + return ResponseEntity.status(403).body("본인만 탈퇴할 수 있습니다."); + } + try { memberService.deleteMember(userId); return ResponseEntity.ok("회원 탈퇴가 완료되었습니다."); diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java index 9f2ab5e..92a4f12 100644 --- a/user-service/src/main/java/com/example/userservice/service/MemberService.java +++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java @@ -124,8 +124,7 @@ public MemberDto.MemberResponse updateProfile(String userId, MemberDto.UpdateReq @Transactional public void deleteMember(String userId) { Member member = memberRepository.findByUserId(userId) - .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); - + .orElseThrow(() -> new IllegalArgumentException("회원이 존재하지 않습니다.")); memberRepository.delete(member); } From b502e9277ef6402180d8dd36e07da48e5c9a5a16 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 30 Apr 2025 16:45:07 +0900 Subject: [PATCH 234/361] =?UTF-8?q?Accommodation=20entity=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 --- gateway/.gitignore | 1 + .../gateway/config/SecurityConfig.java | 19 ++++ .../filter/JwtAuthenticationFilter.java | 104 +++++++++++++++++- .../gateway/security/JwtTokenProvider.java | 67 +++++++++++ .../controller/AreaPlaceController.java | 2 +- .../dto/accommodation/AccommodationDto.java | 4 +- .../placeservice/entity/Accommodation.java | 3 +- .../service/AccommodationService.java | 2 +- 8 files changed, 194 insertions(+), 8 deletions(-) diff --git a/gateway/.gitignore b/gateway/.gitignore index 8aca0b7..5dec76b 100644 --- a/gateway/.gitignore +++ b/gateway/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ src/main/resources/application.properties +.env ### STS ### .apt_generated diff --git a/gateway/src/main/java/com/example/gateway/config/SecurityConfig.java b/gateway/src/main/java/com/example/gateway/config/SecurityConfig.java index f90d1af..5e7673e 100644 --- a/gateway/src/main/java/com/example/gateway/config/SecurityConfig.java +++ b/gateway/src/main/java/com/example/gateway/config/SecurityConfig.java @@ -1,5 +1,24 @@ package com.example.gateway.config; + +import com.example.gateway.filter.JwtAuthenticationFilter; +import com.example.gateway.security.JwtTokenProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; + +@Configuration +@EnableWebSecurity public class SecurityConfig { + private final JwtTokenProvider jwtTokenProvider; + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + public SecurityConfig(JwtTokenProvider jwtTokenProvider, JwtAuthenticationFilter jwtAuthenticationFilter) { + this.jwtTokenProvider = jwtTokenProvider; + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + } + + @Bean } diff --git a/gateway/src/main/java/com/example/gateway/filter/JwtAuthenticationFilter.java b/gateway/src/main/java/com/example/gateway/filter/JwtAuthenticationFilter.java index 7711023..d816b9e 100644 --- a/gateway/src/main/java/com/example/gateway/filter/JwtAuthenticationFilter.java +++ b/gateway/src/main/java/com/example/gateway/filter/JwtAuthenticationFilter.java @@ -1,4 +1,102 @@ -package com.example.gateway.filter; +package com.example.gateway.filter; // 실제 프로젝트 패키지 구조에 맞게 수정하세요 -public class JwtAuthenticationFilter { -} +import com.example.gateway.security.JwtTokenProvider; // JwtTokenProvider 위치 확인 +// import io.jsonwebtoken.JwtException; // 직접적인 사용은 줄어듦 +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.List; // 경로 제외 목록을 위해 추가 + +@Slf4j +@Component +public class JwtAuthenticationFilter implements GlobalFilter, Ordered { + + private final JwtTokenProvider jwtTokenProvider; + + // 인증이 필요 없는 경로 목록 (필요에 따라 추가/수정) + private final List excludedPaths = List.of( + "/user/auth/login", + "/user/auth/signup", + "/place/", + "/congestion/", + "/external/" + ); + + public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + String path = request.getURI().getPath(); + + boolean isExcluded = excludedPaths.stream().anyMatch(p -> path.startsWith(p)); + if (isExcluded) { + log.debug("인증 필터 제외 경로: {}", path); + return chain.filter(exchange); // 필터링 없이 다음 단계로 + } + + String token = extractToken(request); + + if (token == null) { + log.warn("요청 헤더에 JWT 토큰이 없습니다: {}", path); + return onError(exchange, "토큰 없음", HttpStatus.UNAUTHORIZED); + } + + if (jwtTokenProvider.validateToken(token)) { + log.debug("JWT 토큰 유효함: {}", path); + + // (선택 사항) 토큰이 유효하면 사용자 ID 같은 정보를 추출하여 요청 헤더에 추가할 수 있음 + // String userId = jwtTokenProvider.getUserIdFromToken(token); + // ServerHttpRequest modifiedRequest = exchange.getRequest().mutate() + // .header("X-USER-ID", userId) // 백엔드 서비스에서 사용할 헤더 이름 지정 + // .build(); + // return chain.filter(exchange.mutate().request(modifiedRequest).build()); + + // 기본: 수정 없이 다음 필터로 전달 + return chain.filter(exchange); + } else { + // validateToken에서 false를 반환 (내부적으로 로깅됨) + log.warn("유효하지 않은 JWT 토큰입니다: {}", path); + return onError(exchange, "토큰 유효하지 않음", HttpStatus.UNAUTHORIZED); + } + } + + private String extractToken(ServerHttpRequest request) { + String bearerToken = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); // "Bearer " 이후의 토큰 값 반환 + } + log.debug("요청 헤더에 Bearer 토큰이 없습니다."); + return null; + } + + private Mono onError(ServerWebExchange exchange, String errMessage, HttpStatus httpStatus) { + ServerHttpResponse response = exchange.getResponse(); + response.setStatusCode(httpStatus); + log.error(errMessage + " - 요청 URI: {}", exchange.getRequest().getURI()); + return response.setComplete(); + } + + /** + * 필터의 실행 순서를 지정합니다. + * 일반적으로 인증 필터는 다른 필터들보다 먼저 실행되어야 하므로 낮은 값을 가집니다. + * Spring Security Filter보다는 뒤, 다른 로깅/라우팅 필터보다는 앞에 실행되도록 조정할 수 있습니다. + * (예: NettyRoutingFilter는 Ordered.LOWEST_PRECEDENCE, LoadBalancerClientFilter는 10100) + * @return 필터 순서 값 + */ + @Override + public int getOrder() { + return -1; // 높은 우선순위 (낮은 값) + } +} \ No newline at end of file diff --git a/gateway/src/main/java/com/example/gateway/security/JwtTokenProvider.java b/gateway/src/main/java/com/example/gateway/security/JwtTokenProvider.java index 83ac1fb..3619f60 100644 --- a/gateway/src/main/java/com/example/gateway/security/JwtTokenProvider.java +++ b/gateway/src/main/java/com/example/gateway/security/JwtTokenProvider.java @@ -1,4 +1,71 @@ package com.example.gateway.security; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; + +@Slf4j +@Component public class JwtTokenProvider { + + @Value("${service.jwt.secret-key}") + private String secretKeyString; + private SecretKey secretKey; + + @PostConstruct + protected void init() { + if (secretKeyString == null || secretKeyString.isEmpty()) { + log.error("JWT Secret Key가 설정되지 않았습니다! 환경 변수 또는 설정 파일을 확인하세요."); + throw new IllegalStateException("JWT_SECRET_KEY가 설정되지 않았습니다!"); + } + this.secretKey = Keys.hmacShaKeyFor(secretKeyString.getBytes(StandardCharsets.UTF_8)); + log.info("JWT SecretKey 초기화 완료"); + } + + public boolean validateToken(String token) { + if (token == null || token.isEmpty()) { + log.warn("입력된 JWT 토큰이 비어 있습니다."); + return false; + } + try { + Jwts.parser() + .verifyWith(this.secretKey) + .build() + .parseSignedClaims(token); + return true; // 예외가 발생하지 않으면 유효함 + } catch (SignatureException e) { + log.error("JWT 서명이 유효하지 않습니다.", e); + } catch (MalformedJwtException e) { + log.error("JWT 토큰 형식이 잘못되었습니다.", e); + } catch (ExpiredJwtException e) { + log.warn("만료된 JWT 토큰입니다.", e); + } catch (UnsupportedJwtException e) { + log.error("지원되지 않는 JWT 토큰 형식입니다.", e); + } catch (IllegalArgumentException e) { + log.error("JWT 클레임 문자열이 비어 있습니다.", e); + } catch (Exception e) { // 그 외 예외 처리 + log.error("JWT 토큰 검증 중 알 수 없는 오류 발생", e); + } + return false; // 예외 발생 시 유효하지 않음 + } + + public String getUserIdFromToken(String token) { + Claims claims = Jwts.parser() + .verifyWith(this.secretKey) + .build() + .parseSignedClaims(token) + .getPayload(); + return claims.getSubject(); // Subject (userId) 반환 + } } diff --git a/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java b/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java index 2bc0ead..7073294 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java @@ -130,7 +130,7 @@ public ResponseEntity>> getAllPlacesByAreaId(@PathVaria for (Accommodation accommodation : accommodations) { Map accommodationMap = new LinkedHashMap<>(); accommodationMap.put("id", accommodation.getAccommodationId()); - accommodationMap.put("name", accommodation.getName()); + accommodationMap.put("name", accommodation.getAccommodationName()); accommodationMap.put("address", accommodation.getAddress()); accommodationMap.put("phone", accommodation.getPhone()); accommodationMap.put("gu", accommodation.getGu()); diff --git a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java index fc783c7..1f97639 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java @@ -15,7 +15,7 @@ public class AccommodationDto { private Long accommodation_id; private Long area_id; private String area_name; - private String name; + private String accommodation_name; private String address; private BigDecimal lat; private BigDecimal lon; @@ -46,7 +46,7 @@ public static AccommodationDto fromEntity(Accommodation entity) { entity.getAccommodationId(), areaIdValue, areaNameValue, - entity.getName(), + entity.getAccommodationName(), entity.getAddress(), entity.getLat(), entity.getLon(), diff --git a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java index 79d051a..7debe67 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java @@ -16,7 +16,8 @@ public class Accommodation { @JoinColumn(name = "area_id", referencedColumnName = "area_id", nullable = false) private Area area; - private String name; + @Column(name = "name") + private String accommodationName; private String address; @Column(precision = 10, scale = 6) private BigDecimal lat; diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java index 75b7b78..be95d0b 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java @@ -142,7 +142,7 @@ private Accommodation mapDocumentToAccommodation(KakaoApiResponse.Document doc, throw new IllegalArgumentException("Invalid accommodation ID format: " + doc.getId(), e); } accommodation.setArea(area); - accommodation.setName(doc.getPlaceName()); + accommodation.setAccommodationName(doc.getPlaceName()); accommodation.setAddress(doc.getRoadAddressName()); try { accommodation.setLat(new BigDecimal(doc.getY())); // String 좌표를 BigDecimal로 변환 From 8d3dae92a73aba2581c164b58b7f84575ed2ff9d Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 30 Apr 2025 17:10:42 +0900 Subject: [PATCH 235/361] =?UTF-8?q?accommodation=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/placeservice/controller/AreaPlaceController.java | 2 +- .../placeservice/dto/accommodation/AccommodationDto.java | 2 +- .../placeservice/dto/culturalevent/CulturalEventDto.java | 4 ++-- .../java/com/example/placeservice/entity/Accommodation.java | 3 +-- .../example/placeservice/service/AccommodationService.java | 2 +- place-service/src/main/resources/application.properties | 5 +++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java b/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java index 7073294..2bc0ead 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java @@ -130,7 +130,7 @@ public ResponseEntity>> getAllPlacesByAreaId(@PathVaria for (Accommodation accommodation : accommodations) { Map accommodationMap = new LinkedHashMap<>(); accommodationMap.put("id", accommodation.getAccommodationId()); - accommodationMap.put("name", accommodation.getAccommodationName()); + accommodationMap.put("name", accommodation.getName()); accommodationMap.put("address", accommodation.getAddress()); accommodationMap.put("phone", accommodation.getPhone()); accommodationMap.put("gu", accommodation.getGu()); diff --git a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java index 1f97639..e923d11 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/accommodation/AccommodationDto.java @@ -46,7 +46,7 @@ public static AccommodationDto fromEntity(Accommodation entity) { entity.getAccommodationId(), areaIdValue, areaNameValue, - entity.getAccommodationName(), + entity.getName(), entity.getAddress(), entity.getLat(), entity.getLon(), diff --git a/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventDto.java b/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventDto.java index d1fcfb1..26bb657 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/culturalevent/CulturalEventDto.java @@ -11,7 +11,7 @@ public class CulturalEventDto { private Long event_id; private String category; - private String title; + private String event_name; private String address; private BigDecimal lat; private BigDecimal lon; @@ -24,7 +24,7 @@ public class CulturalEventDto { public CulturalEventDto(CulturalEvent culturalEvent) { this.event_id = culturalEvent.getEventId(); this.category = culturalEvent.getCategory(); - this.title = culturalEvent.getTitle(); + this.event_name = culturalEvent.getTitle(); this.address = culturalEvent.getAddress(); this.lat = culturalEvent.getLat(); this.lon = culturalEvent.getLon(); diff --git a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java index 92e1254..9c1bc03 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Accommodation.java @@ -18,8 +18,7 @@ public class Accommodation { @JoinColumn(name = "area_id", referencedColumnName = "area_id", nullable = false) private Area area; - @Column(name = "name") - private String accommodationName; + private String name; private String address; @Column(precision = 10, scale = 6) private BigDecimal lat; diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java index be95d0b..75b7b78 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java @@ -142,7 +142,7 @@ private Accommodation mapDocumentToAccommodation(KakaoApiResponse.Document doc, throw new IllegalArgumentException("Invalid accommodation ID format: " + doc.getId(), e); } accommodation.setArea(area); - accommodation.setAccommodationName(doc.getPlaceName()); + accommodation.setName(doc.getPlaceName()); accommodation.setAddress(doc.getRoadAddressName()); try { accommodation.setLat(new BigDecimal(doc.getY())); // String 좌표를 BigDecimal로 변환 diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 638be00..003f95a 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -1,4 +1,5 @@ spring.application.name=place-service +server.port=8082 spring.config.import=optional:file:.env[.properties] # postgreSQL ?? @@ -9,8 +10,8 @@ spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.defer-datasource-initialization=true -#spring.jpa.hibernate.ddl-auto=update - spring.jpa.hibernate.ddl-auto=create +spring.jpa.hibernate.ddl-auto=update +#spring.jpa.hibernate.ddl-auto=create spring.jpa.show-sql=true #spring.sql.init.mode=always From bfa43383c16f673b92dc58a12211fc04d17e1c6f Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 30 Apr 2025 17:15:04 +0900 Subject: [PATCH 236/361] =?UTF-8?q?attraction=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeservice/dto/attraction/AttractionInfoDto.java | 4 ++-- .../placeservice/dto/attraction/AttractionListDto.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java index 7899022..dcb5546 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionInfoDto.java @@ -8,7 +8,7 @@ public class AttractionInfoDto { private Long attraction_id; private String seoul_attraction_id; - private String name; + private String attraction_name; private String address; private BigDecimal lat; private BigDecimal lon; @@ -23,7 +23,7 @@ public class AttractionInfoDto { public AttractionInfoDto(Long attractionId, String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon, String phone, String homepage_url, String close_day, String use_time, String kakaomapUrl, Long area_id, String area_name) { this.attraction_id = attractionId; this.seoul_attraction_id = seoul_attraction_id; - this.name = name; + this.attraction_name = name; this.address = address; this.lat = lat; this.lon = lon; diff --git a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java index 8abe1e6..b3a262e 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/attraction/AttractionListDto.java @@ -8,7 +8,7 @@ public class AttractionListDto { private Long attraction_id; private String seoul_attraction_id; - private String name; + private String attraction_name; private String address; private BigDecimal lat; private BigDecimal lon; @@ -18,7 +18,7 @@ public class AttractionListDto { public AttractionListDto(Long attraction_id, String seoul_attraction_id, String name, String address, BigDecimal lat, BigDecimal lon, String kakaomapUrl) { this.attraction_id = attraction_id; this.seoul_attraction_id = seoul_attraction_id; - this.name = name; + this.attraction_name = name; this.address = address; this.lat = lat; this.lon = lon; From 0c518192a66426521f7ba378043a4db01fcff98a Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Wed, 30 Apr 2025 17:21:12 +0900 Subject: [PATCH 237/361] =?UTF-8?q?cafe=20id,=20name=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AreaPlaceController.java | 5 ++- .../controller/CafeController.java | 32 +++---------------- 2 files changed, 6 insertions(+), 31 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java b/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java index 2bc0ead..d0dac8b 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java @@ -55,11 +55,10 @@ public ResponseEntity>> getAllPlacesByAreaId(@PathVaria for (Cafe cafe : cafes) { Map cafeMap = new LinkedHashMap<>(); - cafeMap.put("id", cafe.getId()); - cafeMap.put("name", cafe.getName()); + cafeMap.put("cafe_id", cafe.getId()); + cafeMap.put("cafe_name", cafe.getName()); cafeMap.put("address", cafe.getAddress()); cafeMap.put("phone", cafe.getPhone()); - cafeMap.put("category_code", cafe.getCategoryCode()); cafeMap.put("kakaomap_url", cafe.getKakaomapUrl()); cafeMap.put("lat", cafe.getLat()); cafeMap.put("lon", cafe.getLon()); diff --git a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java index f66eaf6..0fd7250 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java @@ -39,8 +39,8 @@ public ResponseEntity>> getCafeList() { .sorted(Comparator.comparing(Cafe::getId)) .map(cafe -> { Map cafeMap = new LinkedHashMap<>(); - cafeMap.put("id", cafe.getId().toString()); - cafeMap.put("name", cafe.getName()); + cafeMap.put("cafe_id", cafe.getId().toString()); + cafeMap.put("cafe_name", cafe.getName()); cafeMap.put("lat", cafe.getLat().toString()); cafeMap.put("lon", cafe.getLon().toString()); return cafeMap; @@ -50,30 +50,7 @@ public ResponseEntity>> getCafeList() { return ResponseEntity.ok(cafeList); } - /** - * 지역 ID를 기준으로 카페 목록 조회 (간략 정보만 반환) - */ - @GetMapping("/cafe/list/{areaId}") - public ResponseEntity>> getCafesByAreaId(@PathVariable Long areaId) { - log.info("지역 ID {}의 간략 카페 목록 조회", areaId); - List cafes = cafeRepository.findByAreaId(areaId); - log.info("지역 ID {}에서 총 {}개의 카페 찾음", areaId, cafes.size()); - - // ID 기준으로 정렬 및 간략 정보만 매핑 - List> simpleCafeList = cafes.stream() - .sorted(Comparator.comparing(Cafe::getId)) - .map(cafe -> { - Map cafeMap = new LinkedHashMap<>(); - cafeMap.put("id", cafe.getId().toString()); - cafeMap.put("name", cafe.getName()); - cafeMap.put("lat", cafe.getLat().toString()); - cafeMap.put("lon", cafe.getLon().toString()); - return cafeMap; - }) - .collect(Collectors.toList()); - return ResponseEntity.ok(simpleCafeList); - } @GetMapping("/info/cafe/{cafeId}") public ResponseEntity> getCafeInfo(@PathVariable Long cafeId) { @@ -85,11 +62,10 @@ public ResponseEntity> getCafeInfo(@PathVariable Long cafeId if (cafeOptional.isPresent()) { Cafe cafe = cafeOptional.get(); Map cafeInfo = new LinkedHashMap<>(); - cafeInfo.put("id", cafe.getId()); - cafeInfo.put("name", cafe.getName()); + cafeInfo.put("cafe_id", cafe.getId()); + cafeInfo.put("cafe_name", cafe.getName()); cafeInfo.put("address", cafe.getAddress()); cafeInfo.put("phone", cafe.getPhone()); - cafeInfo.put("category_code", cafe.getCategoryCode()); cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl()); cafeInfo.put("lat", cafe.getLat()); cafeInfo.put("lon", cafe.getLon()); From ae5289aa2eeab57bfa0a74103c90f3c9418ab1ed Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 30 Apr 2025 17:25:40 +0900 Subject: [PATCH 238/361] =?UTF-8?q?user-service=20ApplicationConfig=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/userservice/config/ApplicationConfig.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java index d7c9083..27f8287 100644 --- a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java @@ -15,10 +15,6 @@ import org.springframework.security.crypto.password.PasswordEncoder; import java.util.ArrayList; -<<<<<<< HEAD -======= -import java.util.Collections; ->>>>>>> upstream/Feature#54-유저-Gateway import java.util.List; @Configuration From d5b2342aeee6cab96f391ea0ff230e5d48c47657 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 30 Apr 2025 17:26:11 +0900 Subject: [PATCH 239/361] =?UTF-8?q?Refactor:weatheres=20=ED=98=84=EC=9E=AC?= =?UTF-8?q?=20=EB=82=A0=EC=A7=9C=EB=A1=9C=20=EB=B0=9B=EC=95=84=EC=98=A4?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/WeatherEsService.java | 20 ++++-- .../src/main/resources/application.properties | 2 +- .../userservice/config/ApplicationConfig.java | 64 ------------------- .../userservice/service/MemberService.java | 10 ++- 4 files changed, 18 insertions(+), 78 deletions(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java index 93316d7..9f2a0c0 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java @@ -5,18 +5,25 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.*; @Service @RequiredArgsConstructor public class WeatherEsService { - private final RestTemplate restTemplate = new RestTemplate(); - // 특정 지역 날씨 + // 오늘 날짜 기반 인덱스 이름 생성 + private String getTodayIndex() { + String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + return String.format("http://elasticsearch.seoultravel.life/seoul_citydata_weather_%s/_search", today); + } + + // 특정 지역 날씨 조회 public Map getWeatherFromES(String area) { - String url = "http://elasticsearch.seoultravel.life/seoul_citydata_weather_20250424/_search"; + String url = getTodayIndex(); Map term = Map.of("weather.area_nm", area); Map query = Map.of("term", term); @@ -37,9 +44,9 @@ public Map getWeatherFromES(String area) { return (Map) source.get("weather"); } - // 전체 지역 날씨 + // 전체 지역 날씨 조회 public List> getAllWeatherFromES() { - String url = "http://elasticsearch.seoultravel.life/seoul_citydata_weather_20250424/_search"; + String url = getTodayIndex(); Map body = Map.of( "size", 0, @@ -82,5 +89,4 @@ public List> getAllWeatherFromES() { return results; } - -} \ No newline at end of file +} diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 638be00..ac45c12 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -10,7 +10,7 @@ spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.defer-datasource-initialization=true #spring.jpa.hibernate.ddl-auto=update - spring.jpa.hibernate.ddl-auto=create +spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true #spring.sql.init.mode=always diff --git a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java index d7c9083..e69de29 100644 --- a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java @@ -1,64 +0,0 @@ -package com.example.userservice.config; - -import com.example.userservice.repository.jpa.MemberRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; - -import java.util.ArrayList; -<<<<<<< HEAD -======= -import java.util.Collections; ->>>>>>> upstream/Feature#54-유저-Gateway -import java.util.List; - -@Configuration -@RequiredArgsConstructor -public class ApplicationConfig { - - private final MemberRepository memberRepository; - - @Bean - public UserDetailsService userDetailsService() { - return username -> memberRepository.findByUserId(username) - .map(member -> { - List authorities = new ArrayList<>(); - // 사용자 역할에 따른 권한 추가 - authorities.add(new SimpleGrantedAuthority(member.getRole())); - - return new org.springframework.security.core.userdetails.User( - member.getUserId(), // userId를 사용자 이름으로 사용 - member.getPassword(), // 암호화된 비밀번호 - authorities // 권한 목록 - ); - }) - .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다")); - } - - @Bean - public AuthenticationProvider authenticationProvider() { - DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); - authProvider.setUserDetailsService(userDetailsService()); - authProvider.setPasswordEncoder(passwordEncoder()); - return authProvider; - } - - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { - return config.getAuthenticationManager(); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); // 비밀번호 암호화에 BCrypt 알고리즘 사용 - } -} \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java index 537ebf1..74e3ae6 100644 --- a/user-service/src/main/java/com/example/userservice/service/MemberService.java +++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java @@ -24,13 +24,12 @@ public Member registerUser(MemberSign dto) { return registerMember(dto, "ROLE_USER"); } -<<<<<<< HEAD -======= + // 관리자 회원가입 public Member registerAdmin(MemberSign dto) { return registerMember(dto, "ROLE_ADMIN"); } ->>>>>>> upstream/Feature#54-유저-Gateway + // 공통 회원가입 로직 private Member registerMember(MemberSign dto, String defaultRole) { @@ -128,7 +127,7 @@ public MemberDto.MemberResponse updateProfile(String userId, MemberDto.UpdateReq .created_at(updatedMember.getCreatedAt()) .build(); } -<<<<<<< HEAD + @Transactional public void deleteMember(String userId) { Member member = memberRepository.findByUserId(userId) @@ -157,6 +156,5 @@ public List getAllMembers() { .build()) .collect(Collectors.toList()); } -======= ->>>>>>> upstream/Feature#54-유저-Gateway + } \ No newline at end of file From 943f13a334b26fbfc7ad0c170017ebe613c30dc9 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 30 Apr 2025 17:32:00 +0900 Subject: [PATCH 240/361] =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.properties | 4 +- .../userservice/config/ApplicationConfig.java | 60 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 886c26f..43a3630 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -14,8 +14,8 @@ spring.jpa.defer-datasource-initialization=true #spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.ddl-auto=update -spring.jpa.hibernate.ddl-auto=update -#spring.jpa.hibernate.ddl-auto=create +# spring.jpa.hibernate.ddl-auto=update +# #spring.jpa.hibernate.ddl-auto=create spring.jpa.show-sql=true diff --git a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java index e69de29..27f8287 100644 --- a/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/ApplicationConfig.java @@ -0,0 +1,60 @@ +package com.example.userservice.config; + +import com.example.userservice.repository.jpa.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.ArrayList; +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class ApplicationConfig { + + private final MemberRepository memberRepository; + + @Bean + public UserDetailsService userDetailsService() { + return username -> memberRepository.findByUserId(username) + .map(member -> { + List authorities = new ArrayList<>(); + // 사용자 역할에 따른 권한 추가 + authorities.add(new SimpleGrantedAuthority(member.getRole())); + + return new org.springframework.security.core.userdetails.User( + member.getUserId(), // userId를 사용자 이름으로 사용 + member.getPassword(), // 암호화된 비밀번호 + authorities // 권한 목록 + ); + }) + .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다")); + } + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService()); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); // 비밀번호 암호화에 BCrypt 알고리즘 사용 + } +} \ No newline at end of file From 88e8fd3a8202bdc7a01b3bb3b425219fe3e41ef5 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 7 May 2025 09:26:36 +0900 Subject: [PATCH 241/361] =?UTF-8?q?Refactor:=20area=20dto=20name=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/example/placeservice/dto/AreaDto.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/dto/AreaDto.java b/place-service/src/main/java/com/example/placeservice/dto/AreaDto.java index f7d1673..b4e7583 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/AreaDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/AreaDto.java @@ -9,16 +9,16 @@ public class AreaDto { private Long area_id; private String seoul_id; - private String name; + private String area_name; private String name_eng; private String category; private BigDecimal lat; private BigDecimal lon; - public AreaDto(Long areaId, String seoulId, String name, String nameEng, String category, BigDecimal lat, BigDecimal lon) { + public AreaDto(Long areaId, String seoulId, String area_name, String nameEng, String category, BigDecimal lat, BigDecimal lon) { this.area_id = areaId; this.seoul_id = seoulId; - this.name = name; + this.area_name = area_name; this.name_eng = nameEng; this.category = category; this.lat = lat; From aad16394cf45a7b8baf69525f889231c392c7b2a Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 7 May 2025 09:58:44 +0900 Subject: [PATCH 242/361] =?UTF-8?q?gateway=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gateway/config/SecurityConfig.java | 24 ----- .../filter/JwtAuthenticationFilter.java | 102 ------------------ .../gateway/security/JwtTokenProvider.java | 71 ------------ 3 files changed, 197 deletions(-) delete mode 100644 gateway/src/main/java/com/example/gateway/config/SecurityConfig.java delete mode 100644 gateway/src/main/java/com/example/gateway/filter/JwtAuthenticationFilter.java delete mode 100644 gateway/src/main/java/com/example/gateway/security/JwtTokenProvider.java diff --git a/gateway/src/main/java/com/example/gateway/config/SecurityConfig.java b/gateway/src/main/java/com/example/gateway/config/SecurityConfig.java deleted file mode 100644 index 5e7673e..0000000 --- a/gateway/src/main/java/com/example/gateway/config/SecurityConfig.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.gateway.config; - - -import com.example.gateway.filter.JwtAuthenticationFilter; -import com.example.gateway.security.JwtTokenProvider; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; - -@Configuration -@EnableWebSecurity -public class SecurityConfig { - private final JwtTokenProvider jwtTokenProvider; - private final JwtAuthenticationFilter jwtAuthenticationFilter; - - public SecurityConfig(JwtTokenProvider jwtTokenProvider, JwtAuthenticationFilter jwtAuthenticationFilter) { - this.jwtTokenProvider = jwtTokenProvider; - this.jwtAuthenticationFilter = jwtAuthenticationFilter; - } - - @Bean - -} diff --git a/gateway/src/main/java/com/example/gateway/filter/JwtAuthenticationFilter.java b/gateway/src/main/java/com/example/gateway/filter/JwtAuthenticationFilter.java deleted file mode 100644 index d816b9e..0000000 --- a/gateway/src/main/java/com/example/gateway/filter/JwtAuthenticationFilter.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.example.gateway.filter; // 실제 프로젝트 패키지 구조에 맞게 수정하세요 - -import com.example.gateway.security.JwtTokenProvider; // JwtTokenProvider 위치 확인 -// import io.jsonwebtoken.JwtException; // 직접적인 사용은 줄어듦 -import lombok.extern.slf4j.Slf4j; -import org.springframework.cloud.gateway.filter.GatewayFilterChain; -import org.springframework.cloud.gateway.filter.GlobalFilter; -import org.springframework.core.Ordered; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.stereotype.Component; -import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Mono; - -import java.util.List; // 경로 제외 목록을 위해 추가 - -@Slf4j -@Component -public class JwtAuthenticationFilter implements GlobalFilter, Ordered { - - private final JwtTokenProvider jwtTokenProvider; - - // 인증이 필요 없는 경로 목록 (필요에 따라 추가/수정) - private final List excludedPaths = List.of( - "/user/auth/login", - "/user/auth/signup", - "/place/", - "/congestion/", - "/external/" - ); - - public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) { - this.jwtTokenProvider = jwtTokenProvider; - } - - @Override - public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { - ServerHttpRequest request = exchange.getRequest(); - String path = request.getURI().getPath(); - - boolean isExcluded = excludedPaths.stream().anyMatch(p -> path.startsWith(p)); - if (isExcluded) { - log.debug("인증 필터 제외 경로: {}", path); - return chain.filter(exchange); // 필터링 없이 다음 단계로 - } - - String token = extractToken(request); - - if (token == null) { - log.warn("요청 헤더에 JWT 토큰이 없습니다: {}", path); - return onError(exchange, "토큰 없음", HttpStatus.UNAUTHORIZED); - } - - if (jwtTokenProvider.validateToken(token)) { - log.debug("JWT 토큰 유효함: {}", path); - - // (선택 사항) 토큰이 유효하면 사용자 ID 같은 정보를 추출하여 요청 헤더에 추가할 수 있음 - // String userId = jwtTokenProvider.getUserIdFromToken(token); - // ServerHttpRequest modifiedRequest = exchange.getRequest().mutate() - // .header("X-USER-ID", userId) // 백엔드 서비스에서 사용할 헤더 이름 지정 - // .build(); - // return chain.filter(exchange.mutate().request(modifiedRequest).build()); - - // 기본: 수정 없이 다음 필터로 전달 - return chain.filter(exchange); - } else { - // validateToken에서 false를 반환 (내부적으로 로깅됨) - log.warn("유효하지 않은 JWT 토큰입니다: {}", path); - return onError(exchange, "토큰 유효하지 않음", HttpStatus.UNAUTHORIZED); - } - } - - private String extractToken(ServerHttpRequest request) { - String bearerToken = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); - if (bearerToken != null && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); // "Bearer " 이후의 토큰 값 반환 - } - log.debug("요청 헤더에 Bearer 토큰이 없습니다."); - return null; - } - - private Mono onError(ServerWebExchange exchange, String errMessage, HttpStatus httpStatus) { - ServerHttpResponse response = exchange.getResponse(); - response.setStatusCode(httpStatus); - log.error(errMessage + " - 요청 URI: {}", exchange.getRequest().getURI()); - return response.setComplete(); - } - - /** - * 필터의 실행 순서를 지정합니다. - * 일반적으로 인증 필터는 다른 필터들보다 먼저 실행되어야 하므로 낮은 값을 가집니다. - * Spring Security Filter보다는 뒤, 다른 로깅/라우팅 필터보다는 앞에 실행되도록 조정할 수 있습니다. - * (예: NettyRoutingFilter는 Ordered.LOWEST_PRECEDENCE, LoadBalancerClientFilter는 10100) - * @return 필터 순서 값 - */ - @Override - public int getOrder() { - return -1; // 높은 우선순위 (낮은 값) - } -} \ No newline at end of file diff --git a/gateway/src/main/java/com/example/gateway/security/JwtTokenProvider.java b/gateway/src/main/java/com/example/gateway/security/JwtTokenProvider.java deleted file mode 100644 index 3619f60..0000000 --- a/gateway/src/main/java/com/example/gateway/security/JwtTokenProvider.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.example.gateway.security; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.UnsupportedJwtException; -import io.jsonwebtoken.security.Keys; -import io.jsonwebtoken.security.SignatureException; -import jakarta.annotation.PostConstruct; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import javax.crypto.SecretKey; -import java.nio.charset.StandardCharsets; - -@Slf4j -@Component -public class JwtTokenProvider { - - @Value("${service.jwt.secret-key}") - private String secretKeyString; - private SecretKey secretKey; - - @PostConstruct - protected void init() { - if (secretKeyString == null || secretKeyString.isEmpty()) { - log.error("JWT Secret Key가 설정되지 않았습니다! 환경 변수 또는 설정 파일을 확인하세요."); - throw new IllegalStateException("JWT_SECRET_KEY가 설정되지 않았습니다!"); - } - this.secretKey = Keys.hmacShaKeyFor(secretKeyString.getBytes(StandardCharsets.UTF_8)); - log.info("JWT SecretKey 초기화 완료"); - } - - public boolean validateToken(String token) { - if (token == null || token.isEmpty()) { - log.warn("입력된 JWT 토큰이 비어 있습니다."); - return false; - } - try { - Jwts.parser() - .verifyWith(this.secretKey) - .build() - .parseSignedClaims(token); - return true; // 예외가 발생하지 않으면 유효함 - } catch (SignatureException e) { - log.error("JWT 서명이 유효하지 않습니다.", e); - } catch (MalformedJwtException e) { - log.error("JWT 토큰 형식이 잘못되었습니다.", e); - } catch (ExpiredJwtException e) { - log.warn("만료된 JWT 토큰입니다.", e); - } catch (UnsupportedJwtException e) { - log.error("지원되지 않는 JWT 토큰 형식입니다.", e); - } catch (IllegalArgumentException e) { - log.error("JWT 클레임 문자열이 비어 있습니다.", e); - } catch (Exception e) { // 그 외 예외 처리 - log.error("JWT 토큰 검증 중 알 수 없는 오류 발생", e); - } - return false; // 예외 발생 시 유효하지 않음 - } - - public String getUserIdFromToken(String token) { - Claims claims = Jwts.parser() - .verifyWith(this.secretKey) - .build() - .parseSignedClaims(token) - .getPayload(); - return claims.getSubject(); // Subject (userId) 반환 - } -} From 122f174ae514b2be34ac10807eed06e310b59c5f Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 7 May 2025 10:03:40 +0900 Subject: [PATCH 243/361] =?UTF-8?q?gateway=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway/.gitignore | 1 + .../main/java/com/example/gateway/config/SecurityConfig.java | 5 ----- .../com/example/gateway/filter/JwtAuthenticationFilter.java | 4 ---- .../java/com/example/gateway/security/JwtTokenProvider.java | 4 ---- 4 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 gateway/src/main/java/com/example/gateway/config/SecurityConfig.java delete mode 100644 gateway/src/main/java/com/example/gateway/filter/JwtAuthenticationFilter.java delete mode 100644 gateway/src/main/java/com/example/gateway/security/JwtTokenProvider.java diff --git a/gateway/.gitignore b/gateway/.gitignore index 8aca0b7..5dec76b 100644 --- a/gateway/.gitignore +++ b/gateway/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ src/main/resources/application.properties +.env ### STS ### .apt_generated diff --git a/gateway/src/main/java/com/example/gateway/config/SecurityConfig.java b/gateway/src/main/java/com/example/gateway/config/SecurityConfig.java deleted file mode 100644 index f90d1af..0000000 --- a/gateway/src/main/java/com/example/gateway/config/SecurityConfig.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.gateway.config; - -public class SecurityConfig { - -} diff --git a/gateway/src/main/java/com/example/gateway/filter/JwtAuthenticationFilter.java b/gateway/src/main/java/com/example/gateway/filter/JwtAuthenticationFilter.java deleted file mode 100644 index 7711023..0000000 --- a/gateway/src/main/java/com/example/gateway/filter/JwtAuthenticationFilter.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.gateway.filter; - -public class JwtAuthenticationFilter { -} diff --git a/gateway/src/main/java/com/example/gateway/security/JwtTokenProvider.java b/gateway/src/main/java/com/example/gateway/security/JwtTokenProvider.java deleted file mode 100644 index 83ac1fb..0000000 --- a/gateway/src/main/java/com/example/gateway/security/JwtTokenProvider.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.gateway.security; - -public class JwtTokenProvider { -} From 84294c01c9b711ada0fca0f04afcefcb1d9d4f4f Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 7 May 2025 10:12:34 +0900 Subject: [PATCH 244/361] =?UTF-8?q?build.gradle=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway/build.gradle | 5 ----- 1 file changed, 5 deletions(-) diff --git a/gateway/build.gradle b/gateway/build.gradle index e5bcefb..809df21 100644 --- a/gateway/build.gradle +++ b/gateway/build.gradle @@ -27,11 +27,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.cloud:spring-cloud-starter-gateway' implementation 'org.springframework.boot:spring-boot-starter-webflux' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'io.jsonwebtoken:jjwt-api:0.12.6' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } From ede888fc180c9bb305098f0610e5f5eeaad835b8 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 7 May 2025 10:40:14 +0900 Subject: [PATCH 245/361] =?UTF-8?q?auth/signup=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../userservice/controller/AdminSignup.java | 128 ------------------ 1 file changed, 128 deletions(-) delete mode 100644 user-service/src/main/java/com/example/userservice/controller/AdminSignup.java diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java deleted file mode 100644 index e069e82..0000000 --- a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.example.userservice.controller; - -import com.example.userservice.dto.MemberSign; -import com.example.userservice.entity.Member; -import com.example.userservice.security.JwtUtil; -import com.example.userservice.service.MemberService; -import com.example.userservice.service.TokenService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.web.bind.annotation.*; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/auth") -public class AdminSignup { - private final MemberService memberService; - private final JwtUtil jwtUtil; - private final TokenService tokenService; - - /** - * 일반 사용자 회원가입 API - * @param dto 회원가입 정보 (user_id, password, nickname, birth_year, mbti, gender) - * @return 토큰 및 사용자 정보가 포함된 응답 - */ - @PostMapping("/signup") - public ResponseEntity> signup(@RequestBody MemberSign dto) { - try { - // 디버깅 로그 출력 - System.out.println("회원가입 요청: userId=" + dto.getUserId() + ", nickname=" + dto.getNickname() - + ", birth_year=" + dto.getBirthYear() - + ", mbti=" + dto.getMbti() - + ", gender=" + dto.getGender()); - - // 일반 사용자로 등록 - Member member = memberService.registerUser(dto); - - System.out.println("회원가입 성공: memberId=" + member.getMemberId() - + ", userId=" + member.getUserId() - + ", nickname=" + member.getNickname() - + ", role=" + member.getRole()); - - // 사용자 권한 설정 - List authorities = Collections.singletonList( - new SimpleGrantedAuthority(member.getRole()) - ); - - // 회원가입 성공하면 토큰 발급 - UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities); - String accessToken = jwtUtil.generateToken(userDetails); - String refreshToken = jwtUtil.generateRefreshToken(userDetails); - - // 리프레시 토큰 저장 - tokenService.saveRefreshToken(member.getMemberId(), refreshToken); - - // 응답 생성 - Map response = new HashMap<>(); - response.put("accessToken", accessToken); - response.put("refreshToken", refreshToken); - response.put("member_id", String.valueOf(member.getMemberId())); - response.put("user_id", member.getUserId()); - response.put("nickname", member.getNickname()); - response.put("role", member.getRole()); - - return ResponseEntity.ok(response); - } catch (IllegalStateException e) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "회원가입 실패: " + e.getMessage()); - return ResponseEntity.badRequest().body(errorResponse); - } - } - - /** - * 관리자 회원가입 API - * @param dto 관리자 회원가입 정보 - * @return 토큰 및 관리자 정보가 포함된 응답 - */ - @PostMapping("/admin/signup") - public ResponseEntity> adminSignup(@RequestBody MemberSign dto) { - try { - // 디버깅 로그 출력 - System.out.println("관리자 회원가입 요청: userId=" + dto.getUserId() + ", nickname=" + dto.getNickname()); - - // 관리자로 등록 - Member member = memberService.registerAdmin(dto); - - System.out.println("관리자 회원가입 성공: memberId=" + member.getMemberId() - + ", userId=" + member.getUserId() - + ", nickname=" + member.getNickname() - + ", role=" + member.getRole()); - - // 관리자 권한 설정 - List authorities = Collections.singletonList( - new SimpleGrantedAuthority(member.getRole()) - ); - - // 회원가입 성공하면 토큰 발급 - UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities); - String accessToken = jwtUtil.generateToken(userDetails); - String refreshToken = jwtUtil.generateRefreshToken(userDetails); - - // 리프레시 토큰 저장 - tokenService.saveRefreshToken(member.getMemberId(), refreshToken); - - // 응답 생성 - Map response = new HashMap<>(); - response.put("accessToken", accessToken); - response.put("refreshToken", refreshToken); - response.put("member_id", String.valueOf(member.getMemberId())); - response.put("user_id", member.getUserId()); - response.put("nickname", member.getNickname()); - response.put("role", member.getRole()); - - return ResponseEntity.ok(response); - } catch (IllegalStateException e) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "관리자 회원가입 실패: " + e.getMessage()); - return ResponseEntity.badRequest().body(errorResponse); - } - } -} \ No newline at end of file From 8f90cebbd8dc8cd9aec598ab66c5f61b4404bb4e Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 7 May 2025 10:41:11 +0900 Subject: [PATCH 246/361] =?UTF-8?q?AdminSignup=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../userservice/controller/AdminSignup.java | 128 ------------------ 1 file changed, 128 deletions(-) delete mode 100644 user-service/src/main/java/com/example/userservice/controller/AdminSignup.java diff --git a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java b/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java deleted file mode 100644 index e069e82..0000000 --- a/user-service/src/main/java/com/example/userservice/controller/AdminSignup.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.example.userservice.controller; - -import com.example.userservice.dto.MemberSign; -import com.example.userservice.entity.Member; -import com.example.userservice.security.JwtUtil; -import com.example.userservice.service.MemberService; -import com.example.userservice.service.TokenService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.web.bind.annotation.*; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/auth") -public class AdminSignup { - private final MemberService memberService; - private final JwtUtil jwtUtil; - private final TokenService tokenService; - - /** - * 일반 사용자 회원가입 API - * @param dto 회원가입 정보 (user_id, password, nickname, birth_year, mbti, gender) - * @return 토큰 및 사용자 정보가 포함된 응답 - */ - @PostMapping("/signup") - public ResponseEntity> signup(@RequestBody MemberSign dto) { - try { - // 디버깅 로그 출력 - System.out.println("회원가입 요청: userId=" + dto.getUserId() + ", nickname=" + dto.getNickname() - + ", birth_year=" + dto.getBirthYear() - + ", mbti=" + dto.getMbti() - + ", gender=" + dto.getGender()); - - // 일반 사용자로 등록 - Member member = memberService.registerUser(dto); - - System.out.println("회원가입 성공: memberId=" + member.getMemberId() - + ", userId=" + member.getUserId() - + ", nickname=" + member.getNickname() - + ", role=" + member.getRole()); - - // 사용자 권한 설정 - List authorities = Collections.singletonList( - new SimpleGrantedAuthority(member.getRole()) - ); - - // 회원가입 성공하면 토큰 발급 - UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities); - String accessToken = jwtUtil.generateToken(userDetails); - String refreshToken = jwtUtil.generateRefreshToken(userDetails); - - // 리프레시 토큰 저장 - tokenService.saveRefreshToken(member.getMemberId(), refreshToken); - - // 응답 생성 - Map response = new HashMap<>(); - response.put("accessToken", accessToken); - response.put("refreshToken", refreshToken); - response.put("member_id", String.valueOf(member.getMemberId())); - response.put("user_id", member.getUserId()); - response.put("nickname", member.getNickname()); - response.put("role", member.getRole()); - - return ResponseEntity.ok(response); - } catch (IllegalStateException e) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "회원가입 실패: " + e.getMessage()); - return ResponseEntity.badRequest().body(errorResponse); - } - } - - /** - * 관리자 회원가입 API - * @param dto 관리자 회원가입 정보 - * @return 토큰 및 관리자 정보가 포함된 응답 - */ - @PostMapping("/admin/signup") - public ResponseEntity> adminSignup(@RequestBody MemberSign dto) { - try { - // 디버깅 로그 출력 - System.out.println("관리자 회원가입 요청: userId=" + dto.getUserId() + ", nickname=" + dto.getNickname()); - - // 관리자로 등록 - Member member = memberService.registerAdmin(dto); - - System.out.println("관리자 회원가입 성공: memberId=" + member.getMemberId() - + ", userId=" + member.getUserId() - + ", nickname=" + member.getNickname() - + ", role=" + member.getRole()); - - // 관리자 권한 설정 - List authorities = Collections.singletonList( - new SimpleGrantedAuthority(member.getRole()) - ); - - // 회원가입 성공하면 토큰 발급 - UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities); - String accessToken = jwtUtil.generateToken(userDetails); - String refreshToken = jwtUtil.generateRefreshToken(userDetails); - - // 리프레시 토큰 저장 - tokenService.saveRefreshToken(member.getMemberId(), refreshToken); - - // 응답 생성 - Map response = new HashMap<>(); - response.put("accessToken", accessToken); - response.put("refreshToken", refreshToken); - response.put("member_id", String.valueOf(member.getMemberId())); - response.put("user_id", member.getUserId()); - response.put("nickname", member.getNickname()); - response.put("role", member.getRole()); - - return ResponseEntity.ok(response); - } catch (IllegalStateException e) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "관리자 회원가입 실패: " + e.getMessage()); - return ResponseEntity.badRequest().body(errorResponse); - } - } -} \ No newline at end of file From 487070bf64926902b2c9df94c80864c73b8880a3 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 7 May 2025 10:47:38 +0900 Subject: [PATCH 247/361] =?UTF-8?q?=EC=A3=BC=EC=B0=A8=EC=9E=A5(es)?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=98=84=EC=9E=AC=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=EB=A1=9C=20=EB=B0=9B=EC=95=84=EC=98=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ParkEsService.java | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java index 5336d7f..96ea715 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java @@ -6,6 +6,8 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.*; @Service @@ -15,9 +17,15 @@ public class ParkEsService { private final RestTemplate restTemplate = new RestTemplate(); + // 오늘 날짜 기반 인덱스 URL 생성 + private String getTodayIndexUrl() { + String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + return String.format("http://elasticsearch.seoultravel.life/seoul_citydata_parking_%s/_search", today); + } + // 특정 지역 주차장 정보 public Map getParkFromES(String areaId) { - String url = "http://elasticsearch.seoultravel.life/seoul_citydata_parking_20250424/_search"; + String url = getTodayIndexUrl(); log.info("Requesting park info for areaId: {}", areaId); Map term = Map.of("parking.area_nm", areaId); @@ -42,17 +50,15 @@ public Map getParkFromES(String areaId) { } Map source = (Map) hits.get(0).get("_source"); - log.info("Source: {}", source); - Map parking = (Map) source.get("parking"); - log.info("Parking data: {}", parking); + log.info("Parking data: {}", parking); return parking; } // 전체 지역 주차장 정보 public List> getAllParkFromES() { - String url = "http://elasticsearch.seoultravel.life/seoul_citydata_parking_20250424/_search"; + String url = getTodayIndexUrl(); log.info("Requesting all park info"); Map body = Map.of( @@ -76,11 +82,8 @@ public List> getAllParkFromES() { headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity> entity = new HttpEntity<>(body, headers); - log.info("Request body for all parks: {}", body); - ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class); - log.info("Response status for all parks: {}", response.getStatusCode()); - log.info("Response body keys: {}", response.getBody().keySet()); + log.info("Response status: {}", response.getStatusCode()); if (!response.getBody().containsKey("aggregations")) { log.error("Response does not contain 'aggregations' key"); @@ -88,14 +91,8 @@ public List> getAllParkFromES() { } Map aggregations = (Map) response.getBody().get("aggregations"); - if (!aggregations.containsKey("by_area")) { - log.error("Aggregations does not contain 'by_area' key"); - return Collections.emptyList(); - } - Map byArea = (Map) aggregations.get("by_area"); List> buckets = (List>) byArea.get("buckets"); - log.info("Found {} buckets", buckets.size()); List> results = new ArrayList<>(); for (Map bucket : buckets) { @@ -119,4 +116,4 @@ public List> getAllParkFromES() { log.info("Returning {} park results", results.size()); return results; } -} \ No newline at end of file +} From 81228274f50d5b155f4aa311c8cf9d6937a00971 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 7 May 2025 11:03:58 +0900 Subject: [PATCH 248/361] =?UTF-8?q?gateway=20application.properties=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.properties | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties index 3b4e197..1596be3 100644 --- a/gateway/src/main/resources/application.properties +++ b/gateway/src/main/resources/application.properties @@ -1,17 +1,23 @@ spring.application.name=gateway server.port=8080 -spring.cloud.gateway.routes[0].id=realtime-control-service +spring.cloud.gateway.routes[0].id=congestion-service spring.cloud.gateway.routes[0].uri=http://localhost:8081 -spring.cloud.gateway.routes[0].predicates[0]=Path=/control/** -spring.cloud.gateway.routes[0].filters[0]=RewritePath=/control/(?.*), /$\{segment} +spring.cloud.gateway.routes[0].predicates[0]=Path=/control/congestion/** +spring.cloud.gateway.routes[0].filters[0]=RewritePath=/control/congestion/(?.*), /$\{segment} -spring.cloud.gateway.routes[1].id=place-service -spring.cloud.gateway.routes[1].uri=http://localhost:8082 -spring.cloud.gateway.routes[1].predicates[0]=Path=/place/** -spring.cloud.gateway.routes[1].filters[0]=RewritePath=/place/(?.*), /$\{segment} +spring.cloud.gateway.routes[1].id=external-service +spring.cloud.gateway.routes[1].uri=http://localhost:8081 +spring.cloud.gateway.routes[1].predicates[0]=Path=/control/external/** +spring.cloud.gateway.routes[1].filters[0]=RewritePath=/control/external/(?.*), /$\{segment} -spring.cloud.gateway.routes[2].id=user-service -spring.cloud.gateway.routes[2].uri=http://localhost:8083 -spring.cloud.gateway.routes[2].predicates[0]=Path=/user/** -spring.cloud.gateway.routes[2].filters[0]=RewritePath=/user/(?.*), /$\{segment} \ No newline at end of file + +spring.cloud.gateway.routes[2].id=place-service +spring.cloud.gateway.routes[2].uri=http://localhost:8082 +spring.cloud.gateway.routes[2].predicates[0]=Path=/place/** +spring.cloud.gateway.routes[2].filters[0]=RewritePath=/place/(?.*), /$\{segment} + +spring.cloud.gateway.routes[3].id=user-service +spring.cloud.gateway.routes[3].uri=http://localhost:8083 +spring.cloud.gateway.routes[3].predicates[0]=Path=/user/** +spring.cloud.gateway.routes[3].filters[0]=RewritePath=/user/(?.*), /$\{segment} \ No newline at end of file From 838693bb848502ccb8961d77d94e95489e0d3b16 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 7 May 2025 14:03:53 +0900 Subject: [PATCH 249/361] =?UTF-8?q?Refactor:=20favorite=20API=20-=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=20api=20=ED=98=B8=EC=B6=9C=20localhost?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=84=9C=EB=B2=84url=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/userservice/service/FavoriteService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java index eadf605..4747daf 100644 --- a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java +++ b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java @@ -34,7 +34,7 @@ public List getListData(String userId) { return items.stream() .map(item -> { ResponseEntity response = restTemplate.exchange( - "http://localhost:8082/main/info/"+item.getType()+"/" + item.getPlaceId(), + "http://api.seoultravel.life/place/main/info/"+item.getType()+"/" + item.getPlaceId(), HttpMethod.GET, null, JsonNode.class @@ -114,7 +114,7 @@ public List> getAllFavoriteData() { List favoriteDtoList = items.stream() .map(item ->{ ResponseEntity response = restTemplate.exchange( - "http://localhost:8082/main/info/"+item.getType()+"/" + item.getPlaceId(), + "http://api.seoultravel.life/place/main/info/"+item.getType()+"/" + item.getPlaceId(), HttpMethod.GET, null, JsonNode.class From ad2cd86ec8f0a1bced9700d72343e9bd26fd5a64 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 7 May 2025 14:44:40 +0900 Subject: [PATCH 250/361] =?UTF-8?q?Refactor:=20favorite=20API=20-=20respon?= =?UTF-8?q?se=20name=20=EC=A0=80=EC=9E=A5=20=EB=B3=80=EC=88=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20type+=5Fname?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/userservice/service/FavoriteService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java index 4747daf..73c559c 100644 --- a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java +++ b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java @@ -33,13 +33,14 @@ public List getListData(String userId) { return items.stream() .map(item -> { + ResponseEntity response = restTemplate.exchange( "http://api.seoultravel.life/place/main/info/"+item.getType()+"/" + item.getPlaceId(), HttpMethod.GET, null, JsonNode.class ); - String name = response.getBody().get("name").asText(); + String name = response.getBody().get(item.getType()+"_name").asText(); String address = response.getBody().get("address").asText(); return new FavoriteDto( item.getFavoriteId(), @@ -119,7 +120,7 @@ public List> getAllFavoriteData() { null, JsonNode.class ); - String name = response.getBody().get("name").asText(); + String name = response.getBody().get(item.getType()+"_name").asText(); String address = response.getBody().get("address").asText(); return new FavoriteDto( From a3d95a42dc341ec10b15e669ad9a2c87642ea285 Mon Sep 17 00:00:00 2001 From: jaehoon Date: Wed, 7 May 2025 16:17:14 +0900 Subject: [PATCH 251/361] =?UTF-8?q?feat:fastAPI=EC=99=80=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=ED=9B=84=20=EB=A6=AC=EB=B7=B0=20=ED=82=A4=EC=9B=8C?= =?UTF-8?q?=EB=93=9C=20=EC=9A=94=EC=95=BD=20=ED=98=B8=EC=B6=9C=20API=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- place-service/build.gradle | 1 + .../controller/AiSummaryController.java | 24 +++++++ .../dto/AiSummaryParsedResponse.java | 14 ++++ .../placeservice/dto/AiSummaryResponse.java | 11 +++ .../service/AiSummaryService.java | 69 +++++++++++++++++++ .../src/main/resources/application.properties | 5 +- 6 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 place-service/src/main/java/com/example/placeservice/controller/AiSummaryController.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AiSummaryParsedResponse.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/AiSummaryResponse.java create mode 100644 place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java diff --git a/place-service/build.gradle b/place-service/build.gradle index 07bd32c..1db981b 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -42,6 +42,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-webflux' //implementation 'io.github.cdimascio:dotenv-java:3.0.0' // 최신 버전 implementation 'io.github.cdimascio:dotenv-java:2.2.4' implementation 'org.apache.kafka:kafka-streams' diff --git a/place-service/src/main/java/com/example/placeservice/controller/AiSummaryController.java b/place-service/src/main/java/com/example/placeservice/controller/AiSummaryController.java new file mode 100644 index 0000000..f318229 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/controller/AiSummaryController.java @@ -0,0 +1,24 @@ +package com.example.placeservice.controller; + +import com.example.placeservice.dto.AiSummaryParsedResponse; +import com.example.placeservice.service.AiSummaryService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/summary") +@RequiredArgsConstructor +public class AiSummaryController { + + private final AiSummaryService aiSummaryService; + + @GetMapping("/{targetType}/{targetId}") + public ResponseEntity getSummaryParsed( + @PathVariable String targetType, + @PathVariable String targetId) { + + AiSummaryParsedResponse response = aiSummaryService.getSummaryParsed(targetType, targetId); + return ResponseEntity.ok(response); + } +} diff --git a/place-service/src/main/java/com/example/placeservice/dto/AiSummaryParsedResponse.java b/place-service/src/main/java/com/example/placeservice/dto/AiSummaryParsedResponse.java new file mode 100644 index 0000000..4df31e7 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AiSummaryParsedResponse.java @@ -0,0 +1,14 @@ +package com.example.placeservice.dto; + +import lombok.Builder; +import lombok.Getter; +import java.util.List; + +@Getter +@Builder +public class AiSummaryParsedResponse { + private List positiveKeywords; + private List negativeKeywords; + private int positiveCount; + private int negativeCount; +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/dto/AiSummaryResponse.java b/place-service/src/main/java/com/example/placeservice/dto/AiSummaryResponse.java new file mode 100644 index 0000000..df58fd2 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/AiSummaryResponse.java @@ -0,0 +1,11 @@ +package com.example.placeservice.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class AiSummaryResponse { + private String content; + +} diff --git a/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java b/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java new file mode 100644 index 0000000..7313664 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java @@ -0,0 +1,69 @@ +package com.example.placeservice.service; + +import com.example.placeservice.dto.AiSummaryResponse; +import com.example.placeservice.dto.AiSummaryParsedResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Service +public class AiSummaryService { + + private final WebClient webClient; + + // fastapi-svc 값을 application.properties에서 주입 + public AiSummaryService(@Value("${fastapi-svc}") String fastapiUrl) { + this.webClient = WebClient.builder() + .baseUrl(fastapiUrl) + .build(); + } + + public AiSummaryParsedResponse getSummaryParsed(String targetType, String targetId) { + AiSummaryResponse raw = webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/summary/{target_type}/{target_id}") + .build(targetType, targetId)) + .retrieve() + .bodyToMono(AiSummaryResponse.class) + .block(); + + return parseSummaryContent(raw); + } + + private AiSummaryParsedResponse parseSummaryContent(AiSummaryResponse raw) { + String content = raw.getContent(); + + List posKeywords = extractKeywords(content, "\\[긍정 키워드\\](.*)"); + List negKeywords = extractKeywords(content, "\\[부정 키워드\\](.*)"); + int posCount = extractInt(content, "\\[긍정 라벨 수\\]\\s*(\\d+)"); + int negCount = extractInt(content, "\\[부정 라벨 수\\]\\s*(\\d+)"); + + return AiSummaryParsedResponse.builder() + .positiveKeywords(posKeywords) + .negativeKeywords(negKeywords) + .positiveCount(posCount) + .negativeCount(negCount) + .build(); + } + + private List extractKeywords(String content, String pattern) { + Matcher matcher = Pattern.compile(pattern).matcher(content); + if (matcher.find()) { + String[] keywords = matcher.group(1).trim().split("\\s*,\\s*"); + return Arrays.stream(keywords) + .filter(k -> !k.isBlank()) + .toList(); + } + return List.of(); + } + + private int extractInt(String content, String pattern) { + Matcher matcher = Pattern.compile(pattern).matcher(content); + return matcher.find() ? Integer.parseInt(matcher.group(1)) : 0; + } +} diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties index 43a3630..09704cb 100644 --- a/place-service/src/main/resources/application.properties +++ b/place-service/src/main/resources/application.properties @@ -25,4 +25,7 @@ spring.jpa.show-sql=true kakao.api.key=${KAKAO_API_KEY} # ?? ?? -logging.level.com.example.placeservice=INFO \ No newline at end of file +logging.level.com.example.placeservice=INFO + +#fastApi URL +fastapi-svc=http://localhost:8000 From 1f8ae00dee39c713ef9ce0327b1d7fbf806060b8 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 8 May 2025 14:21:24 +0900 Subject: [PATCH 252/361] =?UTF-8?q?Feat:=20user-CORS=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../userservice/config/SecurityConfig.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java index 9faf817..d2d3226 100644 --- a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java @@ -2,15 +2,23 @@ import com.example.userservice.security.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; +import org.apache.catalina.filters.CorsFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; @Configuration @EnableWebSecurity @@ -23,6 +31,7 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http + .cors(Customizer.withDefaults()) // cors 활성화 .csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화 .authorizeHttpRequests(auth -> auth // 공개 엔드포인트 - 인증 불필요 @@ -46,4 +55,19 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } + + // CORS 설정 추가 + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(List.of("*")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(false); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } + } \ No newline at end of file From f9ee92b874bcd46334db91d120f2bb5a6cda2b2e Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 8 May 2025 15:22:29 +0900 Subject: [PATCH 253/361] =?UTF-8?q?Feat:=20gateway-cors=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 --- .../java/com/example/gateway/config/Cors.java | 25 +++++++++++++++++++ .../src/main/resources/application.properties | 8 +++++- .../userservice/config/SecurityConfig.java | 3 --- .../controller/SignupController.java | 2 ++ 4 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 gateway/src/main/java/com/example/gateway/config/Cors.java diff --git a/gateway/src/main/java/com/example/gateway/config/Cors.java b/gateway/src/main/java/com/example/gateway/config/Cors.java new file mode 100644 index 0000000..e6635b7 --- /dev/null +++ b/gateway/src/main/java/com/example/gateway/config/Cors.java @@ -0,0 +1,25 @@ +package com.example.gateway.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.CorsWebFilter; +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; + +@Configuration +public class Cors { + + @Bean + public CorsWebFilter corsWebFilter() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(false); // 인증 정보 포함 X + config.addAllowedOrigin("*"); // 모든 도메인 허용 + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); // 모든 경로에 적용 + + return new CorsWebFilter(source); + } +} diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties index 1596be3..4fd53a9 100644 --- a/gateway/src/main/resources/application.properties +++ b/gateway/src/main/resources/application.properties @@ -20,4 +20,10 @@ spring.cloud.gateway.routes[2].filters[0]=RewritePath=/place/(?.*), /$\ spring.cloud.gateway.routes[3].id=user-service spring.cloud.gateway.routes[3].uri=http://localhost:8083 spring.cloud.gateway.routes[3].predicates[0]=Path=/user/** -spring.cloud.gateway.routes[3].filters[0]=RewritePath=/user/(?.*), /$\{segment} \ No newline at end of file +spring.cloud.gateway.routes[3].filters[0]=RewritePath=/user/(?.*), /$\{segment} + + +# ?? ?? +logging.level.org.springframework.cloud.gateway=DEBUG +logging.level.reactor.netty.http.client=DEBUG +logging.level.org.springframework.web.server.adapter.HttpWebHandlerAdapter=DEBUG \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java index d2d3226..6b5a4a6 100644 --- a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java @@ -2,11 +2,8 @@ import com.example.userservice.security.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; -import org.apache.catalina.filters.CorsFilter; -import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; diff --git a/user-service/src/main/java/com/example/userservice/controller/SignupController.java b/user-service/src/main/java/com/example/userservice/controller/SignupController.java index 403e7c8..c31f90b 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SignupController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SignupController.java @@ -75,6 +75,8 @@ public ResponseEntity> signup(@RequestBody MemberSign dto) { Map errorResponse = new HashMap<>(); errorResponse.put("error", "회원가입 실패: " + e.getMessage()); return ResponseEntity.badRequest().body(errorResponse); + } catch (Exception e) { + throw new RuntimeException(e); } } From c698b70ce1091fa053aba384232eafb72ce626d7 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 8 May 2025 15:42:17 +0900 Subject: [PATCH 254/361] =?UTF-8?q?Refactor:=20gateway-cors=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/example/gateway/config/Cors.java | 4 ++++ gateway/src/main/resources/application.properties | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/gateway/src/main/java/com/example/gateway/config/Cors.java b/gateway/src/main/java/com/example/gateway/config/Cors.java index e6635b7..14a3d0f 100644 --- a/gateway/src/main/java/com/example/gateway/config/Cors.java +++ b/gateway/src/main/java/com/example/gateway/config/Cors.java @@ -14,6 +14,7 @@ public CorsWebFilter corsWebFilter() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(false); // 인증 정보 포함 X config.addAllowedOrigin("*"); // 모든 도메인 허용 + config.addAllowedOrigin("http://localhost:5173"); // 특정 도메인 허용 config.addAllowedHeader("*"); config.addAllowedMethod("*"); @@ -22,4 +23,7 @@ public CorsWebFilter corsWebFilter() { return new CorsWebFilter(source); } + + + } diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties index 4fd53a9..98d1997 100644 --- a/gateway/src/main/resources/application.properties +++ b/gateway/src/main/resources/application.properties @@ -26,4 +26,11 @@ spring.cloud.gateway.routes[3].filters[0]=RewritePath=/user/(?.*), /$\{ # ?? ?? logging.level.org.springframework.cloud.gateway=DEBUG logging.level.reactor.netty.http.client=DEBUG -logging.level.org.springframework.web.server.adapter.HttpWebHandlerAdapter=DEBUG \ No newline at end of file +logging.level.org.springframework.web.server.adapter.HttpWebHandlerAdapter=DEBUG + + +# CORS ?? +spring.cloud.gateway.default-filters[0].name=CORS +spring.cloud.gateway.default-filters[0].args.allowedOrigins=http://localhost:5173,* +spring.cloud.gateway.default-filters[0].args.allowedMethods=* +spring.cloud.gateway.default-filters[0].args.allowedHeaders=* From 1dcf73b06a86759c45dce5effa563e90a0ece668 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 8 May 2025 16:29:16 +0900 Subject: [PATCH 255/361] =?UTF-8?q?Refactor:=20gateway-cors=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/gateway/config/Cors.java | 7 +++-- .../src/main/resources/application.properties | 1 + .../userservice/config/SecurityConfig.java | 31 +++++++------------ 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/gateway/src/main/java/com/example/gateway/config/Cors.java b/gateway/src/main/java/com/example/gateway/config/Cors.java index 14a3d0f..329a5b8 100644 --- a/gateway/src/main/java/com/example/gateway/config/Cors.java +++ b/gateway/src/main/java/com/example/gateway/config/Cors.java @@ -12,9 +12,10 @@ public class Cors { @Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config = new CorsConfiguration(); - config.setAllowCredentials(false); // 인증 정보 포함 X - config.addAllowedOrigin("*"); // 모든 도메인 허용 - config.addAllowedOrigin("http://localhost:5173"); // 특정 도메인 허용 + config.setAllowCredentials(true); // 인증 정보 포함 X + config.addAllowedOrigin("http://localhost:5173"); // 특정 도메인 + config.addAllowedOrigin("http://58.127.241.84:5173"); + config.addAllowedOrigin("http://192.168.0.186:5173"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties index 98d1997..21cd526 100644 --- a/gateway/src/main/resources/application.properties +++ b/gateway/src/main/resources/application.properties @@ -24,6 +24,7 @@ spring.cloud.gateway.routes[3].filters[0]=RewritePath=/user/(?.*), /$\{ # ?? ?? +logging.level.root=DEBUG logging.level.org.springframework.cloud.gateway=DEBUG logging.level.reactor.netty.http.client=DEBUG logging.level.org.springframework.web.server.adapter.HttpWebHandlerAdapter=DEBUG diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java index 6b5a4a6..b9ee164 100644 --- a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java @@ -5,17 +5,13 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import java.util.List; @Configuration @EnableWebSecurity @@ -28,7 +24,17 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .cors(Customizer.withDefaults()) // cors 활성화 + .cors(cors -> cors.configurationSource(request -> { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOrigin("http://192.168.0.186:5173"); + config.addAllowedOrigin("http://localhost:5173"); + config.addAllowedOrigin("http://58.127.241.84:5173"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + return config; + + })) .csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화 .authorizeHttpRequests(auth -> auth // 공개 엔드포인트 - 인증 불필요 @@ -52,19 +58,4 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } - - // CORS 설정 추가 - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOrigins(List.of("*")); - config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); - config.setAllowedHeaders(List.of("*")); - config.setAllowCredentials(false); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); - return source; - } - } \ No newline at end of file From 68163dbe8404378b70b30f39156805493c875a91 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 8 May 2025 16:42:23 +0900 Subject: [PATCH 256/361] =?UTF-8?q?Refactor:=20gateway-cors=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/gateway/config/Cors.java | 61 ++++++++++--------- .../src/main/resources/application.properties | 2 +- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/gateway/src/main/java/com/example/gateway/config/Cors.java b/gateway/src/main/java/com/example/gateway/config/Cors.java index 329a5b8..f6586b2 100644 --- a/gateway/src/main/java/com/example/gateway/config/Cors.java +++ b/gateway/src/main/java/com/example/gateway/config/Cors.java @@ -1,30 +1,31 @@ -package com.example.gateway.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.reactive.CorsWebFilter; -import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; - -@Configuration -public class Cors { - - @Bean - public CorsWebFilter corsWebFilter() { - CorsConfiguration config = new CorsConfiguration(); - config.setAllowCredentials(true); // 인증 정보 포함 X - config.addAllowedOrigin("http://localhost:5173"); // 특정 도메인 - config.addAllowedOrigin("http://58.127.241.84:5173"); - config.addAllowedOrigin("http://192.168.0.186:5173"); - config.addAllowedHeader("*"); - config.addAllowedMethod("*"); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); // 모든 경로에 적용 - - return new CorsWebFilter(source); - } - - - -} +//package com.example.gateway.config; +// 테스트 + +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.web.cors.CorsConfiguration; +//import org.springframework.web.cors.reactive.CorsWebFilter; +//import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; +// +//@Configuration +//public class Cors { +// +// @Bean +// public CorsWebFilter corsWebFilter() { +// CorsConfiguration config = new CorsConfiguration(); +// config.setAllowCredentials(true); // 인증 정보 포함 X +// config.addAllowedOrigin("http://localhost:5173"); // 특정 도메인 +// config.addAllowedOrigin("http://58.127.241.84:5173"); +// config.addAllowedOrigin("http://192.168.0.186:5173"); +// config.addAllowedHeader("*"); +// config.addAllowedMethod("*"); +// +// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); +// source.registerCorsConfiguration("/**", config); // 모든 경로에 적용 +// +// return new CorsWebFilter(source); +// } +// +// +// +//} diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties index 21cd526..7b0ae1f 100644 --- a/gateway/src/main/resources/application.properties +++ b/gateway/src/main/resources/application.properties @@ -32,6 +32,6 @@ logging.level.org.springframework.web.server.adapter.HttpWebHandlerAdapter=DEBUG # CORS ?? spring.cloud.gateway.default-filters[0].name=CORS -spring.cloud.gateway.default-filters[0].args.allowedOrigins=http://localhost:5173,* +spring.cloud.gateway.default-filters[0].args.allowedOrigins=http://localhost:5173,http://192.168.0.186:5173,http://58.127.241.84:5173 spring.cloud.gateway.default-filters[0].args.allowedMethods=* spring.cloud.gateway.default-filters[0].args.allowedHeaders=* From 89e27155b6a749b2d939b8d2ac54fdb1ef13930d Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 8 May 2025 16:51:42 +0900 Subject: [PATCH 257/361] =?UTF-8?q?Refactor:=20gateway-cors=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/gateway/config/Cors.java | 61 +++++++++---------- .../src/main/resources/application.properties | 5 ++ 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/gateway/src/main/java/com/example/gateway/config/Cors.java b/gateway/src/main/java/com/example/gateway/config/Cors.java index f6586b2..329a5b8 100644 --- a/gateway/src/main/java/com/example/gateway/config/Cors.java +++ b/gateway/src/main/java/com/example/gateway/config/Cors.java @@ -1,31 +1,30 @@ -//package com.example.gateway.config; -// 테스트 - -//import org.springframework.context.annotation.Bean; -//import org.springframework.context.annotation.Configuration; -//import org.springframework.web.cors.CorsConfiguration; -//import org.springframework.web.cors.reactive.CorsWebFilter; -//import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; -// -//@Configuration -//public class Cors { -// -// @Bean -// public CorsWebFilter corsWebFilter() { -// CorsConfiguration config = new CorsConfiguration(); -// config.setAllowCredentials(true); // 인증 정보 포함 X -// config.addAllowedOrigin("http://localhost:5173"); // 특정 도메인 -// config.addAllowedOrigin("http://58.127.241.84:5173"); -// config.addAllowedOrigin("http://192.168.0.186:5173"); -// config.addAllowedHeader("*"); -// config.addAllowedMethod("*"); -// -// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); -// source.registerCorsConfiguration("/**", config); // 모든 경로에 적용 -// -// return new CorsWebFilter(source); -// } -// -// -// -//} +package com.example.gateway.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.CorsWebFilter; +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; + +@Configuration +public class Cors { + + @Bean + public CorsWebFilter corsWebFilter() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); // 인증 정보 포함 X + config.addAllowedOrigin("http://localhost:5173"); // 특정 도메인 + config.addAllowedOrigin("http://58.127.241.84:5173"); + config.addAllowedOrigin("http://192.168.0.186:5173"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); // 모든 경로에 적용 + + return new CorsWebFilter(source); + } + + + +} diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.properties index 7b0ae1f..2769dfb 100644 --- a/gateway/src/main/resources/application.properties +++ b/gateway/src/main/resources/application.properties @@ -35,3 +35,8 @@ spring.cloud.gateway.default-filters[0].name=CORS spring.cloud.gateway.default-filters[0].args.allowedOrigins=http://localhost:5173,http://192.168.0.186:5173,http://58.127.241.84:5173 spring.cloud.gateway.default-filters[0].args.allowedMethods=* spring.cloud.gateway.default-filters[0].args.allowedHeaders=* + +spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedOrigins=http://localhost:5173,http://192.168.0.186:5173,http://58.127.241.84:5173 +spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedMethods=GET,POST,PUT,DELETE,OPTIONS +spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedHeaders=* +spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowCredentials=true \ No newline at end of file From 4e728fa1a3f83bc5b93e2c3e112cb36ffc578c40 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 8 May 2025 17:06:59 +0900 Subject: [PATCH 258/361] =?UTF-8?q?Refactor:=20cors=EC=98=A4=EB=A5=98-gate?= =?UTF-8?q?way=20properties=ED=8C=8C=EC=9D=BC=20yml=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/gateway/config/Cors.java | 60 +++++++++---------- ...application.properties => application.txt} | 8 +-- gateway/src/main/resources/application.yml | 53 ++++++++++++++++ 3 files changed, 87 insertions(+), 34 deletions(-) rename gateway/src/main/resources/{application.properties => application.txt} (84%) create mode 100644 gateway/src/main/resources/application.yml diff --git a/gateway/src/main/java/com/example/gateway/config/Cors.java b/gateway/src/main/java/com/example/gateway/config/Cors.java index 329a5b8..e593516 100644 --- a/gateway/src/main/java/com/example/gateway/config/Cors.java +++ b/gateway/src/main/java/com/example/gateway/config/Cors.java @@ -1,30 +1,30 @@ -package com.example.gateway.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.reactive.CorsWebFilter; -import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; - -@Configuration -public class Cors { - - @Bean - public CorsWebFilter corsWebFilter() { - CorsConfiguration config = new CorsConfiguration(); - config.setAllowCredentials(true); // 인증 정보 포함 X - config.addAllowedOrigin("http://localhost:5173"); // 특정 도메인 - config.addAllowedOrigin("http://58.127.241.84:5173"); - config.addAllowedOrigin("http://192.168.0.186:5173"); - config.addAllowedHeader("*"); - config.addAllowedMethod("*"); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); // 모든 경로에 적용 - - return new CorsWebFilter(source); - } - - - -} +//package com.example.gateway.config; +// +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.web.cors.CorsConfiguration; +//import org.springframework.web.cors.reactive.CorsWebFilter; +//import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; +// +//@Configuration +//public class Cors { +// +// @Bean +// public CorsWebFilter corsWebFilter() { +// CorsConfiguration config = new CorsConfiguration(); +// config.setAllowCredentials(true); // 인증 정보 포함 X +// config.addAllowedOrigin("http://localhost:5173"); // 특정 도메인 +// config.addAllowedOrigin("http://58.127.241.84:5173"); +// config.addAllowedOrigin("http://192.168.0.186:5173"); +// config.addAllowedHeader("*"); +// config.addAllowedMethod("*"); +// +// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); +// source.registerCorsConfiguration("/**", config); // 모든 경로에 적용 +// +// return new CorsWebFilter(source); +// } +// +// +// +//} diff --git a/gateway/src/main/resources/application.properties b/gateway/src/main/resources/application.txt similarity index 84% rename from gateway/src/main/resources/application.properties rename to gateway/src/main/resources/application.txt index 2769dfb..8332238 100644 --- a/gateway/src/main/resources/application.properties +++ b/gateway/src/main/resources/application.txt @@ -31,10 +31,10 @@ logging.level.org.springframework.web.server.adapter.HttpWebHandlerAdapter=DEBUG # CORS ?? -spring.cloud.gateway.default-filters[0].name=CORS -spring.cloud.gateway.default-filters[0].args.allowedOrigins=http://localhost:5173,http://192.168.0.186:5173,http://58.127.241.84:5173 -spring.cloud.gateway.default-filters[0].args.allowedMethods=* -spring.cloud.gateway.default-filters[0].args.allowedHeaders=* +#spring.cloud.gateway.default-filters[0].name=CORS +#spring.cloud.gateway.default-filters[0].args.allowedOrigins=http://localhost:5173,http://192.168.0.186:5173,http://58.127.241.84:5173 +#spring.cloud.gateway.default-filters[0].args.allowedMethods=* +#spring.cloud.gateway.default-filters[0].args.allowedHeaders=* spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedOrigins=http://localhost:5173,http://192.168.0.186:5173,http://58.127.241.84:5173 spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedMethods=GET,POST,PUT,DELETE,OPTIONS diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml new file mode 100644 index 0000000..849156a --- /dev/null +++ b/gateway/src/main/resources/application.yml @@ -0,0 +1,53 @@ +spring: + application: + name: gateway + cloud: + gateway: + routes: + - id: congestion-service + uri: http://localhost:8081 + predicates: + - Path=/control/congestion/** + filters: + - RewritePath=/control/congestion/(?.*), /$\{segment} + - id: external-service + uri: http://localhost:8081 + predicates: + - Path=/control/external/** + filters: + - RewritePath=/control/external/(?.*), /$\{segment} + - id: place-service + uri: http://localhost:8082 + predicates: + - Path=/place/** + filters: + - RewritePath=/place/(?.*), /$\{segment} + - id: user-service + uri: http://localhost:8083 + predicates: + - Path=/user/** + filters: + - RewritePath=/user/(?.*), /$\{segment} + globalcors: + corsConfigurations: + '[/**]': + allowedOrigins: + - "http://localhost:5173" + - "http://192.168.0.186:5173" + - "http://58.127.241.84:5173" + allowedMethods: + - GET + - POST + - PUT + - DELETE + - OPTIONS + allowedHeaders: + - "*" + allowCredentials: true + +logging: + level: + root: DEBUG + org.springframework.cloud.gateway: DEBUG + reactor.netty.http.client: DEBUG + org.springframework.web.server.adapter.HttpWebHandlerAdapter: DEBUG From 87464249839a95533f7a1e9df2b1eb5b941ff4ea Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 8 May 2025 17:18:38 +0900 Subject: [PATCH 259/361] =?UTF-8?q?Refactor:=20cors=EC=98=A4=EB=A5=98-gate?= =?UTF-8?q?way=20properties=ED=8C=8C=EC=9D=BC=20yml=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway/src/main/resources/application.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml index 849156a..757652e 100644 --- a/gateway/src/main/resources/application.yml +++ b/gateway/src/main/resources/application.yml @@ -42,7 +42,10 @@ spring: - DELETE - OPTIONS allowedHeaders: - - "*" + - Origin + - Content-Type + - Accept + - Authorization allowCredentials: true logging: From 8d3ef01e9296b810ecc87a040ca57fbacc83ebc8 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 8 May 2025 17:25:10 +0900 Subject: [PATCH 260/361] =?UTF-8?q?Refactor:=20cors=EC=98=A4=EB=A5=98-No?= =?UTF-8?q?=20'Access-Control-Allow-Origin'=20header=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/gateway/config/Cors.java | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/gateway/src/main/java/com/example/gateway/config/Cors.java b/gateway/src/main/java/com/example/gateway/config/Cors.java index e593516..491b2b6 100644 --- a/gateway/src/main/java/com/example/gateway/config/Cors.java +++ b/gateway/src/main/java/com/example/gateway/config/Cors.java @@ -1,30 +1,34 @@ -//package com.example.gateway.config; -// -//import org.springframework.context.annotation.Bean; -//import org.springframework.context.annotation.Configuration; -//import org.springframework.web.cors.CorsConfiguration; -//import org.springframework.web.cors.reactive.CorsWebFilter; -//import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; -// -//@Configuration -//public class Cors { -// -// @Bean -// public CorsWebFilter corsWebFilter() { -// CorsConfiguration config = new CorsConfiguration(); -// config.setAllowCredentials(true); // 인증 정보 포함 X -// config.addAllowedOrigin("http://localhost:5173"); // 특정 도메인 -// config.addAllowedOrigin("http://58.127.241.84:5173"); -// config.addAllowedOrigin("http://192.168.0.186:5173"); -// config.addAllowedHeader("*"); -// config.addAllowedMethod("*"); -// -// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); -// source.registerCorsConfiguration("/**", config); // 모든 경로에 적용 -// -// return new CorsWebFilter(source); -// } -// -// -// -//} +package com.example.gateway.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.CorsWebFilter; +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; + +import java.util.List; + +@Configuration +public class Cors { + + @Bean + public CorsWebFilter corsWebFilter() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); // 인증 정보 포함 X + config.setAllowedOrigins(List.of( + "http://localhost:5173", + "http://192.168.0.186:5173", + "http://58.127.241.84:5173" + )); + config.setAllowedHeaders(List.of("Origin", "Content-Type", "Accept", "Authorization")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); // 모든 경로에 적용 + + return new CorsWebFilter(source); + } + + + +} From f4af0cb0cfac016881c5a8324f6a92ad2be5dfa1 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 9 May 2025 09:07:03 +0900 Subject: [PATCH 261/361] =?UTF-8?q?Refactor:=20cors=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../userservice/config/SecurityConfig.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java index b9ee164..95546b8 100644 --- a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java @@ -24,17 +24,18 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .cors(cors -> cors.configurationSource(request -> { - CorsConfiguration config = new CorsConfiguration(); - config.setAllowCredentials(true); - config.addAllowedOrigin("http://192.168.0.186:5173"); - config.addAllowedOrigin("http://localhost:5173"); - config.addAllowedOrigin("http://58.127.241.84:5173"); - config.addAllowedHeader("*"); - config.addAllowedMethod("*"); - return config; - - })) + // CORS -> api gateway만 처리(중복 방지) +// .cors(cors -> cors.configurationSource(request -> { +// CorsConfiguration config = new CorsConfiguration(); +// config.setAllowCredentials(true); +// config.addAllowedOrigin("http://192.168.0.186:5173"); +// config.addAllowedOrigin("http://localhost:5173"); +// config.addAllowedOrigin("http://58.127.241.84:5173"); +// config.addAllowedHeader("*"); +// config.addAllowedMethod("*"); +// return config; +// +// })) .csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화 .authorizeHttpRequests(auth -> auth // 공개 엔드포인트 - 인증 불필요 From bf08c4f7b03bafacba984d944a09889c78f30629 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 9 May 2025 10:52:43 +0900 Subject: [PATCH 262/361] =?UTF-8?q?Refactor:=20application=20properties=20?= =?UTF-8?q?=EC=BA=90=EC=8B=9C=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.properties | 20 ------------ .../src/main/resources/application.properties | 31 ------------------- 2 files changed, 51 deletions(-) delete mode 100644 external-info-service/src/main/resources/application.properties delete mode 100644 place-service/src/main/resources/application.properties diff --git a/external-info-service/src/main/resources/application.properties b/external-info-service/src/main/resources/application.properties deleted file mode 100644 index 05948dd..0000000 --- a/external-info-service/src/main/resources/application.properties +++ /dev/null @@ -1,20 +0,0 @@ -spring.application.name=external-info-service - -server.port=8081 - -# postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db -spring.datasource.username=root -spring.datasource.password=admin -spring.datasource.driver-class-name=org.postgresql.Driver - -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update -spring.elasticsearch.uris= http://elasticsearch.seoultravel.life:9200 - -spring.mvc.contentnegotiation.favor-path-extension=false -spring.mvc.contentnegotiation.media-types.json=application/json -spring.mvc.default-response-type=application/json - - -seoul.api.key=505a6255746b696d34344b46566174 diff --git a/place-service/src/main/resources/application.properties b/place-service/src/main/resources/application.properties deleted file mode 100644 index 09704cb..0000000 --- a/place-service/src/main/resources/application.properties +++ /dev/null @@ -1,31 +0,0 @@ -spring.application.name=place-service -server.port=8082 - -spring.config.import=optional:file:.env[.properties] -# postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db -spring.datasource.username=root -spring.datasource.password=admin -spring.datasource.driver-class-name=org.postgresql.Driver - -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.defer-datasource-initialization=true - -#spring.jpa.hibernate.ddl-auto=update -spring.jpa.hibernate.ddl-auto=update - -# spring.jpa.hibernate.ddl-auto=update -# #spring.jpa.hibernate.ddl-auto=create - -spring.jpa.show-sql=true - -#spring.sql.init.mode=always - -#API -kakao.api.key=${KAKAO_API_KEY} - -# ?? ?? -logging.level.com.example.placeservice=INFO - -#fastApi URL -fastapi-svc=http://localhost:8000 From ec3cbcec25694a026dc045fe4f77beeb3700c363 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 9 May 2025 13:24:20 +0900 Subject: [PATCH 263/361] =?UTF-8?q?Refactor:=20congestion=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EB=A1=9C=EA=B7=B8=20=EC=B6=95=EC=86=8C=20&=20user?= =?UTF-8?q?=20=EC=8A=A4=ED=94=84=EB=A7=81=20=EC=8B=9C=ED=81=90=EB=A6=AC?= =?UTF-8?q?=ED=8B=B0=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=EB=A9=94=EC=84=B8=EC=A7=80=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 --- .../scheduler/PushScheduler.java | 3 +- .../config/CustomAccessDeniedHandler.java | 22 +++++++++++ .../CustomAuthenticationEntryPoint.java | 38 +++++++++++++++++++ .../example/userservice/config/JpaConfig.java | 11 ------ .../userservice/config/SecurityConfig.java | 7 +++- .../controller/SignupController.java | 1 + .../userservice/service/AuthService.java | 9 ++++- 7 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 user-service/src/main/java/com/example/userservice/config/CustomAccessDeniedHandler.java create mode 100644 user-service/src/main/java/com/example/userservice/config/CustomAuthenticationEntryPoint.java delete mode 100644 user-service/src/main/java/com/example/userservice/config/JpaConfig.java diff --git a/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java b/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java index 2c59ba7..ef543a7 100644 --- a/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java +++ b/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java @@ -39,7 +39,8 @@ public void push() { currentLevels.put(areaName, currentLevel); String previousLevel = previousLevels.get(areaName); - System.out.println(areaName+"의"+previousLevel+":ㅣㅣ"+currentLevel); + // 혼잡도 변화 알림 로그 확인 +// System.out.println(areaName+"의"+previousLevel+":ㅣㅣ"+currentLevel); // 혼잡도 알림 전송(조건 : 1,2단계 -> 3,4 단계 or 3단계 -> 4단계) if (previousLevel == null && (currentLevel.equals("약간 붐빔") || currentLevel.equals("붐빔"))) { diff --git a/user-service/src/main/java/com/example/userservice/config/CustomAccessDeniedHandler.java b/user-service/src/main/java/com/example/userservice/config/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..9040c52 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/config/CustomAccessDeniedHandler.java @@ -0,0 +1,22 @@ +package com.example.userservice.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + @Override + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"error\": \"접근이 거부되었습니다: 권한이 없습니다.\"}"); + } + +} diff --git a/user-service/src/main/java/com/example/userservice/config/CustomAuthenticationEntryPoint.java b/user-service/src/main/java/com/example/userservice/config/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..b5c4462 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/config/CustomAuthenticationEntryPoint.java @@ -0,0 +1,38 @@ +package com.example.userservice.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.LinkedHashMap; +import java.util.Map; + +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) + throws IOException { + sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "사용자가 없습니다.", request.getRequestURI()); + } + + private void sendErrorResponse(HttpServletResponse response, HttpStatus status, String message, String path) + throws IOException { + response.setStatus(status.value()); + response.setContentType("application/json;charset=UTF-8"); + + Map body = new LinkedHashMap<>(); + body.put("timestamp", LocalDateTime.now().toString()); + body.put("status", status.value()); + body.put("error", status.getReasonPhrase()); + body.put("message", message); + body.put("path", path); + + ObjectMapper objectMapper = new ObjectMapper(); + response.getWriter().write(objectMapper.writeValueAsString(body)); + } +} diff --git a/user-service/src/main/java/com/example/userservice/config/JpaConfig.java b/user-service/src/main/java/com/example/userservice/config/JpaConfig.java deleted file mode 100644 index 4ceb53d..0000000 --- a/user-service/src/main/java/com/example/userservice/config/JpaConfig.java +++ /dev/null @@ -1,11 +0,0 @@ -//package com.example.userservice.config; -// -//import org.springframework.context.annotation.Configuration; -//import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -// -//@Configuration -//@EnableJpaRepositories( -// basePackages = "com.example.userservice.repository.jpa" -//) -//public class JpaConfig { -//} diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java index 95546b8..05cb8db 100644 --- a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java @@ -8,6 +8,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; @@ -55,7 +56,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authenticationProvider(authenticationProvider) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(new CustomAuthenticationEntryPoint()) + .accessDeniedHandler(new CustomAccessDeniedHandler()) + ); return http.build(); } diff --git a/user-service/src/main/java/com/example/userservice/controller/SignupController.java b/user-service/src/main/java/com/example/userservice/controller/SignupController.java index c31f90b..a4ecf83 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SignupController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SignupController.java @@ -93,6 +93,7 @@ public ResponseEntity signout( try { memberService.deleteMember(userId); + System.out.println("회원탈퇴 완료: memberId=" + userId); return ResponseEntity.ok("회원 탈퇴가 완료되었습니다."); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body("회원 탈퇴 실패: " + e.getMessage()); diff --git a/user-service/src/main/java/com/example/userservice/service/AuthService.java b/user-service/src/main/java/com/example/userservice/service/AuthService.java index 347ef7d..bc2b483 100644 --- a/user-service/src/main/java/com/example/userservice/service/AuthService.java +++ b/user-service/src/main/java/com/example/userservice/service/AuthService.java @@ -4,13 +4,18 @@ import com.example.userservice.entity.Member; import com.example.userservice.repository.jpa.MemberRepository; import com.example.userservice.security.JwtUtil; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; import java.util.Collections; import java.util.List; @@ -50,7 +55,7 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) { ) ); System.out.println("인증 성공"); - } catch (Exception e) { + }catch (Exception e) { System.out.println("인증 실패: " + e.getMessage()); throw e; } @@ -89,6 +94,8 @@ public AuthDto.LogoutResponse logout(AuthDto.LogoutRequest request) { Long memberId = Long.parseLong(request.getMemberId()); tokenService.deleteRefreshToken(memberId); + System.out.println("로그아웃 완료" + memberId); + // 2. 성공 리턴 return AuthDto.LogoutResponse.builder() .success(true) From b70e85abcde6beb0ef755eed4eb7ee8a2aab4f7b Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 9 May 2025 14:46:41 +0900 Subject: [PATCH 264/361] =?UTF-8?q?refresh=20token=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/MemberController.java | 18 +++++++++++++++++- .../security/JwtAuthenticationFilter.java | 7 ++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/controller/MemberController.java b/user-service/src/main/java/com/example/userservice/controller/MemberController.java index d51b3e1..263cb04 100644 --- a/user-service/src/main/java/com/example/userservice/controller/MemberController.java +++ b/user-service/src/main/java/com/example/userservice/controller/MemberController.java @@ -57,15 +57,31 @@ public ResponseEntity updateProfile( * @return 추출된 사용자 ID */ private String extractUserIdFromRequest(HttpServletRequest request) { + String authHeader = request.getHeader("Authorization"); + String refreshTokenHeader = request.getHeader("refreshToken"); // 추가: refreshToken 헤더 확인 String jwt = null; + // Authorization 헤더가 있으면 해당 토큰 사용 if (authHeader != null && authHeader.startsWith("Bearer ")) { jwt = authHeader.substring(7); + System.out.println("Authorization 헤더에서 토큰 추출: " + jwt); + } + // refreshToken 헤더가 있으면 해당 토큰 사용 (추가) + else if (refreshTokenHeader != null) { + jwt = refreshTokenHeader; + System.out.println("refreshToken 헤더에서 토큰 추출: " + jwt); } if (jwt != null) { - return jwtUtil.extractUsername(jwt); + try { + String userId = jwtUtil.extractUsername(jwt); + System.out.println("토큰에서 추출한 userId: " + userId); + return userId; + } catch (Exception e) { + System.out.println("토큰 처리 중 오류 발생: " + e.getMessage()); + e.printStackTrace(); + } } throw new RuntimeException("인증 토큰이 유효하지 않습니다."); diff --git a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java index 7f40fe9..1dfe077 100644 --- a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java +++ b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java @@ -39,7 +39,6 @@ protected void doFilterInternal( HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { - // 인증이 필요없는 경로는 필터를 적용하지 않음 if (shouldNotFilter(request)) { filterChain.doFilter(request, response); @@ -48,12 +47,17 @@ protected void doFilterInternal( // Authorization 헤더에서 토큰 추출 시도 final String authHeader = request.getHeader("Authorization"); + final String refreshTokenHeader = request.getHeader("refreshToken"); // 추가: refreshToken 헤더 확인 String jwt = null; // Authorization 헤더가 있으면 해당 토큰 사용 if (authHeader != null && authHeader.startsWith("Bearer ")) { jwt = authHeader.substring(7); // "Bearer " 이후의 문자열 추출 } + // refreshToken 헤더가 있으면 해당 토큰 사용 (추가) + else if (refreshTokenHeader != null) { + jwt = refreshTokenHeader; + } // 아니면 쿠키에서 토큰 찾기 else { Cookie[] cookies = request.getCookies(); @@ -67,6 +71,7 @@ protected void doFilterInternal( } } + // 토큰이 없으면 다음 필터로 이동 if (jwt == null) { filterChain.doFilter(request, response); From 5eea657ced9e8f22f626b1df6f1ede52136238aa Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 9 May 2025 15:10:35 +0900 Subject: [PATCH 265/361] =?UTF-8?q?accesstoken=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.properties | 20 +++++++++++++++++++ .../controller/MemberController.java | 17 ++++------------ .../security/JwtAuthenticationFilter.java | 8 ++++---- 3 files changed, 28 insertions(+), 17 deletions(-) create mode 100644 external-info-service/src/main/resources/application.properties diff --git a/external-info-service/src/main/resources/application.properties b/external-info-service/src/main/resources/application.properties new file mode 100644 index 0000000..05948dd --- /dev/null +++ b/external-info-service/src/main/resources/application.properties @@ -0,0 +1,20 @@ +spring.application.name=external-info-service + +server.port=8081 + +# postgreSQL ?? +spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db +spring.datasource.username=root +spring.datasource.password=admin +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update +spring.elasticsearch.uris= http://elasticsearch.seoultravel.life:9200 + +spring.mvc.contentnegotiation.favor-path-extension=false +spring.mvc.contentnegotiation.media-types.json=application/json +spring.mvc.default-response-type=application/json + + +seoul.api.key=505a6255746b696d34344b46566174 diff --git a/user-service/src/main/java/com/example/userservice/controller/MemberController.java b/user-service/src/main/java/com/example/userservice/controller/MemberController.java index 263cb04..488668a 100644 --- a/user-service/src/main/java/com/example/userservice/controller/MemberController.java +++ b/user-service/src/main/java/com/example/userservice/controller/MemberController.java @@ -59,29 +59,20 @@ public ResponseEntity updateProfile( private String extractUserIdFromRequest(HttpServletRequest request) { String authHeader = request.getHeader("Authorization"); - String refreshTokenHeader = request.getHeader("refreshToken"); // 추가: refreshToken 헤더 확인 + String acessTokenHeader = request.getHeader("accessToken"); // 추가: refreshToken 헤더 확인 String jwt = null; // Authorization 헤더가 있으면 해당 토큰 사용 if (authHeader != null && authHeader.startsWith("Bearer ")) { jwt = authHeader.substring(7); - System.out.println("Authorization 헤더에서 토큰 추출: " + jwt); } // refreshToken 헤더가 있으면 해당 토큰 사용 (추가) - else if (refreshTokenHeader != null) { - jwt = refreshTokenHeader; - System.out.println("refreshToken 헤더에서 토큰 추출: " + jwt); + else if (acessTokenHeader != null) { + jwt = acessTokenHeader; } if (jwt != null) { - try { - String userId = jwtUtil.extractUsername(jwt); - System.out.println("토큰에서 추출한 userId: " + userId); - return userId; - } catch (Exception e) { - System.out.println("토큰 처리 중 오류 발생: " + e.getMessage()); - e.printStackTrace(); - } + return jwtUtil.extractUsername(jwt); } throw new RuntimeException("인증 토큰이 유효하지 않습니다."); diff --git a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java index 1dfe077..bf33cc5 100644 --- a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java +++ b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java @@ -47,16 +47,16 @@ protected void doFilterInternal( // Authorization 헤더에서 토큰 추출 시도 final String authHeader = request.getHeader("Authorization"); - final String refreshTokenHeader = request.getHeader("refreshToken"); // 추가: refreshToken 헤더 확인 + final String accessTokenHeader = request.getHeader("accessToken"); // refreshToken 대신 accessToken 사용 String jwt = null; // Authorization 헤더가 있으면 해당 토큰 사용 if (authHeader != null && authHeader.startsWith("Bearer ")) { jwt = authHeader.substring(7); // "Bearer " 이후의 문자열 추출 } - // refreshToken 헤더가 있으면 해당 토큰 사용 (추가) - else if (refreshTokenHeader != null) { - jwt = refreshTokenHeader; + // accessToken 헤더가 있으면 해당 토큰 사용 + else if (accessTokenHeader != null) { + jwt = accessTokenHeader; } // 아니면 쿠키에서 토큰 찾기 else { From 5cca378e2872469101c126fe97a6348f1a91d000 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 9 May 2025 15:16:52 +0900 Subject: [PATCH 266/361] =?UTF-8?q?Refactor:=20gateway=20content-type=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway/src/main/java/com/example/gateway/config/Cors.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gateway/src/main/java/com/example/gateway/config/Cors.java b/gateway/src/main/java/com/example/gateway/config/Cors.java index 491b2b6..a1e9470 100644 --- a/gateway/src/main/java/com/example/gateway/config/Cors.java +++ b/gateway/src/main/java/com/example/gateway/config/Cors.java @@ -23,6 +23,9 @@ public CorsWebFilter corsWebFilter() { config.setAllowedHeaders(List.of("Origin", "Content-Type", "Accept", "Authorization")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + // expose Content-Type so it can be sent with the response + config.setExposedHeaders(List.of("Content-Type", "Cache-Control")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); // 모든 경로에 적용 From 3c11217caa808b05daca386491f9f5737dd240d6 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Sat, 10 May 2025 10:40:55 +0900 Subject: [PATCH 267/361] =?UTF-8?q?user=5Fid=EA=B0=80=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/MemberController.java | 28 +++- .../example/userservice/dto/MemberDto.java | 1 + .../userservice/entity/AccessToken.java | 26 ++++ .../userservice/entity/RefreshToken.java | 19 ++- .../userservice/jwt/JwtTokenProvider.java | 2 +- .../security/JwtAuthenticationFilter.java | 107 ++++++++------ .../example/userservice/security/JwtUtil.java | 30 +++- .../userservice/service/AuthService.java | 139 +++++++++++++++++- .../userservice/service/MemberService.java | 1 + .../userservice/service/TokenService.java | 97 ++++++++++-- 10 files changed, 373 insertions(+), 77 deletions(-) create mode 100644 user-service/src/main/java/com/example/userservice/entity/AccessToken.java diff --git a/user-service/src/main/java/com/example/userservice/controller/MemberController.java b/user-service/src/main/java/com/example/userservice/controller/MemberController.java index 488668a..faf39c4 100644 --- a/user-service/src/main/java/com/example/userservice/controller/MemberController.java +++ b/user-service/src/main/java/com/example/userservice/controller/MemberController.java @@ -57,24 +57,38 @@ public ResponseEntity updateProfile( * @return 추출된 사용자 ID */ private String extractUserIdFromRequest(HttpServletRequest request) { + // 요청 디버깅 로그 + System.out.println("요청 URI: " + request.getRequestURI()); + System.out.println("Authorization 헤더: " + request.getHeader("Authorization")); + System.out.println("accessToken 헤더: " + request.getHeader("accessToken")); String authHeader = request.getHeader("Authorization"); - String acessTokenHeader = request.getHeader("accessToken"); // 추가: refreshToken 헤더 확인 + String accessTokenHeader = request.getHeader("accessToken"); String jwt = null; - // Authorization 헤더가 있으면 해당 토큰 사용 + // Authorization 헤더 확인 if (authHeader != null && authHeader.startsWith("Bearer ")) { jwt = authHeader.substring(7); + System.out.println("Authorization 헤더에서 토큰 추출: " + jwt); } - // refreshToken 헤더가 있으면 해당 토큰 사용 (추가) - else if (acessTokenHeader != null) { - jwt = acessTokenHeader; + // accessToken 헤더 확인 + else if (accessTokenHeader != null) { + jwt = accessTokenHeader; + System.out.println("accessToken 헤더에서 토큰 추출: " + jwt); } + // 토큰 유효성 검사 및 사용자 ID 추출 if (jwt != null) { - return jwtUtil.extractUsername(jwt); + try { + String userId = jwtUtil.extractUsername(jwt); + System.out.println("추출된 사용자 ID: " + userId); + return userId; + } catch (Exception e) { + System.out.println("토큰 처리 중 오류 발생: " + e.getMessage()); + e.printStackTrace(); + } } - throw new RuntimeException("인증 토큰이 유효하지 않습니다."); + throw new RuntimeException("인증 토큰이 유효하지 않습니다. 'Authorization: Bearer [token]' 또는 'accessToken' 헤더가 필요합니다."); } } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java index 8b6e619..41fe595 100644 --- a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java +++ b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java @@ -30,6 +30,7 @@ public static class MemberResponse { @AllArgsConstructor @NoArgsConstructor public static class ProfileResponse { + private String user_id; private String nickname; // 닉네임 private Short birth_year; // 출생연도 private String mbti; // MBTI 유형 diff --git a/user-service/src/main/java/com/example/userservice/entity/AccessToken.java b/user-service/src/main/java/com/example/userservice/entity/AccessToken.java new file mode 100644 index 0000000..3b63010 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/entity/AccessToken.java @@ -0,0 +1,26 @@ +package com.example.userservice.entity; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; + +@Getter +@NoArgsConstructor +@RedisHash(value = "accessToken", timeToLive = 2700) // 45분(2700초) 후 자동 삭제 +public class AccessToken { + + @Id + private String id; // 사용자 ID를 저장 + + @Indexed // 인덱싱하여 검색 가능하도록 + private String accessToken; + + @Builder + public AccessToken(String id, String accessToken) { + this.id = id; + this.accessToken = accessToken; + } +} \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java index fe4a75d..9ae1712 100644 --- a/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java +++ b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java @@ -1,19 +1,26 @@ package com.example.userservice.entity; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.data.annotation.Id; import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; @Getter -@Builder @NoArgsConstructor -@AllArgsConstructor -@RedisHash(value = "refresh_token", timeToLive = 60 * 60 * 24 * 7) // 7일 저장 +@RedisHash(value = "refreshToken", timeToLive = 2700) // 45분(2700초) 후 자동 삭제 public class RefreshToken { + @Id - private String id; // memberId를 String으로 저장 (Redis 키) - private String refreshToken; // 리프레시 토큰 값 + private String id; // 사용자 ID를 저장 + + @Indexed // 인덱싱하여 검색 가능하도록 + private String refreshToken; + + @Builder + public RefreshToken(String id, String refreshToken) { + this.id = id; + this.refreshToken = refreshToken; + } } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java b/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java index 1809e59..ae40772 100644 --- a/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java +++ b/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java @@ -30,7 +30,7 @@ public void init() { public String generateAccessToken(String userId) { // nickname 대신 userId 사용 Date now = new Date(); - Date expiryDate = new Date(now.getTime() + 1000 * 60 * 30); // 30분 + Date expiryDate = new Date(now.getTime() + 1000 * 60 * 45 ); // 30분 return Jwts.builder() .setSubject(userId) // userId로 변경 diff --git a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java index bf33cc5..5920699 100644 --- a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java +++ b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java @@ -1,5 +1,8 @@ package com.example.userservice.security; +import com.example.userservice.entity.Member; +import com.example.userservice.repository.jpa.MemberRepository; +import com.example.userservice.service.TokenService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; @@ -10,6 +13,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; @@ -24,6 +28,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserDetailsService userDetailsService; + private final MemberRepository memberRepository; + private final TokenService tokenService; // 필터링하지 않을 URL 패턴 목록 private final List excludedPaths = Arrays.asList( @@ -45,35 +51,15 @@ protected void doFilterInternal( return; } - // Authorization 헤더에서 토큰 추출 시도 - final String authHeader = request.getHeader("Authorization"); - final String accessTokenHeader = request.getHeader("accessToken"); // refreshToken 대신 accessToken 사용 - String jwt = null; - - // Authorization 헤더가 있으면 해당 토큰 사용 - if (authHeader != null && authHeader.startsWith("Bearer ")) { - jwt = authHeader.substring(7); // "Bearer " 이후의 문자열 추출 - } - // accessToken 헤더가 있으면 해당 토큰 사용 - else if (accessTokenHeader != null) { - jwt = accessTokenHeader; - } - // 아니면 쿠키에서 토큰 찾기 - else { - Cookie[] cookies = request.getCookies(); - if (cookies != null) { - for (Cookie cookie : cookies) { - if ("access_token".equals(cookie.getName())) { - jwt = cookie.getValue(); - break; - } - } - } - } + // 디버깅 로그 추가 + logger.info("요청 URI: " + request.getRequestURI()); + // 토큰 추출 + String jwt = extractTokenFromRequest(request); // 토큰이 없으면 다음 필터로 이동 if (jwt == null) { + logger.info("토큰이 없음, 인증 스킵"); filterChain.doFilter(request, response); return; } @@ -81,6 +67,10 @@ else if (accessTokenHeader != null) { try { // 토큰에서 사용자 이름(userId) 추출 final String username = jwtUtil.extractUsername(jwt); + logger.info("토큰에서 사용자 ID 추출: " + username); + + // 토큰에서 토큰 ID 추출 + final String tokenId = jwtUtil.extractTokenId(jwt); // 보안 컨텍스트에 인증 정보가 없으면 새로 설정 if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { @@ -88,30 +78,65 @@ else if (accessTokenHeader != null) { // 토큰이 유효한지 확인 if (jwtUtil.validateToken(jwt, userDetails)) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() // 사용자 권한 포함 - ); - authToken.setDetails( - new WebAuthenticationDetailsSource().buildDetails(request) - ); - SecurityContextHolder.getContext().setAuthentication(authToken); - - logger.info("사용자 인증 성공: " + username + " (권한: " + userDetails.getAuthorities() + ")"); + // 사용자 ID 가져오기 (MemberRepository 사용) + Member member = memberRepository.findByUserId(username) + .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다")); + Long userId = member.getMemberId(); + + // 토큰이 최신 버전인지 확인 + if (tokenService.isLatestToken(userId, tokenId)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request) + ); + SecurityContextHolder.getContext().setAuthentication(authToken); + + logger.info("사용자 인증 성공: " + username + " (권한: " + userDetails.getAuthorities() + ")"); + } else { + logger.warn("토큰 검증 실패: 이전 버전의 토큰"); + } + } else { + logger.warn("토큰 검증 실패: 유효하지 않은 토큰"); } } } catch (Exception e) { - // 토큰 검증 과정에서 오류가 발생하면 로그만 남기고 계속 진행 logger.error("JWT 토큰 검증 실패: " + e.getMessage()); } filterChain.doFilter(request, response); } - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - String path = request.getServletPath(); - return excludedPaths.stream().anyMatch(path::startsWith); + // 요청에서 토큰 추출 헬퍼 메서드 + private String extractTokenFromRequest(HttpServletRequest request) { + // Authorization 헤더에서 토큰 추출 + final String authHeader = request.getHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + logger.info("Authorization 헤더에서 토큰 추출됨"); + return authHeader.substring(7); + } + + // accessToken 헤더에서 토큰 추출 + final String accessTokenHeader = request.getHeader("accessToken"); + if (accessTokenHeader != null) { + logger.info("accessToken 헤더에서 토큰 추출됨"); + return accessTokenHeader; + } + + // 쿠키에서 토큰 추출 + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if ("access_token".equals(cookie.getName())) { + logger.info("쿠키에서 토큰 추출됨"); + return cookie.getValue(); + } + } + } + + return null; } } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/security/JwtUtil.java b/user-service/src/main/java/com/example/userservice/security/JwtUtil.java index c582096..aa67e07 100644 --- a/user-service/src/main/java/com/example/userservice/security/JwtUtil.java +++ b/user-service/src/main/java/com/example/userservice/security/JwtUtil.java @@ -35,8 +35,11 @@ public void init() { } this.secretKey = Keys.hmacShaKeyFor(jwtSecret.getBytes()); - this.accessTokenExpiration = Long.parseLong(dotenv.get("JWT_EXPIRATION", "1800000")); // 기본값 30분 + // 액세스 토큰 유효 시간을 45분(2,700,000 밀리초)으로 설정 + this.accessTokenExpiration = 2700000L; // 45분 this.refreshTokenExpiration = Long.parseLong(dotenv.get("JWT_REFRESH_EXPIRATION", "604800000")); // 기본값 7일 + + System.out.println("JWT 설정 완료: 액세스 토큰 만료 시간 = " + (accessTokenExpiration / 60000) + "분"); } // 사용자 이름 추출 @@ -49,6 +52,11 @@ public Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } + // 토큰 ID 추출 (토큰 버전 관리용) + public String extractTokenId(String token) { + return extractClaim(token, claims -> claims.get("tokenId", String.class)); + } + // 클레임 추출 public T extractClaim(String token, Function claimsResolver) { final Claims claims = extractAllClaims(token); @@ -73,32 +81,44 @@ private Boolean isTokenExpired(String token) { // 액세스 토큰 생성 public String generateToken(UserDetails userDetails) { Map claims = new HashMap<>(); - return createToken(claims, userDetails.getUsername(), accessTokenExpiration); + String token = createToken(claims, userDetails.getUsername(), accessTokenExpiration); + System.out.println("액세스 토큰 발급: 사용자 = " + userDetails.getUsername() + ", 만료 시간 = " + new Date(System.currentTimeMillis() + accessTokenExpiration)); + return token; } // 액세스 토큰 생성 (nickname 직접 전달) public String generateAccessToken(String nickname) { Map claims = new HashMap<>(); - return createToken(claims, nickname, accessTokenExpiration); + String token = createToken(claims, nickname, accessTokenExpiration); + System.out.println("액세스 토큰 발급: 사용자 = " + nickname + ", 만료 시간 = " + new Date(System.currentTimeMillis() + accessTokenExpiration)); + return token; } // 리프레시 토큰 생성 public String generateRefreshToken(UserDetails userDetails) { Map claims = new HashMap<>(); - return createToken(claims, userDetails.getUsername(), refreshTokenExpiration); + String token = createToken(claims, userDetails.getUsername(), refreshTokenExpiration); + System.out.println("리프레시 토큰 발급: 사용자 = " + userDetails.getUsername() + ", 만료 시간 = " + new Date(System.currentTimeMillis() + refreshTokenExpiration)); + return token; } // 리프레시 토큰 생성 (nickname 직접 전달) public String generateRefreshToken(String nickname) { Map claims = new HashMap<>(); - return createToken(claims, nickname, refreshTokenExpiration); + String token = createToken(claims, nickname, refreshTokenExpiration); + System.out.println("리프레시 토큰 발급: 사용자 = " + nickname + ", 만료 시간 = " + new Date(System.currentTimeMillis() + refreshTokenExpiration)); + return token; } // 토큰 생성 메소드 private String createToken(Map claims, String subject, Long expirationTime) { + // 토큰 ID 생성 (토큰 버전 관리를 위한 고유 식별자) + String tokenId = String.valueOf(System.currentTimeMillis()); + return Jwts.builder() .claims(claims) .subject(subject) + .claim("tokenId", tokenId) // 토큰 ID 추가 (버전 관리용) .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(System.currentTimeMillis() + expirationTime)) .signWith(secretKey, SignatureAlgorithm.HS512) diff --git a/user-service/src/main/java/com/example/userservice/service/AuthService.java b/user-service/src/main/java/com/example/userservice/service/AuthService.java index bc2b483..cd65108 100644 --- a/user-service/src/main/java/com/example/userservice/service/AuthService.java +++ b/user-service/src/main/java/com/example/userservice/service/AuthService.java @@ -2,10 +2,12 @@ import com.example.userservice.dto.AuthDto; import com.example.userservice.entity.Member; +import com.example.userservice.entity.RefreshToken; import com.example.userservice.repository.jpa.MemberRepository; import com.example.userservice.security.JwtUtil; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; @@ -19,6 +21,8 @@ import java.util.Collections; import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; @Service @RequiredArgsConstructor @@ -28,6 +32,10 @@ public class AuthService { private final JwtUtil jwtUtil; private final AuthenticationManager authenticationManager; private final TokenService tokenService; + private final RedisTemplate redisTemplate; + + private static final String TOKEN_VERSION_PREFIX = "token:version:"; + private static final long TOKEN_VERSION_TTL = 2700; // 45분(초 단위) /** * 로그인 처리 @@ -55,25 +63,45 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) { ) ); System.out.println("인증 성공"); - }catch (Exception e) { + } catch (Exception e) { System.out.println("인증 실패: " + e.getMessage()); throw e; } + // 3. 기존 토큰이 있으면 삭제 + Optional existingToken = tokenService.findRefreshTokenByMemberId(member.getMemberId()); + if (existingToken.isPresent()) { + System.out.println("기존 리프레시 토큰 발견: 사용자 ID = " + member.getMemberId() + ", 삭제 진행"); + tokenService.deleteRefreshToken(member.getMemberId()); + } + + // 기존 토큰 버전이 있으면 삭제 (이전 토큰 무효화) + String versionKey = TOKEN_VERSION_PREFIX + member.getMemberId(); + if (Boolean.TRUE.equals(redisTemplate.hasKey(versionKey))) { + System.out.println("기존 토큰 버전 발견: 사용자 ID = " + member.getMemberId() + ", 삭제 진행"); + redisTemplate.delete(versionKey); + } + // 사용자 권한 설정 List authorities = Collections.singletonList( new SimpleGrantedAuthority(member.getRole()) ); - // 3. JWT 토큰 발급 + // 4. JWT 토큰 발급 (유효 시간 45분) UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities); String accessToken = jwtUtil.generateToken(userDetails); String refreshToken = jwtUtil.generateRefreshToken(userDetails); - // 4. Refresh Token Redis 저장 + // 5. 토큰 저장 및 버전 관리 tokenService.saveRefreshToken(member.getMemberId(), refreshToken); - // 5. 결과 반환 + // 토큰 ID 저장 (버전 관리) + String tokenId = jwtUtil.extractTokenId(accessToken); + saveTokenVersion(member.getMemberId(), tokenId); + + System.out.println("새 토큰 발급 완료: 액세스 토큰 유효 시간 = 45분, 사용자 ID = " + member.getMemberId() + ", 토큰 ID = " + tokenId); + + // 6. 결과 반환 return AuthDto.LoginResponse.builder() .accessToken(accessToken) .refreshToken(refreshToken) @@ -90,16 +118,111 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) { * @return 로그아웃 성공 여부와 메시지 */ public AuthDto.LogoutResponse logout(AuthDto.LogoutRequest request) { - // 1. 사용자의 Redis Refresh Token 삭제 + // 사용자 ID 파싱 Long memberId = Long.parseLong(request.getMemberId()); + + // 1. 토큰 버전 삭제 (토큰 무효화) + String versionKey = TOKEN_VERSION_PREFIX + memberId; + if (Boolean.TRUE.equals(redisTemplate.hasKey(versionKey))) { + redisTemplate.delete(versionKey); + System.out.println("토큰 버전 삭제 완료: 사용자 ID = " + memberId); + } + + // 2. 리프레시 토큰 삭제 tokenService.deleteRefreshToken(memberId); + System.out.println("리프레시 토큰 삭제 완료: 사용자 ID = " + memberId); - System.out.println("로그아웃 완료" + memberId); + System.out.println("로그아웃 완료: 사용자 ID = " + memberId); - // 2. 성공 리턴 + // 3. 성공 리턴 return AuthDto.LogoutResponse.builder() .success(true) - .message("로그아웃 완료: RefreshToken 삭제 완료") + .message("로그아웃 완료: 모든 토큰 무효화 완료") .build(); } + + /** + * 토큰 재발급 + * @param refreshToken 리프레시 토큰 + * @return 새로운 액세스 토큰 + */ + public String refreshAccessToken(String refreshToken) { + // 1. 리프레시 토큰 유효성 검사 + if (!jwtUtil.validateToken(refreshToken)) { + throw new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다."); + } + + // 2. 데이터베이스(레디스)에서 리프레시 토큰 확인 + Optional storedToken = tokenService.findRefreshTokenByRefreshToken(refreshToken); + if (storedToken.isEmpty()) { + throw new IllegalArgumentException("존재하지 않는 리프레시 토큰입니다."); + } + + // 3. 사용자 ID로 사용자 조회 + Long userId = Long.valueOf(storedToken.get().getId()); // id는 String으로 저장되니까 Long으로 변환 + Member member = memberRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + + // 4. 현재 버전 삭제 (이전 토큰 무효화) + String versionKey = TOKEN_VERSION_PREFIX + userId; + if (Boolean.TRUE.equals(redisTemplate.hasKey(versionKey))) { + redisTemplate.delete(versionKey); + } + + // 5. 새 액세스 토큰 생성 + List authorities = Collections.singletonList( + new SimpleGrantedAuthority(member.getRole()) + ); + + UserDetails userDetails = new User( + member.getUserId(), + member.getPassword(), + authorities + ); + + String newAccessToken = jwtUtil.generateToken(userDetails); + + // 6. 새 토큰 버전 저장 + String tokenId = jwtUtil.extractTokenId(newAccessToken); + saveTokenVersion(userId, tokenId); + + System.out.println("액세스 토큰 재발급 완료: 유효 시간 = 45분, 사용자 ID = " + userId + ", 토큰 ID = " + tokenId); + + return newAccessToken; + } + + /** + * 토큰 버전 저장 (현재 유효한 토큰 ID) + * @param userId 사용자 ID + * @param tokenId 토큰 ID + */ + private void saveTokenVersion(Long userId, String tokenId) { + String key = TOKEN_VERSION_PREFIX + userId; + redisTemplate.opsForValue().set(key, tokenId, TOKEN_VERSION_TTL, TimeUnit.SECONDS); + System.out.println("토큰 버전 저장: 사용자 ID = " + userId + ", 토큰 ID = " + tokenId); + } + + /** + * 토큰이 최신 버전인지 확인 + * @param userId 사용자 ID + * @param tokenId 토큰 ID + * @return 최신 토큰이면 true, 아니면 false + */ + public boolean isLatestToken(Long userId, String tokenId) { + String key = TOKEN_VERSION_PREFIX + userId; + String latestTokenId = redisTemplate.opsForValue().get(key); + + if (latestTokenId == null) { + return false; // 저장된 토큰 ID가 없으면 유효하지 않음 (로그아웃 상태) + } + + boolean isLatest = latestTokenId.equals(tokenId); + if (!isLatest) { + System.out.println("토큰 버전 불일치: 사용자 ID = " + userId + + ", 요청 토큰 ID = " + tokenId + + ", 최신 토큰 ID = " + latestTokenId); + } + + return isLatest; + } } \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java index 74e3ae6..d52c688 100644 --- a/user-service/src/main/java/com/example/userservice/service/MemberService.java +++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java @@ -70,6 +70,7 @@ public MemberDto.ProfileResponse getProfile(String userId) { .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); return MemberDto.ProfileResponse.builder() + .user_id(member.getUserId()) .nickname(member.getNickname()) .birth_year(member.getBirthYear()) .mbti(member.getMbti()) diff --git a/user-service/src/main/java/com/example/userservice/service/TokenService.java b/user-service/src/main/java/com/example/userservice/service/TokenService.java index 3d6070f..b3642b9 100644 --- a/user-service/src/main/java/com/example/userservice/service/TokenService.java +++ b/user-service/src/main/java/com/example/userservice/service/TokenService.java @@ -6,6 +6,7 @@ import com.example.userservice.repository.redis.RefreshTokenRepository; import com.example.userservice.security.JwtUtil; import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; @@ -13,6 +14,7 @@ import java.util.Collections; import java.util.Optional; +import java.util.concurrent.TimeUnit; @Service @RequiredArgsConstructor @@ -21,11 +23,13 @@ public class TokenService { private final JwtUtil jwtUtil; private final MemberRepository memberRepository; private final RefreshTokenRepository refreshTokenRepository; + private final RedisTemplate redisTemplate; + + private static final String TOKEN_VERSION_PREFIX = "token:version:"; + private static final long TOKEN_VERSION_TTL = 2700; // 45분 /** * 리프레시 토큰을 사용하여 새 액세스 토큰 생성 - * @param refreshToken 리프레시 토큰 - * @return 새 액세스 토큰 */ public String createNewAccessToken(String refreshToken) { // 리프레시 토큰 유효성 검사 @@ -40,10 +44,17 @@ public String createNewAccessToken(String refreshToken) { } // 사용자 ID로 사용자 조회 - Long userId = Long.valueOf(storedToken.get().getId()); // id는 String으로 저장되니까 Long으로 변환 + Long userId = Long.valueOf(storedToken.get().getId()); Member member = memberRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + // 기존 토큰 버전 삭제 (이전 액세스 토큰 무효화) + String key = TOKEN_VERSION_PREFIX + userId; + if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) { + redisTemplate.delete(key); + System.out.println("기존 토큰 버전 삭제: 사용자 ID = " + userId); + } + // 사용자 권한 설정 SimpleGrantedAuthority authority = new SimpleGrantedAuthority(member.getRole()); @@ -54,29 +65,97 @@ public String createNewAccessToken(String refreshToken) { Collections.singletonList(authority) ); - return jwtUtil.generateToken(userDetails); + String accessToken = jwtUtil.generateToken(userDetails); + + // 토큰 버전 업데이트 + String tokenId = jwtUtil.extractTokenId(accessToken); + saveTokenVersion(userId, tokenId); + + System.out.println("새 액세스 토큰 생성 완료: 사용자 ID = " + userId + ", 토큰 ID = " + tokenId); + + return accessToken; + } + + /** + * 토큰 버전 저장 (현재 유효한 토큰 ID) + */ + public void saveTokenVersion(Long userId, String tokenId) { + String key = TOKEN_VERSION_PREFIX + userId; + redisTemplate.opsForValue().set(key, tokenId, TOKEN_VERSION_TTL, TimeUnit.SECONDS); + System.out.println("토큰 버전 업데이트: 사용자 ID = " + userId + ", 토큰 ID = " + tokenId); + } + + /** + * 토큰이 최신 버전인지 확인 + */ + public boolean isLatestToken(Long userId, String tokenId) { + String key = TOKEN_VERSION_PREFIX + userId; + String latestTokenId = redisTemplate.opsForValue().get(key); + + if (latestTokenId == null) { + System.out.println("저장된 토큰 버전 없음: 사용자 ID = " + userId); + return false; // 저장된 토큰이 없으면 로그아웃된 상태로 간주하고 인증 실패 + } + + boolean isLatest = latestTokenId.equals(tokenId); + if (!isLatest) { + System.out.println("토큰 버전 불일치: 사용자 ID = " + userId + + ", 요청 토큰 ID = " + tokenId + + ", 최신 토큰 ID = " + latestTokenId); + } + + return isLatest; + } + + /** + * 토큰 무효화 (토큰 버전 삭제) + */ + public void invalidateToken(Long userId) { + String key = TOKEN_VERSION_PREFIX + userId; + redisTemplate.delete(key); + System.out.println("토큰 무효화 완료: 사용자 ID = " + userId); } /** * 리프레시 토큰 저장 - * @param memberId 회원 ID - * @param refreshToken 리프레시 토큰 */ public void saveRefreshToken(Long memberId, String refreshToken) { + // 기존 토큰이 있는지 확인하고 있으면 삭제 + Optional existingToken = refreshTokenRepository.findById(String.valueOf(memberId)); + if (existingToken.isPresent()) { + refreshTokenRepository.deleteById(String.valueOf(memberId)); + System.out.println("기존 리프레시 토큰 삭제: 사용자 ID = " + memberId); + } + + // 새 토큰 저장 RefreshToken token = RefreshToken.builder() - .id(String.valueOf(memberId)) // memberId를 String으로 변환해서 저장 + .id(String.valueOf(memberId)) .refreshToken(refreshToken) .build(); refreshTokenRepository.save(token); + System.out.println("새 리프레시 토큰 저장: 사용자 ID = " + memberId); } /** * 리프레시 토큰 삭제 - * @param memberId 회원 ID */ public void deleteRefreshToken(Long memberId) { - // memberId를 String으로 변환하여 deleteById 호출 refreshTokenRepository.deleteById(String.valueOf(memberId)); + System.out.println("리프레시 토큰 삭제 완료: 사용자 ID = " + memberId); + } + + /** + * 사용자 ID로 리프레시 토큰 조회 + */ + public Optional findRefreshTokenByMemberId(Long memberId) { + return refreshTokenRepository.findById(String.valueOf(memberId)); + } + + /** + * 리프레시 토큰 값으로 리프레시 토큰 엔티티 조회 + */ + public Optional findRefreshTokenByRefreshToken(String refreshToken) { + return refreshTokenRepository.findByRefreshToken(refreshToken); } } \ No newline at end of file From 6c6fd0cf1054374a732336e9e7ecc18273a41d5d Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Sat, 10 May 2025 10:52:59 +0900 Subject: [PATCH 268/361] =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/vcs.xml | 1 - external-info-service/src/main/resources/application.properties | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 4801151..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/external-info-service/src/main/resources/application.properties b/external-info-service/src/main/resources/application.properties index 05948dd..b8c9aed 100644 --- a/external-info-service/src/main/resources/application.properties +++ b/external-info-service/src/main/resources/application.properties @@ -17,4 +17,4 @@ spring.mvc.contentnegotiation.media-types.json=application/json spring.mvc.default-response-type=application/json -seoul.api.key=505a6255746b696d34344b46566174 +seoul.api.key=505a6255746b696d34344b46566174 \ No newline at end of file From 9d55ebf8235d51ce2c6d9ade8229d942f303ec54 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Sat, 10 May 2025 11:00:39 +0900 Subject: [PATCH 269/361] =?UTF-8?q?applicaion.properties=20=EC=B6=94?= =?UTF-8?q?=EC=A0=81=20=EC=A4=91=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.properties | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 external-info-service/src/main/resources/application.properties diff --git a/external-info-service/src/main/resources/application.properties b/external-info-service/src/main/resources/application.properties deleted file mode 100644 index b8c9aed..0000000 --- a/external-info-service/src/main/resources/application.properties +++ /dev/null @@ -1,20 +0,0 @@ -spring.application.name=external-info-service - -server.port=8081 - -# postgreSQL ?? -spring.datasource.url=jdbc:postgresql://localhost:5432/stars_db -spring.datasource.username=root -spring.datasource.password=admin -spring.datasource.driver-class-name=org.postgresql.Driver - -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update -spring.elasticsearch.uris= http://elasticsearch.seoultravel.life:9200 - -spring.mvc.contentnegotiation.favor-path-extension=false -spring.mvc.contentnegotiation.media-types.json=application/json -spring.mvc.default-response-type=application/json - - -seoul.api.key=505a6255746b696d34344b46566174 \ No newline at end of file From b4e2ec660aa05ab2a9f9b69ff4ec6774f0aef4fe Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Sat, 10 May 2025 11:09:08 +0900 Subject: [PATCH 270/361] =?UTF-8?q?=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20?= =?UTF-8?q?memberid=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AuthController.java | 20 +++++++++ .../controller/SignupController.java | 41 +++++++------------ .../repository/jpa/MemberRepository.java | 4 +- .../userservice/service/MemberService.java | 13 +++++- .../userservice/service/TokenService.java | 33 ++++++++++----- 5 files changed, 70 insertions(+), 41 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/controller/AuthController.java b/user-service/src/main/java/com/example/userservice/controller/AuthController.java index cc38cbf..bbdde96 100644 --- a/user-service/src/main/java/com/example/userservice/controller/AuthController.java +++ b/user-service/src/main/java/com/example/userservice/controller/AuthController.java @@ -6,6 +6,7 @@ import com.example.userservice.security.JwtUtil; import com.example.userservice.service.AuthService; import com.example.userservice.service.CookieUtil; +import com.example.userservice.service.TokenService; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -16,6 +17,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.HashMap; +import java.util.Map; + @RestController @RequestMapping("/auth") @RequiredArgsConstructor @@ -25,6 +29,7 @@ public class AuthController { private final CookieUtil cookieUtil; private final JwtUtil jwtUtil; private final MemberRepository memberRepository; + private final TokenService tokenService; /** * 로그인 API @@ -49,6 +54,21 @@ public ResponseEntity login( return ResponseEntity.ok(loginResponse); } + + /* + * 토큰 재발급 api 추가 + * */ + + @PostMapping("/refresh") + public ResponseEntity> refreshAccessToken(@RequestBody AuthDto.RefreshTokenRequest request) { + String newAccessToken = tokenService.createNewAccessToken(request.getRefreshToken()); + + Map response = new HashMap<>(); + response.put("accessToken", newAccessToken); + + return ResponseEntity.ok(response); + } + /** * 로그아웃 API * @param request HTTP 요청 (토큰 추출용) diff --git a/user-service/src/main/java/com/example/userservice/controller/SignupController.java b/user-service/src/main/java/com/example/userservice/controller/SignupController.java index a4ecf83..8a1a13c 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SignupController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SignupController.java @@ -28,40 +28,24 @@ public class SignupController { /** * 일반 사용자 회원가입 API - * @param dto 회원가입 정보 (user_id, password, nickname, birth_year, mbti, gender) - * @return 토큰 및 사용자 정보가 포함된 응답 */ @PostMapping("/signup") public ResponseEntity> signup(@RequestBody MemberSign dto) { try { - // 디버깅 로그 출력 - System.out.println("회원가입 요청: userId=" + dto.getUserId() + ", nickname=" + dto.getNickname() - + ", birth_year=" + dto.getBirthYear() - + ", mbti=" + dto.getMbti() - + ", gender=" + dto.getGender()); + System.out.println("회원가입 요청: userId=" + dto.getUserId()); - // 일반 사용자로 등록 Member member = memberService.registerUser(dto); - System.out.println("회원가입 성공: memberId=" + member.getMemberId() - + ", userId=" + member.getUserId() - + ", nickname=" + member.getNickname() - + ", role=" + member.getRole()); - - // 사용자 권한 설정 List authorities = Collections.singletonList( new SimpleGrantedAuthority(member.getRole()) ); - // 회원가입 성공하면 토큰 발급 UserDetails userDetails = new User(member.getUserId(), member.getPassword(), authorities); String accessToken = jwtUtil.generateToken(userDetails); String refreshToken = jwtUtil.generateRefreshToken(userDetails); - // 리프레시 토큰 저장 tokenService.saveRefreshToken(member.getMemberId(), refreshToken); - // 응답 생성 Map response = new HashMap<>(); response.put("accessToken", accessToken); response.put("refreshToken", refreshToken); @@ -80,24 +64,29 @@ public ResponseEntity> signup(@RequestBody MemberSign dto) { } } - //회원탈퇴 - @DeleteMapping("/signout/{userId}") + /** + * 회원 탈퇴 (본인 인증 포함) + */ + @DeleteMapping("/signout/{memberId}") public ResponseEntity signout( - @PathVariable String userId, + @PathVariable Long memberId, @AuthenticationPrincipal UserDetails userDetails) { - // 본인 확인 - if (!userDetails.getUsername().equals(userId)) { + // 현재 로그인한 사용자 정보 조회 + String userId = userDetails.getUsername(); + Member loginMember = memberService.findByUserId(userId); + + // 본인인지 확인 + if (!loginMember.getMemberId().equals(memberId)) { return ResponseEntity.status(403).body("본인만 탈퇴할 수 있습니다."); } try { - memberService.deleteMember(userId); - System.out.println("회원탈퇴 완료: memberId=" + userId); + memberService.deleteMemberById(memberId); + System.out.println("회원탈퇴 완료: memberId=" + memberId); return ResponseEntity.ok("회원 탈퇴가 완료되었습니다."); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body("회원 탈퇴 실패: " + e.getMessage()); } } - -} \ No newline at end of file +} diff --git a/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java b/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java index 69cb7dc..6d98e36 100644 --- a/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java +++ b/user-service/src/main/java/com/example/userservice/repository/jpa/MemberRepository.java @@ -9,8 +9,8 @@ @Repository public interface MemberRepository extends JpaRepository { boolean existsByMemberId(Long memberId); - boolean existsByUserId(String userId); + boolean existsByUserId(String memberId); boolean existsByNickname(String nickname); Optional findByNickname(String nickname); - Optional findByUserId(String userId); + Optional findByUserId(String memberId); } diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java index 74e3ae6..9c20b4e 100644 --- a/user-service/src/main/java/com/example/userservice/service/MemberService.java +++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java @@ -128,15 +128,24 @@ public MemberDto.MemberResponse updateProfile(String userId, MemberDto.UpdateReq .build(); } + + // 로그인된 userId로 Member 엔티티 조회 + public Member findByUserId(String userId) { + return memberRepository.findByUserId(userId) + .orElseThrow(() -> new IllegalArgumentException("회원을 찾을 수 없습니다.")); + } + + // memberId로 회원 삭제 @Transactional - public void deleteMember(String userId) { - Member member = memberRepository.findByUserId(userId) + public void deleteMemberById(Long memberId) { + Member member = memberRepository.findById(memberId) .orElseThrow(() -> new IllegalArgumentException("회원이 존재하지 않습니다.")); memberRepository.delete(member); } + /** * 모든 회원 목록 조회 * @return 모든 회원 정보 목록 diff --git a/user-service/src/main/java/com/example/userservice/service/TokenService.java b/user-service/src/main/java/com/example/userservice/service/TokenService.java index 3d6070f..f166bd9 100644 --- a/user-service/src/main/java/com/example/userservice/service/TokenService.java +++ b/user-service/src/main/java/com/example/userservice/service/TokenService.java @@ -12,7 +12,6 @@ import org.springframework.stereotype.Service; import java.util.Collections; -import java.util.Optional; @Service @RequiredArgsConstructor @@ -28,26 +27,39 @@ public class TokenService { * @return 새 액세스 토큰 */ public String createNewAccessToken(String refreshToken) { - // 리프레시 토큰 유효성 검사 + // 1. 리프레시 토큰 유효성 검사 if (!jwtUtil.validateToken(refreshToken)) { throw new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다."); } - // 데이터베이스(레디스)에서 리프레시 토큰 확인 - Optional storedToken = refreshTokenRepository.findByRefreshToken(refreshToken); - if (storedToken.isEmpty()) { + // 2. Redis에서 refreshToken과 일치하는 엔트리 찾기 (findByRefreshToken 대신 직접 탐색) + RefreshToken matchedToken = null; + for (RefreshToken token : refreshTokenRepository.findAll()) { + + if (token == null || token.getRefreshToken() == null) continue; + + if (token.getRefreshToken().trim().equals(refreshToken.trim())) { + matchedToken = token; + break; + } + } + + + + + if (matchedToken == null) { throw new IllegalArgumentException("존재하지 않는 리프레시 토큰입니다."); } - // 사용자 ID로 사용자 조회 - Long userId = Long.valueOf(storedToken.get().getId()); // id는 String으로 저장되니까 Long으로 변환 + // 3. 사용자 ID로 사용자 조회 + Long userId = Long.valueOf(matchedToken.getId()); Member member = memberRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); - // 사용자 권한 설정 + // 4. 사용자 권한 설정 SimpleGrantedAuthority authority = new SimpleGrantedAuthority(member.getRole()); - // 새 액세스 토큰 생성 + // 5. 새 액세스 토큰 생성 UserDetails userDetails = new User( member.getUserId(), member.getPassword(), @@ -76,7 +88,6 @@ public void saveRefreshToken(Long memberId, String refreshToken) { * @param memberId 회원 ID */ public void deleteRefreshToken(Long memberId) { - // memberId를 String으로 변환하여 deleteById 호출 refreshTokenRepository.deleteById(String.valueOf(memberId)); } -} \ No newline at end of file +} From fc70a6a1a1ec6ab7a22bc46f302da1ddb33e07df Mon Sep 17 00:00:00 2001 From: ha-yun Date: Sat, 10 May 2025 15:30:29 +0900 Subject: [PATCH 271/361] =?UTF-8?q?Refactor:=20cors=20=EB=B0=B0=ED=8F=AC?= =?UTF-8?q?=EB=90=9C=20=ED=94=84=EB=A1=A0=ED=8A=B8=20url=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 --- gateway/src/main/java/com/example/gateway/config/Cors.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gateway/src/main/java/com/example/gateway/config/Cors.java b/gateway/src/main/java/com/example/gateway/config/Cors.java index a1e9470..def3bbf 100644 --- a/gateway/src/main/java/com/example/gateway/config/Cors.java +++ b/gateway/src/main/java/com/example/gateway/config/Cors.java @@ -18,7 +18,8 @@ public CorsWebFilter corsWebFilter() { config.setAllowedOrigins(List.of( "http://localhost:5173", "http://192.168.0.186:5173", - "http://58.127.241.84:5173" + "http://58.127.241.84:5173", + "http://map.seoultravel.life" )); config.setAllowedHeaders(List.of("Origin", "Content-Type", "Accept", "Authorization")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); From e6d0f2bf4447ce58b896995a3f37b74af2ca565a Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Sat, 10 May 2025 16:49:46 +0900 Subject: [PATCH 272/361] =?UTF-8?q?swagger-ui=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user-service/build.gradle | 2 ++ .../userservice/config/SecurityConfig.java | 27 ++++++------------ .../userservice/config/SwaggerConfig.java | 28 +++++++++++++++++++ 3 files changed, 39 insertions(+), 18 deletions(-) create mode 100644 user-service/src/main/java/com/example/userservice/config/SwaggerConfig.java diff --git a/user-service/build.gradle b/user-service/build.gradle index 0205bbb..795f99d 100644 --- a/user-service/build.gradle +++ b/user-service/build.gradle @@ -50,6 +50,8 @@ dependencies { testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' implementation 'io.github.cdimascio:java-dotenv:5.2.2' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + } tasks.named('bootBuildImage') { diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java index 05cb8db..ce6f93b 100644 --- a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java @@ -8,11 +8,8 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.web.cors.CorsConfiguration; - @Configuration @EnableWebSecurity @@ -25,20 +22,15 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - // CORS -> api gateway만 처리(중복 방지) -// .cors(cors -> cors.configurationSource(request -> { -// CorsConfiguration config = new CorsConfiguration(); -// config.setAllowCredentials(true); -// config.addAllowedOrigin("http://192.168.0.186:5173"); -// config.addAllowedOrigin("http://localhost:5173"); -// config.addAllowedOrigin("http://58.127.241.84:5173"); -// config.addAllowedHeader("*"); -// config.addAllowedMethod("*"); -// return config; -// -// })) - .csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화 + .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth + // Swagger 접근 허용 + .requestMatchers( + "/swagger-ui.html", + "/swagger-ui/**", + "/v3/api-docs/**" + ).permitAll() + // 공개 엔드포인트 - 인증 불필요 .requestMatchers("/auth/**").permitAll() @@ -52,7 +44,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .anyRequest().authenticated() ) .sessionManagement(session -> session - // 세션 상태 없음 (JWT 사용하므로) .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authenticationProvider(authenticationProvider) @@ -64,4 +55,4 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } -} \ No newline at end of file +} diff --git a/user-service/src/main/java/com/example/userservice/config/SwaggerConfig.java b/user-service/src/main/java/com/example/userservice/config/SwaggerConfig.java new file mode 100644 index 0000000..f48c45f --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/config/SwaggerConfig.java @@ -0,0 +1,28 @@ +package com.example.userservice.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@Configuration +@EnableWebMvc +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .components(new Components()) + .info(apiInfo()); + } + + private Info apiInfo() { + return new Info() + .title("API Title") // API의 제목 + .description("This is my Swagger UI") // API에 대한 설명 + .version("1.0.0"); // API의 버전 + } + +} From 62ffdabc51b48a944de3e58e67204a7067a9457e Mon Sep 17 00:00:00 2001 From: ha-yun Date: Sat, 10 May 2025 16:53:32 +0900 Subject: [PATCH 273/361] =?UTF-8?q?Refactor:=20deploy.yml=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/deploy.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 12bc95e..10b939e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -27,6 +27,8 @@ jobs: - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 + with: + mask-password: true - name: 자바 설치 uses: actions/setup-java@v2 @@ -74,6 +76,8 @@ jobs: - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 + with: + mask-password: true - name: 자바 설치 uses: actions/setup-java@v2 @@ -122,6 +126,8 @@ jobs: - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 + with: + mask-password: true - name: 자바 설치 uses: actions/setup-java@v2 @@ -167,6 +173,8 @@ jobs: - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 + with: + mask-password: true - name: 자바 설치 uses: actions/setup-java@v2 @@ -215,6 +223,8 @@ jobs: - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 + with: + mask-password: true - name: 자바 설치 uses: actions/setup-java@v2 From 9d266cec12a561526096367fbc00d3e86da2ee87 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Sat, 10 May 2025 17:07:30 +0900 Subject: [PATCH 274/361] =?UTF-8?q?swagger-ui=20jwt=20=EB=A7=89=ED=9E=98?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/JwtAuthenticationFilter.java | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java index 5920699..3ecaecc 100644 --- a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java +++ b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java @@ -31,12 +31,14 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final MemberRepository memberRepository; private final TokenService tokenService; - // 필터링하지 않을 URL 패턴 목록 - private final List excludedPaths = Arrays.asList( + // 완전 일치 제외 경로 목록 (Swagger 포함) + private final List excludedExactPaths = Arrays.asList( "/auth/signup", "/auth/admin/signup", "/auth/login", - "/auth/logout" + "/auth/logout", + "/swagger-ui.html", + "/v3/api-docs" ); @Override @@ -45,19 +47,16 @@ protected void doFilterInternal( HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { - // 인증이 필요없는 경로는 필터를 적용하지 않음 + if (shouldNotFilter(request)) { filterChain.doFilter(request, response); return; } - // 디버깅 로그 추가 logger.info("요청 URI: " + request.getRequestURI()); - // 토큰 추출 String jwt = extractTokenFromRequest(request); - // 토큰이 없으면 다음 필터로 이동 if (jwt == null) { logger.info("토큰이 없음, 인증 스킵"); filterChain.doFilter(request, response); @@ -65,37 +64,28 @@ protected void doFilterInternal( } try { - // 토큰에서 사용자 이름(userId) 추출 - final String username = jwtUtil.extractUsername(jwt); + String username = jwtUtil.extractUsername(jwt); logger.info("토큰에서 사용자 ID 추출: " + username); - // 토큰에서 토큰 ID 추출 - final String tokenId = jwtUtil.extractTokenId(jwt); + String tokenId = jwtUtil.extractTokenId(jwt); - // 보안 컨텍스트에 인증 정보가 없으면 새로 설정 if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); - // 토큰이 유효한지 확인 if (jwtUtil.validateToken(jwt, userDetails)) { - // 사용자 ID 가져오기 (MemberRepository 사용) Member member = memberRepository.findByUserId(username) .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다")); Long userId = member.getMemberId(); - // 토큰이 최신 버전인지 확인 if (tokenService.isLatestToken(userId, tokenId)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); - authToken.setDetails( - new WebAuthenticationDetailsSource().buildDetails(request) - ); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); - - logger.info("사용자 인증 성공: " + username + " (권한: " + userDetails.getAuthorities() + ")"); + logger.info("사용자 인증 성공: " + username); } else { logger.warn("토큰 검증 실패: 이전 버전의 토큰"); } @@ -110,23 +100,28 @@ protected void doFilterInternal( filterChain.doFilter(request, response); } - // 요청에서 토큰 추출 헬퍼 메서드 + // Swagger 및 인증 제외 경로 처리 + public boolean shouldNotFilter(HttpServletRequest request) { + String path = request.getRequestURI(); + return excludedExactPaths.contains(path) + || path.startsWith("/swagger-ui") + || path.startsWith("/v3/api-docs"); + } + + // 토큰 추출 메서드 private String extractTokenFromRequest(HttpServletRequest request) { - // Authorization 헤더에서 토큰 추출 final String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { logger.info("Authorization 헤더에서 토큰 추출됨"); return authHeader.substring(7); } - // accessToken 헤더에서 토큰 추출 final String accessTokenHeader = request.getHeader("accessToken"); if (accessTokenHeader != null) { logger.info("accessToken 헤더에서 토큰 추출됨"); return accessTokenHeader; } - // 쿠키에서 토큰 추출 Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { @@ -139,4 +134,4 @@ private String extractTokenFromRequest(HttpServletRequest request) { return null; } -} \ No newline at end of file +} From 1b75a0810d6dfb6a878a27e1585c22429fcd66b0 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Sat, 10 May 2025 17:11:38 +0900 Subject: [PATCH 275/361] =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/example/userservice/dto/MemberDto.java | 2 ++ .../java/com/example/userservice/service/MemberService.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java index 41fe595..05b7eba 100644 --- a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java +++ b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java @@ -15,6 +15,7 @@ public class MemberDto { @AllArgsConstructor @NoArgsConstructor public static class MemberResponse { + private String memberId; private String user_id; // 로그인 ID private String nickname; // 닉네임 private Short birth_year; // 출생연도 @@ -30,6 +31,7 @@ public static class MemberResponse { @AllArgsConstructor @NoArgsConstructor public static class ProfileResponse { + private String member_id; private String user_id; private String nickname; // 닉네임 private Short birth_year; // 출생연도 diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java index e5ce460..f2e0420 100644 --- a/user-service/src/main/java/com/example/userservice/service/MemberService.java +++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java @@ -70,6 +70,7 @@ public MemberDto.ProfileResponse getProfile(String userId) { .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); return MemberDto.ProfileResponse.builder() + .member_id(String.valueOf(member.getMemberId())) .user_id(member.getUserId()) .nickname(member.getNickname()) .birth_year(member.getBirthYear()) @@ -119,6 +120,7 @@ public MemberDto.MemberResponse updateProfile(String userId, MemberDto.UpdateReq // 응답 DTO로 변환 return MemberDto.MemberResponse.builder() + .memberId(updatedMember.getUserId()) .user_id(updatedMember.getUserId()) .nickname(updatedMember.getNickname()) .birth_year(updatedMember.getBirthYear()) From 98bf26e7f2281455e903654b64522ae654e995ac Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Sat, 10 May 2025 17:25:42 +0900 Subject: [PATCH 276/361] =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EB=A7=89=ED=9E=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/userservice/security/JwtAuthenticationFilter.java | 1 + 1 file changed, 1 insertion(+) diff --git a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java index 3ecaecc..62d5f0e 100644 --- a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java +++ b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java @@ -38,6 +38,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { "/auth/login", "/auth/logout", "/swagger-ui.html", + "/swagger-ui/index.html", "/v3/api-docs" ); From bdf8f7321a2c61dc9aec988390e71869bcdcca52 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Tue, 13 May 2025 09:33:51 +0900 Subject: [PATCH 277/361] =?UTF-8?q?swagger=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/example/userservice/config/SwaggerConfig.java | 1 - 1 file changed, 1 deletion(-) diff --git a/user-service/src/main/java/com/example/userservice/config/SwaggerConfig.java b/user-service/src/main/java/com/example/userservice/config/SwaggerConfig.java index f48c45f..cbccd19 100644 --- a/user-service/src/main/java/com/example/userservice/config/SwaggerConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/SwaggerConfig.java @@ -8,7 +8,6 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; @Configuration -@EnableWebMvc public class SwaggerConfig { @Bean From 6c33842185252e9aa1af3a1147a27874ef1887a2 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Tue, 13 May 2025 09:54:21 +0900 Subject: [PATCH 278/361] =?UTF-8?q?swaggerui=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway/src/main/resources/application.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml index 757652e..2650462 100644 --- a/gateway/src/main/resources/application.yml +++ b/gateway/src/main/resources/application.yml @@ -9,25 +9,37 @@ spring: predicates: - Path=/control/congestion/** filters: - - RewritePath=/control/congestion/(?.*), /$\{segment} + - RewritePath=/control/congestion/(?.*), /${segment} + - id: external-service uri: http://localhost:8081 predicates: - Path=/control/external/** filters: - - RewritePath=/control/external/(?.*), /$\{segment} + - RewritePath=/control/external/(?.*), /${segment} + - id: place-service uri: http://localhost:8082 predicates: - Path=/place/** filters: - - RewritePath=/place/(?.*), /$\{segment} + - RewritePath=/place/(?.*), /${segment} + - id: user-service uri: http://localhost:8083 predicates: - Path=/user/** filters: - - RewritePath=/user/(?.*), /$\{segment} + - RewritePath=/user/(?.*), /${segment} + + # ✅ Swagger 경로를 user-service로 직접 전달 + - id: user-service-swagger + uri: http://localhost:8083 + predicates: + - Path=/swagger-ui/**, /v3/api-docs/** + filters: + - StripPrefix=0 + globalcors: corsConfigurations: '[/**]': From d1c0d396c62ee8d4ccda51af3fca0b785ded5903 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Tue, 13 May 2025 10:13:16 +0900 Subject: [PATCH 279/361] =?UTF-8?q?swagger-ui=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway/.gitignore | 1 + gateway/src/main/resources/application.yml | 23 ++++++---------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/gateway/.gitignore b/gateway/.gitignore index 5dec76b..bc06ee7 100644 --- a/gateway/.gitignore +++ b/gateway/.gitignore @@ -5,6 +5,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ src/main/resources/application.properties +src/main/resources/application.yml .env ### STS ### diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml index 2650462..db9e993 100644 --- a/gateway/src/main/resources/application.yml +++ b/gateway/src/main/resources/application.yml @@ -9,37 +9,26 @@ spring: predicates: - Path=/control/congestion/** filters: - - RewritePath=/control/congestion/(?.*), /${segment} - + - RewritePath=/control/congestion/(?.*), /$\{segment} - id: external-service uri: http://localhost:8081 predicates: - Path=/control/external/** filters: - - RewritePath=/control/external/(?.*), /${segment} - + - RewritePath=/control/external/(?.*), /$\{segment} - id: place-service uri: http://localhost:8082 predicates: - Path=/place/** filters: - - RewritePath=/place/(?.*), /${segment} - + - RewritePath=/place/(?.*), /$\{segment} - id: user-service uri: http://localhost:8083 predicates: - - Path=/user/** - filters: - - RewritePath=/user/(?.*), /${segment} - - # ✅ Swagger 경로를 user-service로 직접 전달 - - id: user-service-swagger - uri: http://localhost:8083 - predicates: - - Path=/swagger-ui/**, /v3/api-docs/** + - Path=/user/**,Path=/auth/**, /v3/api-docs/**, /swagger-ui/** filters: - - StripPrefix=0 - + - RewritePath=/user/(?.*), /$\{segment} + - RewritePath=/swagger-ui/(?.*), /swagger-ui/${segment} globalcors: corsConfigurations: '[/**]': From ea8eaef1c2d4e38bd27639296addebf28f814fa2 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Tue, 13 May 2025 10:19:45 +0900 Subject: [PATCH 280/361] swagger 404 --- gateway/src/main/resources/application.yml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml index db9e993..5c12b13 100644 --- a/gateway/src/main/resources/application.yml +++ b/gateway/src/main/resources/application.yml @@ -9,26 +9,35 @@ spring: predicates: - Path=/control/congestion/** filters: - - RewritePath=/control/congestion/(?.*), /$\{segment} + - RewritePath=/control/congestion/(?.*), /${segment} + - id: external-service uri: http://localhost:8081 predicates: - Path=/control/external/** filters: - - RewritePath=/control/external/(?.*), /$\{segment} + - RewritePath=/control/external/(?.*), /${segment} + - id: place-service uri: http://localhost:8082 predicates: - Path=/place/** filters: - - RewritePath=/place/(?.*), /$\{segment} + - RewritePath=/place/(?.*), /${segment} + - id: user-service uri: http://localhost:8083 predicates: - - Path=/user/**,Path=/auth/**, /v3/api-docs/**, /swagger-ui/** + - Path=/user/** + - Path=/auth/** + - Path=/v3/api-docs/** + - Path=/swagger-ui/** filters: - - RewritePath=/user/(?.*), /$\{segment} + - RewritePath=/user/(?.*), /${segment} + - RewritePath=/auth/(?.*), /${segment} - RewritePath=/swagger-ui/(?.*), /swagger-ui/${segment} + - RewritePath=/v3/api-docs/(?.*), /v3/api-docs/${segment} + globalcors: corsConfigurations: '[/**]': From 6ed8a21208bb62a9cd78278ab39ab110c0f57645 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Tue, 13 May 2025 10:32:43 +0900 Subject: [PATCH 281/361] =?UTF-8?q?swgger=EC=98=A4=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway/src/main/resources/application.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml index 5c12b13..d8d52fe 100644 --- a/gateway/src/main/resources/application.yml +++ b/gateway/src/main/resources/application.yml @@ -30,13 +30,17 @@ spring: predicates: - Path=/user/** - Path=/auth/** - - Path=/v3/api-docs/** - - Path=/swagger-ui/** filters: - RewritePath=/user/(?.*), /${segment} - RewritePath=/auth/(?.*), /${segment} - - RewritePath=/swagger-ui/(?.*), /swagger-ui/${segment} - - RewritePath=/v3/api-docs/(?.*), /v3/api-docs/${segment} + + - id: user-service-swagger + uri: http://localhost:8083 + predicates: + - Path=/swagger-ui/** + - Path=/v3/api-docs/** + filters: + - StripPrefix=0 # globalcors: corsConfigurations: From 92a41526856a696cb12d6069a295ada3564add05 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Tue, 13 May 2025 10:46:37 +0900 Subject: [PATCH 282/361] =?UTF-8?q?=EC=98=A4=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 --- gateway/src/main/resources/application.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml index d8d52fe..bee3c59 100644 --- a/gateway/src/main/resources/application.yml +++ b/gateway/src/main/resources/application.yml @@ -34,14 +34,6 @@ spring: - RewritePath=/user/(?.*), /${segment} - RewritePath=/auth/(?.*), /${segment} - - id: user-service-swagger - uri: http://localhost:8083 - predicates: - - Path=/swagger-ui/** - - Path=/v3/api-docs/** - filters: - - StripPrefix=0 # - globalcors: corsConfigurations: '[/**]': @@ -49,6 +41,7 @@ spring: - "http://localhost:5173" - "http://192.168.0.186:5173" - "http://58.127.241.84:5173" + - "http://api.seoultravel.life" allowedMethods: - GET - POST From 7b81f50e102c62334d52dceceb3ff20d63bbbb45 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Tue, 13 May 2025 11:06:16 +0900 Subject: [PATCH 283/361] =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway/src/main/resources/application.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml index bee3c59..25976e0 100644 --- a/gateway/src/main/resources/application.yml +++ b/gateway/src/main/resources/application.yml @@ -1,6 +1,7 @@ spring: application: name: gateway + cloud: gateway: routes: From 2f2b37a8d61ec85dc46646b195051359c3c7e787 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Tue, 13 May 2025 11:21:56 +0900 Subject: [PATCH 284/361] =?UTF-8?q?=EC=98=A4=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 --- .../java/com/example/userservice/config/SecurityConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java index ce6f93b..1bbc55c 100644 --- a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java @@ -28,7 +28,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers( "/swagger-ui.html", "/swagger-ui/**", - "/v3/api-docs/**" + "/v3/api-docs/**", + "/user/swagger-ui/index.html" ).permitAll() // 공개 엔드포인트 - 인증 불필요 From ca3f36eab9cefd976f400f1281dde4774223cb03 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 13 May 2025 13:14:31 +0900 Subject: [PATCH 285/361] =?UTF-8?q?build.gradle=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- congestion-service/build.gradle | 1 + external-info-service/build.gradle | 1 + 2 files changed, 2 insertions(+) diff --git a/congestion-service/build.gradle b/congestion-service/build.gradle index 9367d46..02b7ced 100644 --- a/congestion-service/build.gradle +++ b/congestion-service/build.gradle @@ -46,6 +46,7 @@ dependencies { implementation 'org.apache.kafka:kafka-streams' implementation 'org.springframework.kafka:spring-kafka' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'org.postgresql:postgresql' diff --git a/external-info-service/build.gradle b/external-info-service/build.gradle index 2d6d5c0..21e026f 100644 --- a/external-info-service/build.gradle +++ b/external-info-service/build.gradle @@ -56,6 +56,7 @@ dependencies { implementation 'org.apache.kafka:kafka-streams' implementation 'org.springframework.kafka:spring-kafka' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'org.postgresql:postgresql' From 94e5fd0939aa3560fe007022de0c650a17879656 Mon Sep 17 00:00:00 2001 From: GLENSIS Date: Tue, 13 May 2025 13:27:44 +0900 Subject: [PATCH 286/361] =?UTF-8?q?place=5Fid=EC=99=80=20type=EB=A5=BC=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/placeservice/dto/PlaceDocument.java | 3 ++- .../placeservice/service/PlaceDocumentMapper.java | 15 ++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java b/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java index b91c9ba..6ad5003 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java +++ b/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java @@ -9,6 +9,7 @@ public class PlaceDocument { // 공통 private String name; - private String category; + private String type; private String address; + private String place_id; } diff --git a/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java index 7d7791f..c6a0ff1 100644 --- a/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java +++ b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java @@ -12,7 +12,8 @@ public PlaceDocument toPlaceDocument(Accommodation item) { return PlaceDocument.builder() .name(item.getName()) .address(item.getAddress()) - .category("accommodation") + .type("accommodation") + .place_id(String.valueOf(item.getArea().getAreaId())) .build(); } @@ -20,7 +21,8 @@ public PlaceDocument toPlaceDocument(Cafe item) { return PlaceDocument.builder() .name(item.getName()) .address(item.getAddress()) - .category("cafe") + .type("cafe") + .place_id(String.valueOf(item.getArea().getAreaId())) .build(); } @@ -28,7 +30,8 @@ public PlaceDocument toPlaceDocument(CulturalEvent item) { return PlaceDocument.builder() .name(item.getTitle()) .address(item.getAddress()) - .category("culturalevent") + .type("culturalevent") + .place_id(String.valueOf(item.getArea().getAreaId())) .build(); } @@ -36,7 +39,8 @@ public PlaceDocument toPlaceDocument(Restaurant item) { return PlaceDocument.builder() .name(item.getName()) .address(item.getAddress()) - .category("restaurant") + .type("restaurant") + .place_id(String.valueOf(item.getArea().getAreaId())) .build(); } @@ -44,7 +48,8 @@ public PlaceDocument toPlaceDocument(Attraction item) { return PlaceDocument.builder() .name(item.getName()) .address(item.getAddress()) - .category("attraction") + .type("attraction") + .place_id(String.valueOf(item.getArea().getAreaId())) .build(); } } From a829eea0667b1daeea5a30d5f1f9981f710be7c8 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Tue, 13 May 2025 13:27:46 +0900 Subject: [PATCH 287/361] =?UTF-8?q?place-service=20swagger-ui=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 --- place-service/build.gradle | 2 ++ .../placeservice/config/SecurityConfig.java | 10 ++++--- .../placeservice/config/SwaggerConfig.java | 26 +++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/config/SwaggerConfig.java diff --git a/place-service/build.gradle b/place-service/build.gradle index 1db981b..1dfc1ee 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -29,6 +29,8 @@ dependencies { // JAXB Runtime (XML 파싱을 위해 필요) implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + // JWT 처리 implementation 'io.jsonwebtoken:jjwt-api:0.12.6' diff --git a/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java index 8fe1402..a610a14 100644 --- a/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java +++ b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java @@ -12,13 +12,17 @@ public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .csrf(csrf -> csrf.disable()) // CSRF 비활성화 + .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth - .anyRequest().permitAll() // 모든 요청 허용 + .requestMatchers( + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html" + ).permitAll() + .anyRequest().permitAll() ); return http.build(); } - } diff --git a/place-service/src/main/java/com/example/placeservice/config/SwaggerConfig.java b/place-service/src/main/java/com/example/placeservice/config/SwaggerConfig.java new file mode 100644 index 0000000..fba4e37 --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/SwaggerConfig.java @@ -0,0 +1,26 @@ +package com.example.placeservice.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .components(new Components()) + .info(apiInfo()); + } + + private Info apiInfo() { + return new Info() + .title("API Title") // API의 제목 + .description("This is my Swagger UI") // API에 대한 설명 + .version("1.0.0"); // API의 버전 + } + +} From c2b239e3c03af7f2a35b2ea99705c5f3e068fa76 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Tue, 13 May 2025 13:35:09 +0900 Subject: [PATCH 288/361] =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/placeservice/config/SecurityConfig.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java index a610a14..7010ad6 100644 --- a/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java +++ b/place-service/src/main/java/com/example/placeservice/config/SecurityConfig.java @@ -11,8 +11,7 @@ public class SecurityConfig { // spring-security 임시 block @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .csrf(csrf -> csrf.disable()) + http.csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers( "/v3/api-docs/**", From bd8e99f2eba833092dfd16313d892b441763afb4 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Tue, 13 May 2025 13:35:53 +0900 Subject: [PATCH 289/361] =?UTF-8?q?Refactor:=20=ED=86=B5=ED=95=A9=EA=B2=80?= =?UTF-8?q?=EC=83=89=20place=5Fid=20=EC=88=98=EC=A0=95,=20=EA=B4=80?= =?UTF-8?q?=EA=B4=91=EC=A7=80=20=EB=8D=B0=EC=9D=B4=ED=84=B0(=EC=9D=8C?= =?UTF-8?q?=EC=8B=9D=EC=A0=90=EC=9C=BC=EB=A1=9C=20=EC=B6=94=EC=A0=95?= =?UTF-8?q?=EB=90=98=EB=8A=94=20id=EA=B0=92=20=EC=A0=9C=EC=99=B8),=20exter?= =?UTF-8?q?nal=20stream=20sse=20=EA=B5=AC=EB=8F=85=20=EC=8B=9C=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=9C=EA=B3=B5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StreamController.java | 18 ++++++++++++++++++ .../service/AttractionService.java | 2 +- .../service/PlaceDocumentMapper.java | 10 +++++----- .../service/PlaceDocumentService.java | 2 -- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java index 8e95ae2..d9782c1 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java @@ -1,5 +1,8 @@ package com.example.externalinfoservice.controller; +import com.example.externalinfoservice.service.ESRoadService; +import com.example.externalinfoservice.service.ParkEsService; +import com.example.externalinfoservice.service.WeatherEsService; import com.fasterxml.jackson.databind.JsonNode; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; @@ -18,6 +21,10 @@ public class StreamController { private final List emitters = new CopyOnWriteArrayList<>(); + private final WeatherEsService weatherEsService; + private final ESRoadService roadService; + private final ParkEsService parkEsService; + @GetMapping("/stream") public SseEmitter streamWeather() { SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음 @@ -27,6 +34,17 @@ public SseEmitter streamWeather() { emitter.onTimeout(() -> emitters.remove(emitter)); emitter.onError((e) -> emitters.remove(emitter)); + + // 구독시, 초기 데이터 주입 + try { + var weatherList = weatherEsService.getAllWeatherFromES(); + var trafficList = roadService.getTrafficData(); + var parkList = parkEsService.getAllParkFromES(); + sendToClients(weatherList, trafficList, parkList); + } catch (Exception e) { + throw new RuntimeException(e); + } + return emitter; } diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index 6de9b9b..e170be4 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -57,7 +57,7 @@ public List fetchDataFromVisitSeoul() { // seoul_attraction_id 중복 체크 if (attractionRepository.existsBySeoulAttractionId(table.getId())) return null; - + if (Integer.valueOf(table.getId()) >= 31345 && Integer.valueOf(table.getId()) <= 53595) return null; // 👇: 조건에 따라 Area 객체를 지정 Area area = findAreaByCondition(table, areaList); // 예: 주소나 지역코드 등으로 판단 if (area == null) return null; diff --git a/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java index c6a0ff1..766aadb 100644 --- a/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java +++ b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java @@ -13,7 +13,7 @@ public PlaceDocument toPlaceDocument(Accommodation item) { .name(item.getName()) .address(item.getAddress()) .type("accommodation") - .place_id(String.valueOf(item.getArea().getAreaId())) + .place_id(String.valueOf(item.getAccommodationId())) .build(); } @@ -22,7 +22,7 @@ public PlaceDocument toPlaceDocument(Cafe item) { .name(item.getName()) .address(item.getAddress()) .type("cafe") - .place_id(String.valueOf(item.getArea().getAreaId())) + .place_id(String.valueOf(item.getId())) .build(); } @@ -31,7 +31,7 @@ public PlaceDocument toPlaceDocument(CulturalEvent item) { .name(item.getTitle()) .address(item.getAddress()) .type("culturalevent") - .place_id(String.valueOf(item.getArea().getAreaId())) + .place_id(String.valueOf(item.getEventId())) .build(); } @@ -40,7 +40,7 @@ public PlaceDocument toPlaceDocument(Restaurant item) { .name(item.getName()) .address(item.getAddress()) .type("restaurant") - .place_id(String.valueOf(item.getArea().getAreaId())) + .place_id(String.valueOf(item.getRestaurantId())) .build(); } @@ -49,7 +49,7 @@ public PlaceDocument toPlaceDocument(Attraction item) { .name(item.getName()) .address(item.getAddress()) .type("attraction") - .place_id(String.valueOf(item.getArea().getAreaId())) + .place_id(String.valueOf(item.getAttractionId())) .build(); } } diff --git a/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentService.java b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentService.java index 70abe76..412bc42 100644 --- a/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentService.java +++ b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentService.java @@ -27,8 +27,6 @@ public class PlaceDocumentService { private final CulturalEventRepository culturalEventRepository; private final CafeRepository cafeRepository; - - private final PlaceDocumentRepository placeDocumentRepository; private final PlaceDocumentMapper placeDocumentMapper; public List searchPlacesByName(String keyword) { From 0e58866a26577f10ffd7420c92ce5985648f060b Mon Sep 17 00:00:00 2001 From: ha-yun Date: Tue, 13 May 2025 13:57:01 +0900 Subject: [PATCH 290/361] =?UTF-8?q?Refactor:=20@RestControllerAdvice=20?= =?UTF-8?q?=EC=B6=A9=EB=8F=8C=20=EC=98=A4=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 --- .../exception/CustomExceptionClass.java | 34 -------- .../exception/ErrorCode.java | 49 ----------- .../exception/ErrorResponse.java | 18 ---- .../exception/GlobalExceptionHandler.java | 87 ------------------- 4 files changed, 188 deletions(-) delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java deleted file mode 100644 index 9a78486..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/exception/CustomExceptionClass.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.externalinfoservice.exception; - -public class CustomExceptionClass { - public static class DataNotFoundException extends RuntimeException { - public DataNotFoundException(String message) { - super(message); - } - - public DataNotFoundException(String message, Throwable cause) { - super(message, cause); - } - } - - public static class ConflictException extends RuntimeException { - public ConflictException(String message) { - super(message); - } - - public ConflictException(String message, Throwable cause) { - super(message, cause); - } - } - - public static class TooManyRequestsException extends RuntimeException { - public TooManyRequestsException(String message) { - super(message); - } - - public TooManyRequestsException(String message, Throwable cause) { - super(message, cause); - } - } - -} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java deleted file mode 100644 index a84b39c..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorCode.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.externalinfoservice.exception; - -import org.springframework.http.HttpStatus; - -public enum ErrorCode { - // 400 - 1000 - Invalid request - INVALID_REQUEST("1000", HttpStatus.BAD_REQUEST), - DATA_NOT_FOUND("1001", HttpStatus.BAD_REQUEST), - - // 401 - 2000 - Unauthorized - UNAUTHORIZED("2000", HttpStatus.UNAUTHORIZED), - - // 403 - 2001 - Forbidden - FORBIDDEN("2001", HttpStatus.FORBIDDEN), - - // 404 - 3000 - Resource not found - RESOURCE_NOT_FOUND("3000", HttpStatus.NOT_FOUND), - - // 409 - 3001 - Conflict - CONFLICT("3001", HttpStatus.CONFLICT), - - // 422 - 3002 - Validation failed - VALIDATION_FAILED("3002", HttpStatus.UNPROCESSABLE_ENTITY), - - // 429 - 3003 - Too Many Requests - TOO_MANY_REQUESTS("3003", HttpStatus.TOO_MANY_REQUESTS), - - // 500 - 9000 - Internal server error - INTERNAL_SERVER_ERROR("9000", HttpStatus.INTERNAL_SERVER_ERROR), - - // 503 - 9001 - Service unavailable - SERVICE_UNAVAILABLE("9001", HttpStatus.SERVICE_UNAVAILABLE); - - private final String code; - private final HttpStatus status; - - ErrorCode(String code, HttpStatus status) { - this.code = code; - this.status = status; - } - - public String getCode() { - return code; - } - - public HttpStatus getStatus() { - return status; - } -} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java deleted file mode 100644 index 276838f..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/exception/ErrorResponse.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.externalinfoservice.exception; - -import lombok.Data; - -@Data -public class ErrorResponse { - private int status; - private String errorCode; - private String message; - - public ErrorResponse(int status, String errorCode, String message) { - this.status = status; - this.errorCode = errorCode; - this.message = message; - } - - -} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java b/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java deleted file mode 100644 index ef70e57..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/exception/GlobalExceptionHandler.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.example.externalinfoservice.exception; - -import org.apache.kafka.common.errors.InvalidRequestException; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.servlet.NoHandlerFoundException; - -import javax.naming.ServiceUnavailableException; -import javax.xml.bind.ValidationException; - -@RestControllerAdvice -public class GlobalExceptionHandler { - // Spring에서 제공하는 기본 예외 클래스 - - // 400 - 1000 - InvalidRequestException - @ExceptionHandler(InvalidRequestException.class) - public ResponseEntity handleInvalidRequestException(InvalidRequestException ex) { - return buildErrorResponse( ErrorCode.INVALID_REQUEST, ex.getMessage()); - } - // 401 - 2000 - Unauthorized - @ExceptionHandler(HttpClientErrorException.Unauthorized.class) - public ResponseEntity handleUnauthorizedException(HttpClientErrorException.Unauthorized ex) { - return buildErrorResponse( ErrorCode.UNAUTHORIZED, ex.getMessage()); - } - // 403 - 2001 - Forbidden - @ExceptionHandler(HttpClientErrorException.Forbidden.class) - public ResponseEntity handleForbiddenException(HttpClientErrorException.Forbidden ex) { - return buildErrorResponse(ErrorCode.FORBIDDEN, ex.getMessage()); - } - // 404 - 3000 - Resource not found - @ExceptionHandler(HttpClientErrorException.NotFound.class) - public ResponseEntity handleNotFoundException(HttpClientErrorException.NotFound ex) { - return buildErrorResponse(ErrorCode.RESOURCE_NOT_FOUND, ex.getMessage()); - } - - // 커스텀 예외 클래스 - // 400 - 1001 - DataNotFoundException - @ExceptionHandler(CustomExceptionClass.DataNotFoundException.class) - public ResponseEntity handleDataNotFoundException(CustomExceptionClass.DataNotFoundException ex) { - return buildErrorResponse(ErrorCode.DATA_NOT_FOUND, ex.getMessage()); - } - - // 409 - 3001 - Conflict - @ExceptionHandler(CustomExceptionClass.ConflictException.class) // 커스텀 예외 - public ResponseEntity handleConflictException(CustomExceptionClass.ConflictException ex) { - return buildErrorResponse(ErrorCode.CONFLICT, ex.getMessage()); - } - - // 422 - 3002 - Validation failed - @ExceptionHandler(ValidationException.class) // javax.validation 또는 커스텀 가능 - public ResponseEntity handleValidationException(ValidationException ex) { - return buildErrorResponse(ErrorCode.VALIDATION_FAILED, ex.getMessage()); - } - - // 429 - 3003 - Too Many Requests - @ExceptionHandler(CustomExceptionClass.TooManyRequestsException.class) - public ResponseEntity handleTooManyRequestsException(CustomExceptionClass.TooManyRequestsException ex) { - return buildErrorResponse(ErrorCode.TOO_MANY_REQUESTS, ex.getMessage()); - } - - // 500 - 9000 - Internal server error - @ExceptionHandler(RuntimeException.class) - public ResponseEntity handleInternalServerError(Exception ex) { - return buildErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR, ex.getMessage()); - } - - // 503 - 9001 - Service unavailable - @ExceptionHandler(ServiceUnavailableException.class) // 커스텀 예외 - public ResponseEntity handleServiceUnavailableException(ServiceUnavailableException ex) { - return buildErrorResponse(ErrorCode.SERVICE_UNAVAILABLE, ex.getMessage()); - } - - - // === 공통 응답 생성 메서드 === - private ResponseEntity buildErrorResponse(ErrorCode errorCode, String message) { - return new ResponseEntity<>( - new ErrorResponse(errorCode.getStatus().value(), errorCode.getCode(), message), - errorCode.getStatus() - ); - } - - - - -} From 8ceb36ad59ffc4fc349776af3503e1efaf7f1ef5 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Tue, 13 May 2025 14:12:04 +0900 Subject: [PATCH 291/361] =?UTF-8?q?Refactor:=20congestion=20sse=EA=B5=AC?= =?UTF-8?q?=EB=8F=85=20=EC=8B=9C=20=EC=B4=88=EA=B8=B0=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CongestionController.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java index 30b205d..80c1305 100644 --- a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java +++ b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java @@ -1,5 +1,6 @@ package com.example.congestionservice.controller; +import com.example.congestionservice.service.CongestionService; import com.fasterxml.jackson.databind.JsonNode; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; @@ -17,6 +18,9 @@ public class CongestionController { private final List emitters = new CopyOnWriteArrayList<>(); + private final CongestionService congestionService; + + @GetMapping("/congestion") public SseEmitter streamCongestion() { SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음 @@ -26,6 +30,16 @@ public SseEmitter streamCongestion() { emitter.onTimeout(() -> emitters.remove(emitter)); emitter.onError((e) -> emitters.remove(emitter)); + try{ + System.out.println("혼잡도 푸시 중..."); + var congestionList = congestionService.getCongestion(); + + sendToClients(congestionList); // 모든 지역 혼잡도 전송 + System.out.println(".. 혼잡도 푸시 완료"); + } catch (Exception e) { + throw new RuntimeException(e); + } + return emitter; } From 75f2e57b7cda679195f2197e8d308b411a8e669b Mon Sep 17 00:00:00 2001 From: ha-yun Date: Tue, 13 May 2025 15:41:03 +0900 Subject: [PATCH 292/361] =?UTF-8?q?Refactor:=20sse=20=EC=98=A4=EB=A5=98(te?= =?UTF-8?q?xt/plain=EC=9C=BC=EB=A1=9C=20=EC=A0=84=EC=86=A1=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=EC=A4=91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StreamController.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java index d9782c1..613cbb6 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java @@ -5,6 +5,7 @@ import com.example.externalinfoservice.service.WeatherEsService; import com.fasterxml.jackson.databind.JsonNode; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -25,7 +26,7 @@ public class StreamController { private final ESRoadService roadService; private final ParkEsService parkEsService; - @GetMapping("/stream") + @GetMapping(value="/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter streamWeather() { SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음 emitters.add(emitter); @@ -34,16 +35,11 @@ public SseEmitter streamWeather() { emitter.onTimeout(() -> emitters.remove(emitter)); emitter.onError((e) -> emitters.remove(emitter)); - // 구독시, 초기 데이터 주입 - try { - var weatherList = weatherEsService.getAllWeatherFromES(); - var trafficList = roadService.getTrafficData(); - var parkList = parkEsService.getAllParkFromES(); - sendToClients(weatherList, trafficList, parkList); - } catch (Exception e) { - throw new RuntimeException(e); - } + var weatherList = weatherEsService.getAllWeatherFromES(); + var trafficList = roadService.getTrafficData(); + var parkList = parkEsService.getAllParkFromES(); + sendToClients(weatherList, trafficList, parkList); return emitter; } From 9068bc004def3f2f1aceb1ffd3b6157be9e9b9e6 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Tue, 13 May 2025 15:42:35 +0900 Subject: [PATCH 293/361] =?UTF-8?q?Refactor:=20place=20attraction=20?= =?UTF-8?q?=EA=B4=80=EA=B4=91=EC=A7=80=20api=20=EC=88=98=EC=A0=95=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/placeservice/service/AttractionService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index e170be4..8161c66 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -106,7 +106,7 @@ public List fetchDataFromVisitSeoul() { } } - // 관광지 2km이내 area 계산 + // 관광지 2km이내 area 계산(1km로 수정중) private Area findAreaByCondition(AttractionDto.AttractionTable table, List areaList) { double lat = Double.parseDouble(table.getMapY()); double lon = Double.parseDouble(table.getMapX()); @@ -115,12 +115,12 @@ private Area findAreaByCondition(AttractionDto.AttractionTable table, List lat, lon, area.getLat().doubleValue(), area.getLon().doubleValue() ); - if (distance <= 2.0) { // 2km 이내 + if (distance <= 1.0) { // 2km 이내(1km 이내로 수정중) return area; } } - // 2km 이내에 없는 관광지 체크 - // System.out.println(table.getTitle()+"경도:"+table.getMapX()+"위도 :"+table.getMapY() ); + // 2km 이내에 없는 관광지 체크(1km 이내로 수정중) + System.out.println(table.getTitle()+"경도:"+table.getMapX()+"위도 :"+table.getMapY() ); return null; } From fd459c174179af62c775530b88e6edf6345cc889 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Tue, 13 May 2025 16:15:17 +0900 Subject: [PATCH 294/361] =?UTF-8?q?Refactor:=20SSE=20=EC=B4=88=EA=B8=B0=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80=20=EC=8B=9C=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StreamController.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java index 613cbb6..13b13c0 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java @@ -36,10 +36,23 @@ public SseEmitter streamWeather() { emitter.onError((e) -> emitters.remove(emitter)); // 구독시, 초기 데이터 주입 - var weatherList = weatherEsService.getAllWeatherFromES(); - var trafficList = roadService.getTrafficData(); - var parkList = parkEsService.getAllParkFromES(); - sendToClients(weatherList, trafficList, parkList); + try { + var weatherList = weatherEsService.getAllWeatherFromES(); + var trafficList = roadService.getTrafficData(); + var parkList = parkEsService.getAllParkFromES(); + emitter.send(SseEmitter.event() + .name("weather-update") + .data(weatherList)); + emitter.send(SseEmitter.event() + .name("traffic-update") + .data(trafficList)); + emitter.send(SseEmitter.event() + .name("park-update") + .data(parkList)); + }catch (IOException | IllegalStateException e) { + emitter.completeWithError(e); + emitters.remove(emitter); + } return emitter; } @@ -58,7 +71,7 @@ public void sendToClients(List> weatherList, JsonNode traffi .name("park-update") .data(parkList)); - } catch (IOException e) { + } catch (IOException | IllegalStateException e) { emitter.completeWithError(e); emitters.remove(emitter); } From 94742d948b5b11f5e2f2ce9ec0ef33aba9c765d3 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Tue, 13 May 2025 16:33:16 +0900 Subject: [PATCH 295/361] =?UTF-8?q?Refactor:=20SSe=20=EC=B4=88=EA=B8=B0=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80=EC=8B=9C=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CongestionController.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java index 80c1305..8d4478f 100644 --- a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java +++ b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java @@ -30,14 +30,18 @@ public SseEmitter streamCongestion() { emitter.onTimeout(() -> emitters.remove(emitter)); emitter.onError((e) -> emitters.remove(emitter)); - try{ - System.out.println("혼잡도 푸시 중..."); + try { + System.out.println("혼잡도 초기 데이터 푸시 중..."); var congestionList = congestionService.getCongestion(); - sendToClients(congestionList); // 모든 지역 혼잡도 전송 - System.out.println(".. 혼잡도 푸시 완료"); - } catch (Exception e) { - throw new RuntimeException(e); + emitter.send(SseEmitter.event() + .name("congestion-update") + .data(congestionList)); + + System.out.println(".. 혼잡도 초기 데이터 푸시 완료"); + } catch (IOException | IllegalStateException e) { + emitter.completeWithError(e); + emitters.remove(emitter); } return emitter; From 89c41fdb7a432c414edb722fb028af105c2246b2 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Tue, 13 May 2025 16:41:40 +0900 Subject: [PATCH 296/361] =?UTF-8?q?Refactor:=20=EA=B4=80=EA=B4=91=EC=A7=80?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0-=20=EA=B4=80=EA=B4=91=EC=A7=80?= =?UTF-8?q?=20=EA=B8=B0=EC=A4=80=20=EA=B0=80=EC=9E=A5=20=EA=B0=80=EA=B9=8C?= =?UTF-8?q?=EC=9A=B4=20=EC=A7=80=EC=97=AD=EA=B3=BC=20mapping,=202km?= =?UTF-8?q?=EB=84=98=EC=96=B4=EA=B0=80=EB=A9=B4=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AttractionService.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index 8161c66..155c8db 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -58,6 +58,7 @@ public List fetchDataFromVisitSeoul() { // seoul_attraction_id 중복 체크 if (attractionRepository.existsBySeoulAttractionId(table.getId())) return null; if (Integer.valueOf(table.getId()) >= 31345 && Integer.valueOf(table.getId()) <= 53595) return null; + // 👇: 조건에 따라 Area 객체를 지정 Area area = findAreaByCondition(table, areaList); // 예: 주소나 지역코드 등으로 판단 if (area == null) return null; @@ -106,27 +107,34 @@ public List fetchDataFromVisitSeoul() { } } - // 관광지 2km이내 area 계산(1km로 수정중) + // 1. 관광지 기준 가장 가까운 area를 찾는다 private Area findAreaByCondition(AttractionDto.AttractionTable table, List areaList) { double lat = Double.parseDouble(table.getMapY()); double lon = Double.parseDouble(table.getMapX()); + + Area closestArea = null; + double minDistance = Double.MAX_VALUE; + for (Area area : areaList) { double distance = calculateDistanceKm( lat, lon, area.getLat().doubleValue(), area.getLon().doubleValue() ); - if (distance <= 1.0) { // 2km 이내(1km 이내로 수정중) - return area; + + if (distance < minDistance) { + minDistance = distance; + closestArea = area; } } - // 2km 이내에 없는 관광지 체크(1km 이내로 수정중) - System.out.println(table.getTitle()+"경도:"+table.getMapX()+"위도 :"+table.getMapY() ); - return null; + if (minDistance > 2.0) { // 2. 거리가 2km를 넘어가면 제외한다. + System.out.println(table.getTitle() + " - 가까운 지역 없음 (거리: " + minDistance + "km)"); + return null; + } + return closestArea; } - // attraction 목록 DB 불러오기 public List getAttractionData() { try { From 361f878451d67109a0cc1a2f8958494c83462d3f Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 14 May 2025 09:26:58 +0900 Subject: [PATCH 297/361] =?UTF-8?q?=EC=82=AC=EA=B3=A0=20=EC=8B=A4=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExternalInfoServiceApplication.java | 2 + .../controller/AccidentEsController.java | 50 +++++++++ .../controller/ParkingControllerOriginal.java | 19 ---- .../controller/StreamController.java | 12 +- .../controller/WeatherControllerOriginal.java | 28 ----- .../externalinfoservice/dto/AccidentDto.java | 4 + .../scheduler/AccidentPushScheduler.java | 21 ++++ .../scheduler/PushScheduler.java | 6 +- .../service/AccidentEsService.java | 105 ++++++++++++++++++ .../service/RestaurantService.java | 2 +- 10 files changed, 196 insertions(+), 53 deletions(-) create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/AccidentEsController.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingControllerOriginal.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherControllerOriginal.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/dto/AccidentDto.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/scheduler/AccidentPushScheduler.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java index b077f1c..035a874 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java @@ -9,7 +9,9 @@ public class ExternalInfoServiceApplication { public static void main(String[] args) { + SpringApplication.run(ExternalInfoServiceApplication.class, args); + } } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/AccidentEsController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/AccidentEsController.java new file mode 100644 index 0000000..bfc7c6a --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/AccidentEsController.java @@ -0,0 +1,50 @@ +package com.example.externalinfoservice.controller; + +import com.example.externalinfoservice.service.AccidentEsService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +@RestController +@RequiredArgsConstructor +@RequestMapping("main/accident") +public class AccidentEsController { + + private final AccidentEsService accidentEsService; + private final List emitters = new CopyOnWriteArrayList<>(); + + @GetMapping("") + public SseEmitter streamAccident() { + SseEmitter emitter = new SseEmitter(0L); + emitters.add(emitter); + + emitter.onCompletion(() -> emitters.remove(emitter)); + emitter.onTimeout(() -> emitters.remove(emitter)); + emitter.onError((e) -> emitters.remove(emitter)); + + List> accidentList = accidentEsService.getAllAccidentsFromES(); + sendToClients(accidentList); + + return emitter; + } + + public void sendToClients(List> accidentList) { + for (SseEmitter emitter : emitters) { + try { + emitter.send(SseEmitter.event() + .name("accident-alert") + .data(accidentList)); + } catch (IOException e) { + emitter.completeWithError(e); + emitters.remove(emitter); + } + } + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingControllerOriginal.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingControllerOriginal.java deleted file mode 100644 index e9f096d..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingControllerOriginal.java +++ /dev/null @@ -1,19 +0,0 @@ -//package com.example.externalinfoservice.controller; -// -//import com.example.externalinfoservice.service.ParkServiceOriginal; -//import lombok.RequiredArgsConstructor; -//import org.springframework.http.MediaType; -//import org.springframework.web.bind.annotation.*; -// -//@RestController -//@RequestMapping("/parking") -//@RequiredArgsConstructor -//public class ParkingControllerOriginal { -// -// private final ParkServiceOriginal parkServiceOriginal; -// -// @GetMapping(value = "/{areaId}", produces = MediaType.APPLICATION_JSON_VALUE) -// public String getParking(@PathVariable String areaId) { -// return parkServiceOriginal.getParkingData(areaId); -// } -//} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java index d9782c1..567125f 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java @@ -1,5 +1,6 @@ package com.example.externalinfoservice.controller; +import com.example.externalinfoservice.service.AccidentEsService; import com.example.externalinfoservice.service.ESRoadService; import com.example.externalinfoservice.service.ParkEsService; import com.example.externalinfoservice.service.WeatherEsService; @@ -24,6 +25,7 @@ public class StreamController { private final WeatherEsService weatherEsService; private final ESRoadService roadService; private final ParkEsService parkEsService; + private final AccidentEsService accidentEsService; @GetMapping("/stream") public SseEmitter streamWeather() { @@ -34,13 +36,13 @@ public SseEmitter streamWeather() { emitter.onTimeout(() -> emitters.remove(emitter)); emitter.onError((e) -> emitters.remove(emitter)); - // 구독시, 초기 데이터 주입 try { var weatherList = weatherEsService.getAllWeatherFromES(); var trafficList = roadService.getTrafficData(); var parkList = parkEsService.getAllParkFromES(); - sendToClients(weatherList, trafficList, parkList); + var accidentList = accidentEsService.getAllAccidentsFromES(); + sendToClients(weatherList, trafficList, parkList, accidentList); } catch (Exception e) { throw new RuntimeException(e); } @@ -49,7 +51,7 @@ public SseEmitter streamWeather() { } // 주기적으로 클라이언트에게 push - public void sendToClients(List> weatherList, JsonNode trafficList, List> parkList) { + public void sendToClients(List> weatherList, JsonNode trafficList, List> parkList, List> accidentList) { for (SseEmitter emitter : emitters) { try { emitter.send(SseEmitter.event() @@ -61,7 +63,9 @@ public void sendToClients(List> weatherList, JsonNode traffi emitter.send(SseEmitter.event() .name("park-update") .data(parkList)); - + emitter.send(SseEmitter.event() + .name("accident-alert") + .data(accidentList)); } catch (IOException e) { emitter.completeWithError(e); emitters.remove(emitter); diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherControllerOriginal.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherControllerOriginal.java deleted file mode 100644 index c04b7bc..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherControllerOriginal.java +++ /dev/null @@ -1,28 +0,0 @@ -//package com.example.externalinfoservice.controller; -// -//import com.example.externalinfoservice.service.WeatherService; -//import lombok.RequiredArgsConstructor; -//import org.springframework.http.MediaType; -//import org.springframework.http.ResponseEntity; -//import org.springframework.web.bind.annotation.GetMapping; -//import org.springframework.web.bind.annotation.PathVariable; -//import org.springframework.web.bind.annotation.RequestMapping; -//import org.springframework.web.bind.annotation.RestController; -// -//@RestController -//@RequestMapping("/weather") -//@RequiredArgsConstructor -//public class WeatherControllerOriginal { -// private final WeatherService weatherService; -// -// @GetMapping("/{place-code}") -// public String getWeatherData(@PathVariable("place-code") String placeCode) { -// return weatherService.getWeatherData(placeCode); -// } -// -// @GetMapping(value = "/api/{place-code}", produces = MediaType.APPLICATION_JSON_VALUE) -// public ResponseEntity getWeatherDataApi(@PathVariable("place-code") String placeCode) { -// String jsonData = weatherService.getWeatherData(placeCode); -// return ResponseEntity.ok(jsonData); -// } -//} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/AccidentDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/AccidentDto.java new file mode 100644 index 0000000..28fb03e --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/AccidentDto.java @@ -0,0 +1,4 @@ +package com.example.externalinfoservice.dto; + +public class AccidentDto { +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/AccidentPushScheduler.java b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/AccidentPushScheduler.java new file mode 100644 index 0000000..b2c48d1 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/AccidentPushScheduler.java @@ -0,0 +1,21 @@ +package com.example.externalinfoservice.scheduler; + +import com.example.externalinfoservice.controller.AccidentEsController; +import com.example.externalinfoservice.service.AccidentEsService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AccidentPushScheduler { + + private final AccidentEsService accidentEsService; + private final AccidentEsController accidentSseController; + + @Scheduled(fixedRate = 300_000) // 5분마다 실행 + public void pushAccidentsToClients() { + var accidentList = accidentEsService.getAllAccidentsFromES(); + accidentSseController.sendToClients(accidentList); + } +} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/PushScheduler.java b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/PushScheduler.java index d9cc232..2e56b4b 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/PushScheduler.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/PushScheduler.java @@ -1,6 +1,7 @@ package com.example.externalinfoservice.scheduler; import com.example.externalinfoservice.controller.StreamController; +import com.example.externalinfoservice.service.AccidentEsService; import com.example.externalinfoservice.service.ESRoadService; import com.example.externalinfoservice.service.ParkEsService; import com.example.externalinfoservice.service.WeatherEsService; @@ -16,6 +17,8 @@ public class PushScheduler { private final ESRoadService roadService; private final ParkEsService parkEsService; private final StreamController streamController; + private final AccidentEsService accidentEsService; + // 5분마다 날씨 데이터를 SSE 클라이언트에 push @Scheduled(fixedRate = 300_000) // 5분 = 300초 @@ -23,7 +26,8 @@ public void pushWeatherToClients() { var weatherList = weatherEsService.getAllWeatherFromES(); var trafficList = roadService.getTrafficData(); var parkList = parkEsService.getAllParkFromES(); - streamController.sendToClients(weatherList, trafficList, parkList); + var accidentList = accidentEsService.getAllAccidentsFromES(); + streamController.sendToClients(weatherList, trafficList, parkList,accidentList); } } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java new file mode 100644 index 0000000..548d86e --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java @@ -0,0 +1,105 @@ +package com.example.externalinfoservice.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.apache.kafka.common.errors.InvalidRequestException; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class AccidentEsService { + + private static final RestTemplate restTemplate = new RestTemplate(); + + public static JsonNode getAccidentData() { + try { + String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String apiUrl = String.format("http://elasticsearch.seoultravel.life/seoul_citydata_accident_%s/_search", today); + + String jsonBody = "{\n" + + " \"size\": 0,\n" + + " \"aggs\": {\n" + + " \"by_area\": {\n" + + " \"terms\": {\n" + + " \"field\": \"accident.area_nm\",\n" + + " \"size\": 1000\n" + + " },\n" + + " \"aggs\": {\n" + + " \"latest_hit\": {\n" + + " \"top_hits\": {\n" + + " \"size\": 1,\n" + + " \"sort\": [\n" + + " {\n" + + " \"accident.get_time\": {\n" + + " \"order\": \"desc\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(jsonBody, headers); + + ResponseEntity response = restTemplate.postForEntity(apiUrl, requestEntity, String.class); + + String body = response.getBody(); + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(body); + + JsonNode buckets = root + .path("aggregations") + .path("by_area") + .path("buckets"); + + List accidentList = new ArrayList<>(); + for (JsonNode bucket : buckets) { + JsonNode latestHit = bucket.path("latest_hit").path("hits").path("hits").get(0); + JsonNode accident = latestHit.path("_source").path("accident"); + accidentList.add(accident); + } + + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.valueToTree(accidentList); + + } catch (InvalidRequestException e) { + throw new InvalidRequestException(e.getMessage(), e); + } catch (Exception e) { + System.out.println(e); + throw new RuntimeException("예상치 못한 오류", e); + } + } + + public List> getAllAccidentsFromES() { + try { + JsonNode accidentArray = getAccidentData(); + List> list = new ArrayList<>(); + ObjectMapper mapper = new ObjectMapper(); + + for (JsonNode node : accidentArray) { + Map map = mapper.convertValue(node, Map.class); + list.add(map); + } + return list; + } catch (Exception e) { + throw new RuntimeException("사고 데이터 변환 실패", e); + } + } +} diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java index c30a866..b760a7c 100644 --- a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java +++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java @@ -30,7 +30,7 @@ public List saveRestaurantsNearbyArea(Area area) { int savedCount = 0; // 저장한 음식점 수 while (!isEnd) { - // ✅ Kakao API 호출 시 경도(lon), 위도(lat) 순서로 전달 + // Kakao API 호출 시 경도(lon), 위도(lat) 순서로 전달 JsonNode response = kakaoMapClient.searchRestaurantsByCategory(area.getLon(), area.getLat(), page); if (response.has("documents")) { From 75bcece77c8dd1318c3636a95843aefe7731d5b1 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 14 May 2025 09:43:57 +0900 Subject: [PATCH 298/361] =?UTF-8?q?Refactoring:=20gateway=20=ED=94=84?= =?UTF-8?q?=EB=A1=A0=ED=8A=B8=20cors=20url=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../externalinfoservice/config/WebConfig.java | 40 ++--- .../controller/ESRoadController.java | 46 +++--- .../controller/ParkEsController.java | 118 +++++++------- .../controller/SseWeatherController.java | 92 +++++------ .../controller/WeatherEsController.java | 60 +++---- .../externalinfoservice/dto/ParkDto.java | 148 +++++++++--------- .../dto/WeatherDtoOriginal.java | 92 +++++------ .../dto/WeatherRequest.java | 16 +- .../externalinfoservice/entity/Area.java | 72 ++++----- .../repository/AreaRepository.java | 24 +-- .../scheduler/WeatherPushScheduler.java | 46 +++--- .../java/com/example/gateway/config/Cors.java | 3 +- 12 files changed, 379 insertions(+), 378 deletions(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/config/WebConfig.java b/external-info-service/src/main/java/com/example/externalinfoservice/config/WebConfig.java index 8197a08..a374aab 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/config/WebConfig.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/config/WebConfig.java @@ -1,20 +1,20 @@ -package com.example.externalinfoservice.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -public class WebConfig implements WebMvcConfigurer { - - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/main/info/weather/stream") - .allowedOrigins("*") - .allowedMethods("GET"); - - registry.addMapping("/main/info/park/stream") - .allowedOrigins("*") - .allowedMethods("GET"); - } -} +//package com.example.externalinfoservice.config; +// +//import org.springframework.context.annotation.Configuration; +//import org.springframework.web.servlet.config.annotation.CorsRegistry; +//import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +// +//@Configuration +//public class WebConfig implements WebMvcConfigurer { +// +// @Override +// public void addCorsMappings(CorsRegistry registry) { +// registry.addMapping("/main/info/weather/stream") +// .allowedOrigins("*") +// .allowedMethods("GET"); +// +// registry.addMapping("/main/info/park/stream") +// .allowedOrigins("*") +// .allowedMethods("GET"); +// } +//} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ESRoadController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ESRoadController.java index d940faf..0167e88 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ESRoadController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ESRoadController.java @@ -1,23 +1,23 @@ -package com.example.externalinfoservice.controller; - -import com.example.externalinfoservice.service.ESRoadService; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - - -@RestController -@RequestMapping("/main/traffic") -@RequiredArgsConstructor -public class ESRoadController { - private final ESRoadService roadService; - - @GetMapping("") - public JsonNode Traffic() { - return roadService.getTrafficData(); - } - - -} +//package com.example.externalinfoservice.controller; +// +//import com.example.externalinfoservice.service.ESRoadService; +//import com.fasterxml.jackson.databind.JsonNode; +//import lombok.RequiredArgsConstructor; +//import org.springframework.web.bind.annotation.GetMapping; +//import org.springframework.web.bind.annotation.RequestMapping; +//import org.springframework.web.bind.annotation.RestController; +// +// +//@RestController +//@RequestMapping("/main/traffic") +//@RequiredArgsConstructor +//public class ESRoadController { +// private final ESRoadService roadService; +// +// @GetMapping("") +// public JsonNode Traffic() { +// return roadService.getTrafficData(); +// } +// +// +//} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkEsController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkEsController.java index 7764451..9d2c422 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkEsController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkEsController.java @@ -1,61 +1,61 @@ -package com.example.externalinfoservice.controller; - -import com.example.externalinfoservice.service.ParkEsService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/main/info/park") -public class ParkEsController { - - private final ParkEsService parkEsService; - private final List emitters = new CopyOnWriteArrayList<>(); - - // REST API 엔드포인트 - 일반 조회 - @GetMapping(value = {"/{areaId}", ""}, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getPark(@PathVariable(name = "areaId", required = false) String areaId) { - if (areaId == null || areaId.isBlank()) { - List> allPark = parkEsService.getAllParkFromES(); - return ResponseEntity.ok(allPark); - } - - Map park = parkEsService.getParkFromES(areaId); - return park != null ? ResponseEntity.ok(park) : ResponseEntity.notFound().build(); - } - - // SSE 스트림 엔드포인트 - @GetMapping("/stream") - public SseEmitter streamPark() { - SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음 - emitters.add(emitter); - - emitter.onCompletion(() -> emitters.remove(emitter)); - emitter.onTimeout(() -> emitters.remove(emitter)); - emitter.onError((e) -> emitters.remove(emitter)); - - return emitter; - } - -// // 주기적으로 클라이언트에게 push -// public void sendToClients(List> parkList) { -// for (SseEmitter emitter : emitters) { -// try { -// emitter.send(SseEmitter.event() -// .name("park-update") -// .data(parkList)); -// } catch (IOException e) { -// emitter.completeWithError(e); -// emitters.remove(emitter); -// } +//package com.example.externalinfoservice.controller; +// +//import com.example.externalinfoservice.service.ParkEsService; +//import lombok.RequiredArgsConstructor; +//import org.springframework.http.MediaType; +//import org.springframework.http.ResponseEntity; +//import org.springframework.web.bind.annotation.*; +//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +// +//import java.io.IOException; +//import java.util.List; +//import java.util.Map; +//import java.util.concurrent.CopyOnWriteArrayList; +// +//@RestController +//@RequiredArgsConstructor +//@RequestMapping("/main/info/park") +//public class ParkEsController { +// +// private final ParkEsService parkEsService; +// private final List emitters = new CopyOnWriteArrayList<>(); +// +// // REST API 엔드포인트 - 일반 조회 +// @GetMapping(value = {"/{areaId}", ""}, produces = MediaType.APPLICATION_JSON_VALUE) +// public ResponseEntity getPark(@PathVariable(name = "areaId", required = false) String areaId) { +// if (areaId == null || areaId.isBlank()) { +// List> allPark = parkEsService.getAllParkFromES(); +// return ResponseEntity.ok(allPark); // } +// +// Map park = parkEsService.getParkFromES(areaId); +// return park != null ? ResponseEntity.ok(park) : ResponseEntity.notFound().build(); // } -} \ No newline at end of file +// +// // SSE 스트림 엔드포인트 +// @GetMapping("/stream") +// public SseEmitter streamPark() { +// SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음 +// emitters.add(emitter); +// +// emitter.onCompletion(() -> emitters.remove(emitter)); +// emitter.onTimeout(() -> emitters.remove(emitter)); +// emitter.onError((e) -> emitters.remove(emitter)); +// +// return emitter; +// } +// +//// // 주기적으로 클라이언트에게 push +//// public void sendToClients(List> parkList) { +//// for (SseEmitter emitter : emitters) { +//// try { +//// emitter.send(SseEmitter.event() +//// .name("park-update") +//// .data(parkList)); +//// } catch (IOException e) { +//// emitter.completeWithError(e); +//// emitters.remove(emitter); +//// } +//// } +//// } +//} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java index 4ed3585..b83c0ad 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java @@ -1,46 +1,46 @@ -package com.example.externalinfoservice.controller; - -import com.example.externalinfoservice.service.WeatherEsService; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -import java.io.IOException; -import java.util.*; -import java.util.concurrent.CopyOnWriteArrayList; - - -@RestController -@RequiredArgsConstructor -@RequestMapping("/main/info/weather") -public class SseWeatherController { - - private final WeatherEsService weatherEsService; - private final List emitters = new CopyOnWriteArrayList<>(); - - @GetMapping("/stream") - public SseEmitter streamWeather() { - SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음 - emitters.add(emitter); - - emitter.onCompletion(() -> emitters.remove(emitter)); - emitter.onTimeout(() -> emitters.remove(emitter)); - emitter.onError((e) -> emitters.remove(emitter)); - - return emitter; - } - - // 주기적으로 클라이언트에게 push - public void sendToClients(List> weatherList) { - for (SseEmitter emitter : emitters) { - try { - emitter.send(SseEmitter.event() - .name("weather-update") - .data(weatherList)); - } catch (IOException e) { - emitter.completeWithError(e); - emitters.remove(emitter); - } - } - } -} \ No newline at end of file +//package com.example.externalinfoservice.controller; +// +//import com.example.externalinfoservice.service.WeatherEsService; +//import lombok.RequiredArgsConstructor; +//import org.springframework.web.bind.annotation.*; +//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +// +//import java.io.IOException; +//import java.util.*; +//import java.util.concurrent.CopyOnWriteArrayList; +// +// +//@RestController +//@RequiredArgsConstructor +//@RequestMapping("/main/info/weather") +//public class SseWeatherController { +// +// private final WeatherEsService weatherEsService; +// private final List emitters = new CopyOnWriteArrayList<>(); +// +// @GetMapping("/stream") +// public SseEmitter streamWeather() { +// SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음 +// emitters.add(emitter); +// +// emitter.onCompletion(() -> emitters.remove(emitter)); +// emitter.onTimeout(() -> emitters.remove(emitter)); +// emitter.onError((e) -> emitters.remove(emitter)); +// +// return emitter; +// } +// +// // 주기적으로 클라이언트에게 push +// public void sendToClients(List> weatherList) { +// for (SseEmitter emitter : emitters) { +// try { +// emitter.send(SseEmitter.event() +// .name("weather-update") +// .data(weatherList)); +// } catch (IOException e) { +// emitter.completeWithError(e); +// emitters.remove(emitter); +// } +// } +// } +//} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java index 4fd1553..58dcc39 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java @@ -1,30 +1,30 @@ -package com.example.externalinfoservice.controller; - -import com.example.externalinfoservice.service.WeatherEsService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.Map; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/main/info/weather") -public class WeatherEsController { - - - private final WeatherEsService weatherEsService; - - @GetMapping(value = {"/{placeCode}", ""}, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getWeather(@PathVariable(name = "placeCode", required = false) String placeCode) { - if (placeCode == null || placeCode.isBlank()) { - List> allWeather = weatherEsService.getAllWeatherFromES(); - return ResponseEntity.ok(allWeather); - } - - Map weather = weatherEsService.getWeatherFromES(placeCode); - return weather != null ? ResponseEntity.ok(weather) : ResponseEntity.notFound().build(); - } -} \ No newline at end of file +//package com.example.externalinfoservice.controller; +// +//import com.example.externalinfoservice.service.WeatherEsService; +//import lombok.RequiredArgsConstructor; +//import org.springframework.http.MediaType; +//import org.springframework.http.ResponseEntity; +//import org.springframework.web.bind.annotation.*; +// +//import java.util.List; +//import java.util.Map; +// +//@RestController +//@RequiredArgsConstructor +//@RequestMapping("/main/info/weather") +//public class WeatherEsController { +// +// +// private final WeatherEsService weatherEsService; +// +// @GetMapping(value = {"/{placeCode}", ""}, produces = MediaType.APPLICATION_JSON_VALUE) +// public ResponseEntity getWeather(@PathVariable(name = "placeCode", required = false) String placeCode) { +// if (placeCode == null || placeCode.isBlank()) { +// List> allWeather = weatherEsService.getAllWeatherFromES(); +// return ResponseEntity.ok(allWeather); +// } +// +// Map weather = weatherEsService.getWeatherFromES(placeCode); +// return weather != null ? ResponseEntity.ok(weather) : ResponseEntity.notFound().build(); +// } +//} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java index 1deb47b..d8d0690 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/ParkDto.java @@ -1,74 +1,74 @@ -package com.example.externalinfoservice.dto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import jakarta.xml.bind.annotation.*; -import lombok.Data; - -import java.util.List; - -@Data -@XmlRootElement(name = "SeoulRtd.citydata") -@XmlAccessorType(XmlAccessType.FIELD) -@JsonIgnoreProperties(ignoreUnknown = true) -public class ParkDto { - - @XmlElement(name = "CITYDATA") - private CityData cityData; - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class CityData { - - @XmlElement(name = "PRK_STTS") - private PrkSttsWrapper prktts; - } - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class PrkSttsWrapper { - - @XmlElement(name = "PRK_STTS") - private List prkStts; - } - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class ParkingStatus { - - @XmlElement(name = "PRK_NM") - private String prk_nm; - - @XmlElement(name = "PRK_CD") - private String prk_cd; - - @XmlElement(name = "PRK_TYPE") - private String prk_type; - - @XmlElement(name = "CPCTY") - private int cpcty; - - @XmlElement(name = "CUR_PRK_CNT") - private String cur_prk_cnt; - - @XmlElement(name = "CUR_PRK_TIME") - private String cur_prk_time; - - @XmlElement(name = "CUR_PRK_YN") - private String cur_prk_yn; - - @XmlElement(name = "PAY_YN") - private String pay_yn; - - @XmlElement(name = "RATES") - private int rates; - - @XmlElement(name = "TIME_RATES") - private int time_rates; - - @XmlElement(name = "ADD_RATES") - private int add_rates; - - @XmlElement(name = "ADD_TIME_RATES") - private int add_time_rates; - } -} +//package com.example.externalinfoservice.dto; +// +//import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +//import jakarta.xml.bind.annotation.*; +//import lombok.Data; +// +//import java.util.List; +// +//@Data +//@XmlRootElement(name = "SeoulRtd.citydata") +//@XmlAccessorType(XmlAccessType.FIELD) +//@JsonIgnoreProperties(ignoreUnknown = true) +//public class ParkDto { +// +// @XmlElement(name = "CITYDATA") +// private CityData cityData; +// +// @Data +// @XmlAccessorType(XmlAccessType.FIELD) +// public static class CityData { +// +// @XmlElement(name = "PRK_STTS") +// private PrkSttsWrapper prktts; +// } +// +// @Data +// @XmlAccessorType(XmlAccessType.FIELD) +// public static class PrkSttsWrapper { +// +// @XmlElement(name = "PRK_STTS") +// private List prkStts; +// } +// +// @Data +// @XmlAccessorType(XmlAccessType.FIELD) +// public static class ParkingStatus { +// +// @XmlElement(name = "PRK_NM") +// private String prk_nm; +// +// @XmlElement(name = "PRK_CD") +// private String prk_cd; +// +// @XmlElement(name = "PRK_TYPE") +// private String prk_type; +// +// @XmlElement(name = "CPCTY") +// private int cpcty; +// +// @XmlElement(name = "CUR_PRK_CNT") +// private String cur_prk_cnt; +// +// @XmlElement(name = "CUR_PRK_TIME") +// private String cur_prk_time; +// +// @XmlElement(name = "CUR_PRK_YN") +// private String cur_prk_yn; +// +// @XmlElement(name = "PAY_YN") +// private String pay_yn; +// +// @XmlElement(name = "RATES") +// private int rates; +// +// @XmlElement(name = "TIME_RATES") +// private int time_rates; +// +// @XmlElement(name = "ADD_RATES") +// private int add_rates; +// +// @XmlElement(name = "ADD_TIME_RATES") +// private int add_time_rates; +// } +//} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDtoOriginal.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDtoOriginal.java index 02ac271..c483e8b 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDtoOriginal.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherDtoOriginal.java @@ -1,46 +1,46 @@ -package com.example.externalinfoservice.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.xml.bind.annotation.XmlAccessType; -import jakarta.xml.bind.annotation.XmlAccessorType; -import jakarta.xml.bind.annotation.XmlElement; -import jakarta.xml.bind.annotation.XmlRootElement; -import lombok.Data; - -@Data -@XmlRootElement(name = "SeoulRtd.citydata") -@XmlAccessorType(XmlAccessType.FIELD) -public class WeatherDtoOriginal { - @XmlElement(name = "WEATHER_STTS") - @JsonProperty("weather_status") - private WeatherStatus weatherStatus; - - @Data - @XmlAccessorType(XmlAccessType.FIELD) - public static class WeatherStatus { - @XmlElement(name = "TEMP") - @JsonProperty("temperature") - private Double temperature; - - - @XmlElement(name = "PRECIPITATION") - @JsonProperty("precipitation") - private Double precipitation; - - @XmlElement(name = "PRECPT_TYPE") - @JsonProperty("precipitation_type") - private String precipitationType; - - @XmlElement(name = "PM10") - @JsonProperty("pm10") - private Integer pm10; - - @XmlElement(name = "FCST24HOURS") - @JsonProperty("forecast24hours") - private String forecast24hours; - - @XmlElement(name = "SKY_STTS") - @JsonProperty("sky_status") - private String skyStatus; - } -} \ No newline at end of file +//package com.example.externalinfoservice.dto; +// +//import com.fasterxml.jackson.annotation.JsonProperty; +//import jakarta.xml.bind.annotation.XmlAccessType; +//import jakarta.xml.bind.annotation.XmlAccessorType; +//import jakarta.xml.bind.annotation.XmlElement; +//import jakarta.xml.bind.annotation.XmlRootElement; +//import lombok.Data; +// +//@Data +//@XmlRootElement(name = "SeoulRtd.citydata") +//@XmlAccessorType(XmlAccessType.FIELD) +//public class WeatherDtoOriginal { +// @XmlElement(name = "WEATHER_STTS") +// @JsonProperty("weather_status") +// private WeatherStatus weatherStatus; +// +// @Data +// @XmlAccessorType(XmlAccessType.FIELD) +// public static class WeatherStatus { +// @XmlElement(name = "TEMP") +// @JsonProperty("temperature") +// private Double temperature; +// +// +// @XmlElement(name = "PRECIPITATION") +// @JsonProperty("precipitation") +// private Double precipitation; +// +// @XmlElement(name = "PRECPT_TYPE") +// @JsonProperty("precipitation_type") +// private String precipitationType; +// +// @XmlElement(name = "PM10") +// @JsonProperty("pm10") +// private Integer pm10; +// +// @XmlElement(name = "FCST24HOURS") +// @JsonProperty("forecast24hours") +// private String forecast24hours; +// +// @XmlElement(name = "SKY_STTS") +// @JsonProperty("sky_status") +// private String skyStatus; +// } +//} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherRequest.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherRequest.java index f9aab81..23c7ac6 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherRequest.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/WeatherRequest.java @@ -1,8 +1,8 @@ -package com.example.externalinfoservice.dto; - -import lombok.Data; - -@Data -public class WeatherRequest { - private String area; -} +//package com.example.externalinfoservice.dto; +// +//import lombok.Data; +// +//@Data +//public class WeatherRequest { +// private String area; +//} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/entity/Area.java b/external-info-service/src/main/java/com/example/externalinfoservice/entity/Area.java index 7cc4bc2..2669175 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/entity/Area.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/entity/Area.java @@ -1,36 +1,36 @@ -package com.example.externalinfoservice.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import lombok.Getter; -import lombok.Setter; - -@Entity -@Table(name = "area") -@Getter -@Setter -public class Area { - @Id - @Column(name = "area_id") - private Long areaId; - - @Column(name = "seoul_id") - private String seoulId; - - @Column(name = "name") - private String name; - - @Column(name = "name_eng") - private String nameEng; - - @Column(name = "category") - private String category; - - @Column(name = "lat") - private Double latitude; - - @Column(name = "lon") - private Double longitude; -} \ No newline at end of file +//package com.example.externalinfoservice.entity; +// +//import jakarta.persistence.Column; +//import jakarta.persistence.Entity; +//import jakarta.persistence.Id; +//import jakarta.persistence.Table; +//import lombok.Getter; +//import lombok.Setter; +// +//@Entity +//@Table(name = "area") +//@Getter +//@Setter +//public class Area { +// @Id +// @Column(name = "area_id") +// private Long areaId; +// +// @Column(name = "seoul_id") +// private String seoulId; +// +// @Column(name = "name") +// private String name; +// +// @Column(name = "name_eng") +// private String nameEng; +// +// @Column(name = "category") +// private String category; +// +// @Column(name = "lat") +// private Double latitude; +// +// @Column(name = "lon") +// private Double longitude; +//} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/repository/AreaRepository.java b/external-info-service/src/main/java/com/example/externalinfoservice/repository/AreaRepository.java index db3db0c..223cfa2 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/repository/AreaRepository.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/repository/AreaRepository.java @@ -1,12 +1,12 @@ -package com.example.externalinfoservice.repository; - -import com.example.externalinfoservice.entity.Area; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface AreaRepository extends JpaRepository { - Optional findBySeoulId(String seoulId); -} \ No newline at end of file +//package com.example.externalinfoservice.repository; +// +//import com.example.externalinfoservice.entity.Area; +//import org.springframework.data.jpa.repository.JpaRepository; +//import org.springframework.stereotype.Repository; +// +//import java.util.Optional; +// +//@Repository +//public interface AreaRepository extends JpaRepository { +// Optional findBySeoulId(String seoulId); +//} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java index 2017ea7..74a0aa2 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java @@ -1,23 +1,23 @@ -package com.example.externalinfoservice.scheduler; - -import com.example.externalinfoservice.controller.SseWeatherController; -import com.example.externalinfoservice.service.WeatherEsService; -import lombok.RequiredArgsConstructor; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class WeatherPushScheduler { - - private final WeatherEsService weatherEsService; - private final SseWeatherController sseWeatherController; - - // 5분마다 날씨 데이터를 SSE 클라이언트에 push - @Scheduled(fixedRate = 10_000) // 5분 = 300초 - public void pushWeatherToClients() { - var weatherList = weatherEsService.getAllWeatherFromES(); - sseWeatherController.sendToClients(weatherList); - } - -} +//package com.example.externalinfoservice.scheduler; +// +//import com.example.externalinfoservice.controller.SseWeatherController; +//import com.example.externalinfoservice.service.WeatherEsService; +//import lombok.RequiredArgsConstructor; +//import org.springframework.scheduling.annotation.Scheduled; +//import org.springframework.stereotype.Component; +// +//@Component +//@RequiredArgsConstructor +//public class WeatherPushScheduler { +// +// private final WeatherEsService weatherEsService; +// private final SseWeatherController sseWeatherController; +// +// // 5분마다 날씨 데이터를 SSE 클라이언트에 push +// @Scheduled(fixedRate = 10_000) // 5분 = 300초 +// public void pushWeatherToClients() { +// var weatherList = weatherEsService.getAllWeatherFromES(); +// sseWeatherController.sendToClients(weatherList); +// } +// +//} diff --git a/gateway/src/main/java/com/example/gateway/config/Cors.java b/gateway/src/main/java/com/example/gateway/config/Cors.java index def3bbf..b9b41e4 100644 --- a/gateway/src/main/java/com/example/gateway/config/Cors.java +++ b/gateway/src/main/java/com/example/gateway/config/Cors.java @@ -19,7 +19,8 @@ public CorsWebFilter corsWebFilter() { "http://localhost:5173", "http://192.168.0.186:5173", "http://58.127.241.84:5173", - "http://map.seoultravel.life" + "http://map.seoultravel.life", + "http://www.seoultravel.life" )); config.setAllowedHeaders(List.of("Origin", "Content-Type", "Accept", "Authorization")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); From fd26995081990ec13e31d78ddc6f0f2236d49547 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 14 May 2025 09:51:45 +0900 Subject: [PATCH 299/361] =?UTF-8?q?=EC=82=AC=EA=B3=A0=20=EC=8B=A4=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExternalInfoServiceApplication.java | 4 +- .../controller/AccidentEsController.java | 50 +++++++++ .../controller/ParkingControllerOriginal.java | 19 ---- .../controller/StreamController.java | 31 +++--- .../controller/WeatherControllerOriginal.java | 28 ----- .../scheduler/AccidentPushScheduler.java | 21 ++++ .../scheduler/PushScheduler.java | 8 +- .../service/AccidentEsService.java | 105 ++++++++++++++++++ 8 files changed, 198 insertions(+), 68 deletions(-) create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/AccidentEsController.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingControllerOriginal.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherControllerOriginal.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/scheduler/AccidentPushScheduler.java create mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java index b077f1c..6c19e42 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/ExternalInfoServiceApplication.java @@ -9,7 +9,9 @@ public class ExternalInfoServiceApplication { public static void main(String[] args) { + SpringApplication.run(ExternalInfoServiceApplication.class, args); + } -} +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/AccidentEsController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/AccidentEsController.java new file mode 100644 index 0000000..7fbaef7 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/AccidentEsController.java @@ -0,0 +1,50 @@ +package com.example.externalinfoservice.controller; + +import com.example.externalinfoservice.service.AccidentEsService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +@RestController +@RequiredArgsConstructor +@RequestMapping("main/accident") +public class AccidentEsController { + + private final AccidentEsService accidentEsService; + private final List emitters = new CopyOnWriteArrayList<>(); + + @GetMapping("") + public SseEmitter streamAccident() { + SseEmitter emitter = new SseEmitter(0L); + emitters.add(emitter); + + emitter.onCompletion(() -> emitters.remove(emitter)); + emitter.onTimeout(() -> emitters.remove(emitter)); + emitter.onError((e) -> emitters.remove(emitter)); + + List> accidentList = accidentEsService.getAllAccidentsFromES(); + sendToClients(accidentList); + + return emitter; + } + + public void sendToClients(List> accidentList) { + for (SseEmitter emitter : emitters) { + try { + emitter.send(SseEmitter.event() + .name("accident-alert") + .data(accidentList)); + } catch (IOException e) { + emitter.completeWithError(e); + emitters.remove(emitter); + } + } + } +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingControllerOriginal.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingControllerOriginal.java deleted file mode 100644 index e9f096d..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkingControllerOriginal.java +++ /dev/null @@ -1,19 +0,0 @@ -//package com.example.externalinfoservice.controller; -// -//import com.example.externalinfoservice.service.ParkServiceOriginal; -//import lombok.RequiredArgsConstructor; -//import org.springframework.http.MediaType; -//import org.springframework.web.bind.annotation.*; -// -//@RestController -//@RequestMapping("/parking") -//@RequiredArgsConstructor -//public class ParkingControllerOriginal { -// -// private final ParkServiceOriginal parkServiceOriginal; -// -// @GetMapping(value = "/{areaId}", produces = MediaType.APPLICATION_JSON_VALUE) -// public String getParking(@PathVariable String areaId) { -// return parkServiceOriginal.getParkingData(areaId); -// } -//} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java index 13b13c0..60ada1e 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java @@ -1,11 +1,11 @@ package com.example.externalinfoservice.controller; +import com.example.externalinfoservice.service.AccidentEsService; import com.example.externalinfoservice.service.ESRoadService; import com.example.externalinfoservice.service.ParkEsService; import com.example.externalinfoservice.service.WeatherEsService; import com.fasterxml.jackson.databind.JsonNode; import lombok.RequiredArgsConstructor; -import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -25,8 +25,9 @@ public class StreamController { private final WeatherEsService weatherEsService; private final ESRoadService roadService; private final ParkEsService parkEsService; + private final AccidentEsService accidentEsService; - @GetMapping(value="/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + @GetMapping("/stream") public SseEmitter streamWeather() { SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음 emitters.add(emitter); @@ -40,25 +41,17 @@ public SseEmitter streamWeather() { var weatherList = weatherEsService.getAllWeatherFromES(); var trafficList = roadService.getTrafficData(); var parkList = parkEsService.getAllParkFromES(); - emitter.send(SseEmitter.event() - .name("weather-update") - .data(weatherList)); - emitter.send(SseEmitter.event() - .name("traffic-update") - .data(trafficList)); - emitter.send(SseEmitter.event() - .name("park-update") - .data(parkList)); - }catch (IOException | IllegalStateException e) { - emitter.completeWithError(e); - emitters.remove(emitter); + var accidentList = accidentEsService.getAllAccidentsFromES(); + sendToClients(weatherList, trafficList, parkList, accidentList); + } catch (Exception e) { + throw new RuntimeException(e); } return emitter; } // 주기적으로 클라이언트에게 push - public void sendToClients(List> weatherList, JsonNode trafficList, List> parkList) { + public void sendToClients(List> weatherList, JsonNode trafficList, List> parkList, List> accidentList) { for (SseEmitter emitter : emitters) { try { emitter.send(SseEmitter.event() @@ -70,11 +63,13 @@ public void sendToClients(List> weatherList, JsonNode traffi emitter.send(SseEmitter.event() .name("park-update") .data(parkList)); - - } catch (IOException | IllegalStateException e) { + emitter.send(SseEmitter.event() + .name("accident-alert") + .data(accidentList)); + } catch (IOException e) { emitter.completeWithError(e); emitters.remove(emitter); } } } -} +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherControllerOriginal.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherControllerOriginal.java deleted file mode 100644 index c04b7bc..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherControllerOriginal.java +++ /dev/null @@ -1,28 +0,0 @@ -//package com.example.externalinfoservice.controller; -// -//import com.example.externalinfoservice.service.WeatherService; -//import lombok.RequiredArgsConstructor; -//import org.springframework.http.MediaType; -//import org.springframework.http.ResponseEntity; -//import org.springframework.web.bind.annotation.GetMapping; -//import org.springframework.web.bind.annotation.PathVariable; -//import org.springframework.web.bind.annotation.RequestMapping; -//import org.springframework.web.bind.annotation.RestController; -// -//@RestController -//@RequestMapping("/weather") -//@RequiredArgsConstructor -//public class WeatherControllerOriginal { -// private final WeatherService weatherService; -// -// @GetMapping("/{place-code}") -// public String getWeatherData(@PathVariable("place-code") String placeCode) { -// return weatherService.getWeatherData(placeCode); -// } -// -// @GetMapping(value = "/api/{place-code}", produces = MediaType.APPLICATION_JSON_VALUE) -// public ResponseEntity getWeatherDataApi(@PathVariable("place-code") String placeCode) { -// String jsonData = weatherService.getWeatherData(placeCode); -// return ResponseEntity.ok(jsonData); -// } -//} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/AccidentPushScheduler.java b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/AccidentPushScheduler.java new file mode 100644 index 0000000..568a43d --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/AccidentPushScheduler.java @@ -0,0 +1,21 @@ +package com.example.externalinfoservice.scheduler; + +import com.example.externalinfoservice.controller.AccidentEsController; +import com.example.externalinfoservice.service.AccidentEsService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AccidentPushScheduler { + + private final AccidentEsService accidentEsService; + private final AccidentEsController accidentSseController; + + @Scheduled(fixedRate = 300_000) // 5분마다 실행 + public void pushAccidentsToClients() { + var accidentList = accidentEsService.getAllAccidentsFromES(); + accidentSseController.sendToClients(accidentList); + } +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/PushScheduler.java b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/PushScheduler.java index d9cc232..6588156 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/PushScheduler.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/PushScheduler.java @@ -1,6 +1,7 @@ package com.example.externalinfoservice.scheduler; import com.example.externalinfoservice.controller.StreamController; +import com.example.externalinfoservice.service.AccidentEsService; import com.example.externalinfoservice.service.ESRoadService; import com.example.externalinfoservice.service.ParkEsService; import com.example.externalinfoservice.service.WeatherEsService; @@ -16,6 +17,8 @@ public class PushScheduler { private final ESRoadService roadService; private final ParkEsService parkEsService; private final StreamController streamController; + private final AccidentEsService accidentEsService; + // 5분마다 날씨 데이터를 SSE 클라이언트에 push @Scheduled(fixedRate = 300_000) // 5분 = 300초 @@ -23,7 +26,8 @@ public void pushWeatherToClients() { var weatherList = weatherEsService.getAllWeatherFromES(); var trafficList = roadService.getTrafficData(); var parkList = parkEsService.getAllParkFromES(); - streamController.sendToClients(weatherList, trafficList, parkList); + var accidentList = accidentEsService.getAllAccidentsFromES(); + streamController.sendToClients(weatherList, trafficList, parkList,accidentList); } -} +} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java new file mode 100644 index 0000000..7d5d262 --- /dev/null +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java @@ -0,0 +1,105 @@ +package com.example.externalinfoservice.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.apache.kafka.common.errors.InvalidRequestException; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class AccidentEsService { + + private static final RestTemplate restTemplate = new RestTemplate(); + + public static JsonNode getAccidentData() { + try { + String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + String apiUrl = String.format("http://elasticsearch.seoultravel.life/seoul_citydata_accident_%s/_search", today); + + String jsonBody = "{\n" + + " \"size\": 0,\n" + + " \"aggs\": {\n" + + " \"by_area\": {\n" + + " \"terms\": {\n" + + " \"field\": \"accident.area_nm\",\n" + + " \"size\": 1000\n" + + " },\n" + + " \"aggs\": {\n" + + " \"latest_hit\": {\n" + + " \"top_hits\": {\n" + + " \"size\": 1,\n" + + " \"sort\": [\n" + + " {\n" + + " \"accident.get_time\": {\n" + + " \"order\": \"desc\"\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(jsonBody, headers); + + ResponseEntity response = restTemplate.postForEntity(apiUrl, requestEntity, String.class); + + String body = response.getBody(); + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(body); + + JsonNode buckets = root + .path("aggregations") + .path("by_area") + .path("buckets"); + + List accidentList = new ArrayList<>(); + for (JsonNode bucket : buckets) { + JsonNode latestHit = bucket.path("latest_hit").path("hits").path("hits").get(0); + JsonNode accident = latestHit.path("_source").path("accident"); + accidentList.add(accident); + } + + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.valueToTree(accidentList); + + } catch (InvalidRequestException e) { + throw new InvalidRequestException(e.getMessage(), e); + } catch (Exception e) { + System.out.println(e); + throw new RuntimeException("예상치 못한 오류", e); + } + } + + public List> getAllAccidentsFromES() { + try { + JsonNode accidentArray = getAccidentData(); + List> list = new ArrayList<>(); + ObjectMapper mapper = new ObjectMapper(); + + for (JsonNode node : accidentArray) { + Map map = mapper.convertValue(node, Map.class); + list.add(map); + } + return list; + } catch (Exception e) { + throw new RuntimeException("사고 데이터 변환 실패", e); + } + } +} \ No newline at end of file From 4227eb5896fd77f697b65b2179c250aca7bf4133 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 14 May 2025 09:54:09 +0900 Subject: [PATCH 300/361] =?UTF-8?q?application=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway/.gitignore | 2 + gateway/src/main/resources/application.txt | 42 -------------- gateway/src/main/resources/application.yml | 64 ---------------------- 3 files changed, 2 insertions(+), 106 deletions(-) delete mode 100644 gateway/src/main/resources/application.txt delete mode 100644 gateway/src/main/resources/application.yml diff --git a/gateway/.gitignore b/gateway/.gitignore index bc06ee7..15c7cd6 100644 --- a/gateway/.gitignore +++ b/gateway/.gitignore @@ -6,6 +6,8 @@ build/ !**/src/test/**/build/ src/main/resources/application.properties src/main/resources/application.yml +src/main/resources/application.txt + .env ### STS ### diff --git a/gateway/src/main/resources/application.txt b/gateway/src/main/resources/application.txt deleted file mode 100644 index 8332238..0000000 --- a/gateway/src/main/resources/application.txt +++ /dev/null @@ -1,42 +0,0 @@ -spring.application.name=gateway -server.port=8080 - -spring.cloud.gateway.routes[0].id=congestion-service -spring.cloud.gateway.routes[0].uri=http://localhost:8081 -spring.cloud.gateway.routes[0].predicates[0]=Path=/control/congestion/** -spring.cloud.gateway.routes[0].filters[0]=RewritePath=/control/congestion/(?.*), /$\{segment} - -spring.cloud.gateway.routes[1].id=external-service -spring.cloud.gateway.routes[1].uri=http://localhost:8081 -spring.cloud.gateway.routes[1].predicates[0]=Path=/control/external/** -spring.cloud.gateway.routes[1].filters[0]=RewritePath=/control/external/(?.*), /$\{segment} - - -spring.cloud.gateway.routes[2].id=place-service -spring.cloud.gateway.routes[2].uri=http://localhost:8082 -spring.cloud.gateway.routes[2].predicates[0]=Path=/place/** -spring.cloud.gateway.routes[2].filters[0]=RewritePath=/place/(?.*), /$\{segment} - -spring.cloud.gateway.routes[3].id=user-service -spring.cloud.gateway.routes[3].uri=http://localhost:8083 -spring.cloud.gateway.routes[3].predicates[0]=Path=/user/** -spring.cloud.gateway.routes[3].filters[0]=RewritePath=/user/(?.*), /$\{segment} - - -# ?? ?? -logging.level.root=DEBUG -logging.level.org.springframework.cloud.gateway=DEBUG -logging.level.reactor.netty.http.client=DEBUG -logging.level.org.springframework.web.server.adapter.HttpWebHandlerAdapter=DEBUG - - -# CORS ?? -#spring.cloud.gateway.default-filters[0].name=CORS -#spring.cloud.gateway.default-filters[0].args.allowedOrigins=http://localhost:5173,http://192.168.0.186:5173,http://58.127.241.84:5173 -#spring.cloud.gateway.default-filters[0].args.allowedMethods=* -#spring.cloud.gateway.default-filters[0].args.allowedHeaders=* - -spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedOrigins=http://localhost:5173,http://192.168.0.186:5173,http://58.127.241.84:5173 -spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedMethods=GET,POST,PUT,DELETE,OPTIONS -spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedHeaders=* -spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowCredentials=true \ No newline at end of file diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml deleted file mode 100644 index 25976e0..0000000 --- a/gateway/src/main/resources/application.yml +++ /dev/null @@ -1,64 +0,0 @@ -spring: - application: - name: gateway - - cloud: - gateway: - routes: - - id: congestion-service - uri: http://localhost:8081 - predicates: - - Path=/control/congestion/** - filters: - - RewritePath=/control/congestion/(?.*), /${segment} - - - id: external-service - uri: http://localhost:8081 - predicates: - - Path=/control/external/** - filters: - - RewritePath=/control/external/(?.*), /${segment} - - - id: place-service - uri: http://localhost:8082 - predicates: - - Path=/place/** - filters: - - RewritePath=/place/(?.*), /${segment} - - - id: user-service - uri: http://localhost:8083 - predicates: - - Path=/user/** - - Path=/auth/** - filters: - - RewritePath=/user/(?.*), /${segment} - - RewritePath=/auth/(?.*), /${segment} - - globalcors: - corsConfigurations: - '[/**]': - allowedOrigins: - - "http://localhost:5173" - - "http://192.168.0.186:5173" - - "http://58.127.241.84:5173" - - "http://api.seoultravel.life" - allowedMethods: - - GET - - POST - - PUT - - DELETE - - OPTIONS - allowedHeaders: - - Origin - - Content-Type - - Accept - - Authorization - allowCredentials: true - -logging: - level: - root: DEBUG - org.springframework.cloud.gateway: DEBUG - reactor.netty.http.client: DEBUG - org.springframework.web.server.adapter.HttpWebHandlerAdapter: DEBUG From 6dbb5a82efecee86f40d8daf9683a8106232ff87 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 14 May 2025 10:13:08 +0900 Subject: [PATCH 301/361] =?UTF-8?q?=EC=82=AC=EA=B3=A0=20=EC=8B=A4=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/AccidentPushScheduler.java | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/AccidentPushScheduler.java b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/AccidentPushScheduler.java index 568a43d..8af2e12 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/AccidentPushScheduler.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/AccidentPushScheduler.java @@ -1,21 +1,21 @@ -package com.example.externalinfoservice.scheduler; - -import com.example.externalinfoservice.controller.AccidentEsController; -import com.example.externalinfoservice.service.AccidentEsService; -import lombok.RequiredArgsConstructor; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class AccidentPushScheduler { - - private final AccidentEsService accidentEsService; - private final AccidentEsController accidentSseController; - - @Scheduled(fixedRate = 300_000) // 5분마다 실행 - public void pushAccidentsToClients() { - var accidentList = accidentEsService.getAllAccidentsFromES(); - accidentSseController.sendToClients(accidentList); - } -} \ No newline at end of file +//package com.example.externalinfoservice.scheduler; +// +//import com.example.externalinfoservice.controller.AccidentEsController; +//import com.example.externalinfoservice.service.AccidentEsService; +//import lombok.RequiredArgsConstructor; +//import org.springframework.scheduling.annotation.Scheduled; +//import org.springframework.stereotype.Component; +// +//@Component +//@RequiredArgsConstructor +//public class AccidentPushScheduler { +// +// private final AccidentEsService accidentEsService; +// private final AccidentEsController accidentSseController; +// +// @Scheduled(fixedRate = 300_000) // 5분마다 실행 +// public void pushAccidentsToClients() { +// var accidentList = accidentEsService.getAllAccidentsFromES(); +// accidentSseController.sendToClients(accidentList); +// } +//} From 23e518bb61590c2dc29ebd96fa8099f2c1fae5bb Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 14 May 2025 14:07:53 +0900 Subject: [PATCH 302/361] =?UTF-8?q?Refactor:=20congestion-alert=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=20=EB=8D=B0=EC=9D=B4=ED=84=B0=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 --- .../controller/CongestionController.java | 23 +++++++++++++++++++ place-service/build.gradle | 6 ++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java index 8d4478f..48d382f 100644 --- a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java +++ b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java @@ -2,6 +2,8 @@ import com.example.congestionservice.service.CongestionService; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -9,7 +11,9 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; @RestController @@ -30,6 +34,7 @@ public SseEmitter streamCongestion() { emitter.onTimeout(() -> emitters.remove(emitter)); emitter.onError((e) -> emitters.remove(emitter)); + // 초기데이터 투입 try { System.out.println("혼잡도 초기 데이터 푸시 중..."); var congestionList = congestionService.getCongestion(); @@ -38,12 +43,30 @@ public SseEmitter streamCongestion() { .name("congestion-update") .data(congestionList)); + // 혼잡도 알림 전송 위한 로직 + Map currentLevels = new HashMap<>(); + ArrayNode changedList = new ObjectMapper().createArrayNode(); + for (JsonNode area : congestionList) { + String areaName = area.get("area_nm").asText(); + String currentLevel = area.get("area_congest_lvl").asText(); + currentLevels.put(areaName, currentLevel); + if(currentLevel.equals("약간 붐빔") || currentLevel.equals("붐빔")){ + changedList.add(area); + } + } + + emitter.send(SseEmitter.event() + .name("congestion-alert") + .data(changedList)); + System.out.println(".. 혼잡도 초기 데이터 푸시 완료"); + } catch (IOException | IllegalStateException e) { emitter.completeWithError(e); emitters.remove(emitter); } + return emitter; } diff --git a/place-service/build.gradle b/place-service/build.gradle index 1dfc1ee..d1b8b5a 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -24,6 +24,11 @@ repositories { } dependencies { + // redis 추가 + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'com.fasterxml.jackson.core:jackson-databind' + + // JAXB (XML 파싱) implementation 'javax.xml.bind:jaxb-api:2.3.1' // JAXB Runtime (XML 파싱을 위해 필요) @@ -40,7 +45,6 @@ dependencies { implementation 'org.postgresql:postgresql:42.7.2' implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - //implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' From 7cc6549bc516c330a4c45976ae5d2398b5a7319b Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 14 May 2025 14:58:42 +0900 Subject: [PATCH 303/361] =?UTF-8?q?Refactor:=20congestion=20alert=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CongestionController.java | 24 ++++++----- .../scheduler/PushScheduler.java | 41 ++++++++++++++----- user-service/build.gradle | 2 +- 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java index 48d382f..4d70413 100644 --- a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java +++ b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -22,9 +23,6 @@ public class CongestionController { private final List emitters = new CopyOnWriteArrayList<>(); - private final CongestionService congestionService; - - @GetMapping("/congestion") public SseEmitter streamCongestion() { SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음 @@ -37,21 +35,28 @@ public SseEmitter streamCongestion() { // 초기데이터 투입 try { System.out.println("혼잡도 초기 데이터 푸시 중..."); - var congestionList = congestionService.getCongestion(); + var congestionList = CongestionService.getCongestion(); emitter.send(SseEmitter.event() .name("congestion-update") .data(congestionList)); // 혼잡도 알림 전송 위한 로직 - Map currentLevels = new HashMap<>(); - ArrayNode changedList = new ObjectMapper().createArrayNode(); + ObjectMapper mapper = new ObjectMapper(); + ArrayNode changedList = mapper.createArrayNode(); // ArrayNode 생성 + for (JsonNode area : congestionList) { - String areaName = area.get("area_nm").asText(); String currentLevel = area.get("area_congest_lvl").asText(); - currentLevels.put(areaName, currentLevel); + if(currentLevel.equals("약간 붐빔") || currentLevel.equals("붐빔")){ - changedList.add(area); + ObjectNode copy = mapper.createObjectNode(); + area.fieldNames().forEachRemaining(field -> { + if (!"fcst_ppltn".equals(field)) { + copy.set(field, area.get(field)); + } + }); + changedList.add(copy); + } } @@ -66,7 +71,6 @@ public SseEmitter streamCongestion() { emitters.remove(emitter); } - return emitter; } diff --git a/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java b/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java index ef543a7..38eb238 100644 --- a/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java +++ b/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -15,7 +16,6 @@ @Component @RequiredArgsConstructor public class PushScheduler { - private final CongestionService congestionService; private final CongestionController congestionController; @@ -26,13 +26,16 @@ public class PushScheduler { @Scheduled(fixedRate = 300_000) public void push() { System.out.println("혼잡도 푸시 중..."); - var congestionList = congestionService.getCongestion(); + var congestionList = CongestionService.getCongestion(); congestionController.sendToClients(congestionList); // 모든 지역 혼잡도 전송 // 혼잡도 알림 전송 위한 로직 Map currentLevels = new HashMap<>(); - ArrayNode changedList = new ObjectMapper().createArrayNode(); + ObjectMapper mapper = new ObjectMapper(); + // ArrayNode 생성 + ArrayNode changedList = mapper.createArrayNode(); + for (JsonNode area : congestionList) { String areaName = area.get("area_nm").asText(); String currentLevel = area.get("area_congest_lvl").asText(); @@ -42,17 +45,35 @@ public void push() { // 혼잡도 변화 알림 로그 확인 // System.out.println(areaName+"의"+previousLevel+":ㅣㅣ"+currentLevel); - // 혼잡도 알림 전송(조건 : 1,2단계 -> 3,4 단계 or 3단계 -> 4단계) + // 혼잡도 알림 전송(조건 : 1,2단계 -> 3단계 // 4단계) if (previousLevel == null && (currentLevel.equals("약간 붐빔") || currentLevel.equals("붐빔"))) { - changedList.add(area); + ObjectNode copy = mapper.createObjectNode(); + area.fieldNames().forEachRemaining(field -> { + if (!"fcst_ppltn".equals(field)) { + copy.set(field, area.get(field)); + } + }); + changedList.add(copy); } else if (previousLevel != null){ - if ((previousLevel.equals("여유") || previousLevel.equals("보통")) && (currentLevel.equals("약간 붐빔") || currentLevel.equals("붐빔"))) { - changedList.add(area); - }else if(previousLevel.equals("약간 붐빔") && currentLevel.equals("붐빔")){ - changedList.add(area); + if(currentLevel.equals("붐빔")){ + ObjectNode copy = mapper.createObjectNode(); + area.fieldNames().forEachRemaining(field -> { + if (!"fcst_ppltn".equals(field)) { + copy.set(field, area.get(field)); + } + }); + changedList.add(copy); + } + else if ((previousLevel.equals("여유") || previousLevel.equals("보통")) && (currentLevel.equals("약간 붐빔"))) { + ObjectNode copy = mapper.createObjectNode(); + area.fieldNames().forEachRemaining(field -> { + if (!"fcst_ppltn".equals(field)) { + copy.set(field, area.get(field)); + } + }); + changedList.add(copy); } - } } diff --git a/user-service/build.gradle b/user-service/build.gradle index 795f99d..bfb371e 100644 --- a/user-service/build.gradle +++ b/user-service/build.gradle @@ -32,11 +32,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.apache.kafka:kafka-streams' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.session:spring-session-data-redis' implementation 'org.springframework.kafka:spring-kafka' From 16c1da0a3de768bcaf5888d6185c8e8ffa6297ec Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 14 May 2025 15:58:40 +0900 Subject: [PATCH 304/361] =?UTF-8?q?Feat:=20Congestion=20sse=20previouscach?= =?UTF-8?q?e=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/CongestionPreviousCache.java | 22 +++++++++++++++++++ .../controller/CongestionController.java | 10 +++++++++ .../scheduler/PushScheduler.java | 14 ++++++++---- 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 congestion-service/src/main/java/com/example/congestionservice/config/CongestionPreviousCache.java diff --git a/congestion-service/src/main/java/com/example/congestionservice/config/CongestionPreviousCache.java b/congestion-service/src/main/java/com/example/congestionservice/config/CongestionPreviousCache.java new file mode 100644 index 0000000..52c9247 --- /dev/null +++ b/congestion-service/src/main/java/com/example/congestionservice/config/CongestionPreviousCache.java @@ -0,0 +1,22 @@ +package com.example.congestionservice.config; + +import lombok.Getter; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Getter +@Component +public class CongestionPreviousCache { + private final Map previousLevels = new HashMap<>(); + + public void updateLevel(String area, String level) { + previousLevels.put(area, level); + } + + public void clear() { + previousLevels.clear(); + } + +} diff --git a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java index 4d70413..0e3b65a 100644 --- a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java +++ b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java @@ -1,5 +1,6 @@ package com.example.congestionservice.controller; +import com.example.congestionservice.config.CongestionPreviousCache; import com.example.congestionservice.service.CongestionService; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -22,6 +23,7 @@ @RequestMapping("/main") public class CongestionController { private final List emitters = new CopyOnWriteArrayList<>(); + private final CongestionPreviousCache congestionPreviousCache; @GetMapping("/congestion") public SseEmitter streamCongestion() { @@ -32,6 +34,8 @@ public SseEmitter streamCongestion() { emitter.onTimeout(() -> emitters.remove(emitter)); emitter.onError((e) -> emitters.remove(emitter)); + Map previousLevels = congestionPreviousCache.getPreviousLevels(); + // 초기데이터 투입 try { System.out.println("혼잡도 초기 데이터 푸시 중..."); @@ -44,9 +48,13 @@ public SseEmitter streamCongestion() { // 혼잡도 알림 전송 위한 로직 ObjectMapper mapper = new ObjectMapper(); ArrayNode changedList = mapper.createArrayNode(); // ArrayNode 생성 + // previousdata 저장을 위한 데이터 + Map currentLevels = new HashMap<>(); for (JsonNode area : congestionList) { + String areaName = area.get("area_nm").asText(); String currentLevel = area.get("area_congest_lvl").asText(); + currentLevels.put(areaName, currentLevel); if(currentLevel.equals("약간 붐빔") || currentLevel.equals("붐빔")){ ObjectNode copy = mapper.createObjectNode(); @@ -60,6 +68,8 @@ public SseEmitter streamCongestion() { } } + previousLevels.putAll(currentLevels); + emitter.send(SseEmitter.event() .name("congestion-alert") .data(changedList)); diff --git a/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java b/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java index 38eb238..c06bed8 100644 --- a/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java +++ b/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java @@ -1,5 +1,6 @@ package com.example.congestionservice.scheduler; +import com.example.congestionservice.config.CongestionPreviousCache; import com.example.congestionservice.controller.CongestionController; import com.example.congestionservice.service.CongestionService; import com.fasterxml.jackson.databind.JsonNode; @@ -17,14 +18,17 @@ @RequiredArgsConstructor public class PushScheduler { private final CongestionController congestionController; + private final CongestionPreviousCache congestionPreviousCache; // 이전 혼잡도 상태를 저장하는 필드 추가(혼잡도 알림용) - private Map previousLevels = new HashMap<>(); - +// private Map previousLevels = new HashMap<>(); @Scheduled(fixedRate = 300_000) public void push() { + Map previousLevels = congestionPreviousCache.getPreviousLevels(); + + System.out.println("혼잡도 푸시 중..."); var congestionList = CongestionService.getCongestion(); @@ -43,7 +47,7 @@ public void push() { String previousLevel = previousLevels.get(areaName); // 혼잡도 변화 알림 로그 확인 -// System.out.println(areaName+"의"+previousLevel+":ㅣㅣ"+currentLevel); + System.out.println(areaName+"의"+previousLevel+":ㅣㅣ"+currentLevel); // 혼잡도 알림 전송(조건 : 1,2단계 -> 3단계 // 4단계) if (previousLevel == null && (currentLevel.equals("약간 붐빔") || currentLevel.equals("붐빔"))) { @@ -84,7 +88,9 @@ else if ((previousLevel.equals("여유") || previousLevel.equals("보통")) && ( System.out.println("혼잡도 변화 있음 → alert-update로 SSE 전송"); congestionController.sendAlertToClients(changedList); // ✨ 바뀐 것만 보냄 } - previousLevels = currentLevels; + + previousLevels.clear(); + previousLevels.putAll(currentLevels); } } From 8ecc411201757e242246ab3bb7b61c0fc4e6b67b Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Thu, 15 May 2025 09:37:52 +0900 Subject: [PATCH 305/361] =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=B2=98=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AccidentEsService.java | 72 ++++++++----------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java index 7d5d262..b6587a1 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java @@ -21,62 +21,52 @@ @RequiredArgsConstructor public class AccidentEsService { - private static final RestTemplate restTemplate = new RestTemplate(); + private final RestTemplate restTemplate = new RestTemplate(); // 주입 방식으로 대체해도 OK + private final ObjectMapper objectMapper = new ObjectMapper(); - public static JsonNode getAccidentData() { + public JsonNode getAccidentData() { try { String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); String apiUrl = String.format("http://elasticsearch.seoultravel.life/seoul_citydata_accident_%s/_search", today); - String jsonBody = "{\n" + - " \"size\": 0,\n" + - " \"aggs\": {\n" + - " \"by_area\": {\n" + - " \"terms\": {\n" + - " \"field\": \"accident.area_nm\",\n" + - " \"size\": 1000\n" + - " },\n" + - " \"aggs\": {\n" + - " \"latest_hit\": {\n" + - " \"top_hits\": {\n" + - " \"size\": 1,\n" + - " \"sort\": [\n" + - " {\n" + - " \"accident.get_time\": {\n" + - " \"order\": \"desc\"\n" + - " }\n" + - " }\n" + - " ]\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - "}"; + String jsonBody = """ + { + "query": { + "bool": { + "must": [ + { + "range": { + "accident.exp_clr_dt": { + "gt": "now" + } + } + } + ] + } + }, + "collapse": { + "field": "accident.search" + }, + "size": 100 + } + """; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity requestEntity = new HttpEntity<>(jsonBody, headers); ResponseEntity response = restTemplate.postForEntity(apiUrl, requestEntity, String.class); - String body = response.getBody(); - ObjectMapper mapper = new ObjectMapper(); - JsonNode root = mapper.readTree(body); - JsonNode buckets = root - .path("aggregations") - .path("by_area") - .path("buckets"); + JsonNode root = objectMapper.readTree(body); + JsonNode hits = root.path("hits").path("hits"); List accidentList = new ArrayList<>(); - for (JsonNode bucket : buckets) { - JsonNode latestHit = bucket.path("latest_hit").path("hits").path("hits").get(0); - JsonNode accident = latestHit.path("_source").path("accident"); + for (JsonNode hit : hits) { + JsonNode accident = hit.path("_source").path("accident"); accidentList.add(accident); } - ObjectMapper objectMapper = new ObjectMapper(); return objectMapper.valueToTree(accidentList); } catch (InvalidRequestException e) { @@ -91,15 +81,15 @@ public List> getAllAccidentsFromES() { try { JsonNode accidentArray = getAccidentData(); List> list = new ArrayList<>(); - ObjectMapper mapper = new ObjectMapper(); for (JsonNode node : accidentArray) { - Map map = mapper.convertValue(node, Map.class); + Map map = objectMapper.convertValue(node, Map.class); list.add(map); } + return list; } catch (Exception e) { throw new RuntimeException("사고 데이터 변환 실패", e); } } -} \ No newline at end of file +} From b27848ff3ee5ec8527b559717d22ac74f77f17c3 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Thu, 15 May 2025 14:38:33 +0900 Subject: [PATCH 306/361] =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=98=A4=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 --- .../externalinfoservice/controller/StreamController.java | 3 ++- .../com/example/externalinfoservice/dto/AccidentDto.java | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java index 60ada1e..f1fabd8 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java @@ -6,6 +6,7 @@ import com.example.externalinfoservice.service.WeatherEsService; import com.fasterxml.jackson.databind.JsonNode; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -27,7 +28,7 @@ public class StreamController { private final ParkEsService parkEsService; private final AccidentEsService accidentEsService; - @GetMapping("/stream") + @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter streamWeather() { SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음 emitters.add(emitter); diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/dto/AccidentDto.java b/external-info-service/src/main/java/com/example/externalinfoservice/dto/AccidentDto.java index 28fb03e..b35df16 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/dto/AccidentDto.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/dto/AccidentDto.java @@ -1,4 +1,4 @@ -package com.example.externalinfoservice.dto; - -public class AccidentDto { -} +//package com.example.externalinfoservice.dto; +// +//public class AccidentDto { +//} From 126951fca25ef46b811351f69923308bb72c63e7 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Thu, 15 May 2025 15:42:59 +0900 Subject: [PATCH 307/361] =?UTF-8?q?deploy.yml=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 50 ------------------------------------ 1 file changed, 50 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 10b939e..b4ebebb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -155,56 +155,6 @@ jobs: kubectl set image deployment/congestion-service congestion-service=$ECR_REGISTRY/congestion-service:$IMAGE_TAG kubectl rollout status deployment/congestion-service - gateway-build: - runs-on: ubuntu-latest - env: - IMAGE_TAG: 1.${{ github.run_number }} - steps: - - name: Checkout repo - uses: actions/checkout@v4 - - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v2 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} - aws-region: ap-northeast-2 - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - with: - mask-password: true - - - name: 자바 설치 - uses: actions/setup-java@v2 - with: - distribution: 'temurin' - java-version: '17' - - - name: kubeconfig 설정 - run: | - aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster - echo "Step output: ${{ steps.set-ecr.outputs.registry }}" - - name: Build & Push gateway - env: # ecr 업로드 할 때 필요한 변수들 선언 - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # ecr 경로 - run: | - cd gateway - echo "${{ secrets.BACKEND_ENV}}" > .env - gradle bootJar -x test - docker build -t $ECR_REGISTRY/gateway:$IMAGE_TAG . - docker tag $ECR_REGISTRY/gateway:$IMAGE_TAG $ECR_REGISTRY/gateway:latest - - docker push $ECR_REGISTRY/gateway:$IMAGE_TAG - docker push $ECR_REGISTRY/gateway:latest - - echo "태그 확인: gateway=$ECR_REGISTRY/gateway:$IMAGE_TAG" - - kubectl set image deployment/gateway gateway=$ECR_REGISTRY/gateway:$IMAGE_TAG - kubectl rollout status deployment/gateway - - user-service-build: runs-on: ubuntu-latest env: From 1d10f9ad9f67e9250a6984b99a867d5d1af4e82f Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 16 May 2025 11:15:56 +0900 Subject: [PATCH 308/361] =?UTF-8?q?Refactor:=20external=20sse=EC=98=A4?= =?UTF-8?q?=EB=A5=98(=EC=B4=88=EA=B8=B0=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EC=8B=9C)=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ESRoadController.java | 23 ------ .../controller/ParkEsController.java | 61 ---------------- .../controller/SseWeatherController.java | 46 ------------ .../controller/StreamController.java | 37 ++++++++-- .../controller/WeatherEsController.java | 30 -------- .../scheduler/AccidentPushScheduler.java | 22 ------ .../scheduler/WeatherPushScheduler.java | 23 ------ .../service/AccidentEsService.java | 2 +- .../service/ESRoadService.java | 2 +- .../service/ParkEsService.java | 70 +++++++++---------- 10 files changed, 70 insertions(+), 246 deletions(-) delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/ESRoadController.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkEsController.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/scheduler/AccidentPushScheduler.java delete mode 100644 external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ESRoadController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ESRoadController.java deleted file mode 100644 index 0167e88..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ESRoadController.java +++ /dev/null @@ -1,23 +0,0 @@ -//package com.example.externalinfoservice.controller; -// -//import com.example.externalinfoservice.service.ESRoadService; -//import com.fasterxml.jackson.databind.JsonNode; -//import lombok.RequiredArgsConstructor; -//import org.springframework.web.bind.annotation.GetMapping; -//import org.springframework.web.bind.annotation.RequestMapping; -//import org.springframework.web.bind.annotation.RestController; -// -// -//@RestController -//@RequestMapping("/main/traffic") -//@RequiredArgsConstructor -//public class ESRoadController { -// private final ESRoadService roadService; -// -// @GetMapping("") -// public JsonNode Traffic() { -// return roadService.getTrafficData(); -// } -// -// -//} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkEsController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkEsController.java deleted file mode 100644 index 9d2c422..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/ParkEsController.java +++ /dev/null @@ -1,61 +0,0 @@ -//package com.example.externalinfoservice.controller; -// -//import com.example.externalinfoservice.service.ParkEsService; -//import lombok.RequiredArgsConstructor; -//import org.springframework.http.MediaType; -//import org.springframework.http.ResponseEntity; -//import org.springframework.web.bind.annotation.*; -//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -// -//import java.io.IOException; -//import java.util.List; -//import java.util.Map; -//import java.util.concurrent.CopyOnWriteArrayList; -// -//@RestController -//@RequiredArgsConstructor -//@RequestMapping("/main/info/park") -//public class ParkEsController { -// -// private final ParkEsService parkEsService; -// private final List emitters = new CopyOnWriteArrayList<>(); -// -// // REST API 엔드포인트 - 일반 조회 -// @GetMapping(value = {"/{areaId}", ""}, produces = MediaType.APPLICATION_JSON_VALUE) -// public ResponseEntity getPark(@PathVariable(name = "areaId", required = false) String areaId) { -// if (areaId == null || areaId.isBlank()) { -// List> allPark = parkEsService.getAllParkFromES(); -// return ResponseEntity.ok(allPark); -// } -// -// Map park = parkEsService.getParkFromES(areaId); -// return park != null ? ResponseEntity.ok(park) : ResponseEntity.notFound().build(); -// } -// -// // SSE 스트림 엔드포인트 -// @GetMapping("/stream") -// public SseEmitter streamPark() { -// SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음 -// emitters.add(emitter); -// -// emitter.onCompletion(() -> emitters.remove(emitter)); -// emitter.onTimeout(() -> emitters.remove(emitter)); -// emitter.onError((e) -> emitters.remove(emitter)); -// -// return emitter; -// } -// -//// // 주기적으로 클라이언트에게 push -//// public void sendToClients(List> parkList) { -//// for (SseEmitter emitter : emitters) { -//// try { -//// emitter.send(SseEmitter.event() -//// .name("park-update") -//// .data(parkList)); -//// } catch (IOException e) { -//// emitter.completeWithError(e); -//// emitters.remove(emitter); -//// } -//// } -//// } -//} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java deleted file mode 100644 index b83c0ad..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/SseWeatherController.java +++ /dev/null @@ -1,46 +0,0 @@ -//package com.example.externalinfoservice.controller; -// -//import com.example.externalinfoservice.service.WeatherEsService; -//import lombok.RequiredArgsConstructor; -//import org.springframework.web.bind.annotation.*; -//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -// -//import java.io.IOException; -//import java.util.*; -//import java.util.concurrent.CopyOnWriteArrayList; -// -// -//@RestController -//@RequiredArgsConstructor -//@RequestMapping("/main/info/weather") -//public class SseWeatherController { -// -// private final WeatherEsService weatherEsService; -// private final List emitters = new CopyOnWriteArrayList<>(); -// -// @GetMapping("/stream") -// public SseEmitter streamWeather() { -// SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음 -// emitters.add(emitter); -// -// emitter.onCompletion(() -> emitters.remove(emitter)); -// emitter.onTimeout(() -> emitters.remove(emitter)); -// emitter.onError((e) -> emitters.remove(emitter)); -// -// return emitter; -// } -// -// // 주기적으로 클라이언트에게 push -// public void sendToClients(List> weatherList) { -// for (SseEmitter emitter : emitters) { -// try { -// emitter.send(SseEmitter.event() -// .name("weather-update") -// .data(weatherList)); -// } catch (IOException e) { -// emitter.completeWithError(e); -// emitters.remove(emitter); -// } -// } -// } -//} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java index f1fabd8..6a59eab 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java @@ -39,13 +39,31 @@ public SseEmitter streamWeather() { // 구독시, 초기 데이터 주입 try { + System.out.println("초기 데이터 푸시 중..."); var weatherList = weatherEsService.getAllWeatherFromES(); var trafficList = roadService.getTrafficData(); var parkList = parkEsService.getAllParkFromES(); var accidentList = accidentEsService.getAllAccidentsFromES(); - sendToClients(weatherList, trafficList, parkList, accidentList); - } catch (Exception e) { - throw new RuntimeException(e); + + emitter.send(SseEmitter.event() + .name("weather-update") + .data(weatherList)); + emitter.send(SseEmitter.event() + .name("traffic-update") + .data(trafficList)); + emitter.send(SseEmitter.event() + .name("park-update") + .data(parkList)); + emitter.send(SseEmitter.event() + .name("accident-alert") + .data(accidentList)); + + System.out.println(".. 초기 데이터 푸시 완료"); + + } catch (IOException | IllegalStateException e) { + System.out.println("SSE 오류 발생"); + emitter.completeWithError(e); + emitters.remove(emitter); } return emitter; @@ -55,19 +73,30 @@ public SseEmitter streamWeather() { public void sendToClients(List> weatherList, JsonNode trafficList, List> parkList, List> accidentList) { for (SseEmitter emitter : emitters) { try { + System.out.println("external 데이터 푸시 중..."); + emitter.send(SseEmitter.event() .name("weather-update") .data(weatherList)); + System.out.println(".. 날씨 데이터 푸시 완료"); + emitter.send(SseEmitter.event() .name("traffic-update") .data(trafficList)); + System.out.println(".. 도로현황 데이터 푸시 완료"); + emitter.send(SseEmitter.event() .name("park-update") .data(parkList)); + System.out.println(".. 주차 데이터 푸시 완료"); + emitter.send(SseEmitter.event() .name("accident-alert") .data(accidentList)); - } catch (IOException e) { + System.out.println(".. 사고 데이터 푸시 완료"); + + } catch (IOException | IllegalStateException e) { + System.out.println("SSE 오류 발생"); emitter.completeWithError(e); emitters.remove(emitter); } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java deleted file mode 100644 index 58dcc39..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/WeatherEsController.java +++ /dev/null @@ -1,30 +0,0 @@ -//package com.example.externalinfoservice.controller; -// -//import com.example.externalinfoservice.service.WeatherEsService; -//import lombok.RequiredArgsConstructor; -//import org.springframework.http.MediaType; -//import org.springframework.http.ResponseEntity; -//import org.springframework.web.bind.annotation.*; -// -//import java.util.List; -//import java.util.Map; -// -//@RestController -//@RequiredArgsConstructor -//@RequestMapping("/main/info/weather") -//public class WeatherEsController { -// -// -// private final WeatherEsService weatherEsService; -// -// @GetMapping(value = {"/{placeCode}", ""}, produces = MediaType.APPLICATION_JSON_VALUE) -// public ResponseEntity getWeather(@PathVariable(name = "placeCode", required = false) String placeCode) { -// if (placeCode == null || placeCode.isBlank()) { -// List> allWeather = weatherEsService.getAllWeatherFromES(); -// return ResponseEntity.ok(allWeather); -// } -// -// Map weather = weatherEsService.getWeatherFromES(placeCode); -// return weather != null ? ResponseEntity.ok(weather) : ResponseEntity.notFound().build(); -// } -//} \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/AccidentPushScheduler.java b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/AccidentPushScheduler.java deleted file mode 100644 index b189382..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/AccidentPushScheduler.java +++ /dev/null @@ -1,22 +0,0 @@ - -//package com.example.externalinfoservice.scheduler; -// -//import com.example.externalinfoservice.controller.AccidentEsController; -//import com.example.externalinfoservice.service.AccidentEsService; -//import lombok.RequiredArgsConstructor; -//import org.springframework.scheduling.annotation.Scheduled; -//import org.springframework.stereotype.Component; -// -//@Component -//@RequiredArgsConstructor -//public class AccidentPushScheduler { -// -// private final AccidentEsService accidentEsService; -// private final AccidentEsController accidentSseController; -// -// @Scheduled(fixedRate = 300_000) // 5분마다 실행 -// public void pushAccidentsToClients() { -// var accidentList = accidentEsService.getAllAccidentsFromES(); -// accidentSseController.sendToClients(accidentList); -// } -//} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java b/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java deleted file mode 100644 index 74a0aa2..0000000 --- a/external-info-service/src/main/java/com/example/externalinfoservice/scheduler/WeatherPushScheduler.java +++ /dev/null @@ -1,23 +0,0 @@ -//package com.example.externalinfoservice.scheduler; -// -//import com.example.externalinfoservice.controller.SseWeatherController; -//import com.example.externalinfoservice.service.WeatherEsService; -//import lombok.RequiredArgsConstructor; -//import org.springframework.scheduling.annotation.Scheduled; -//import org.springframework.stereotype.Component; -// -//@Component -//@RequiredArgsConstructor -//public class WeatherPushScheduler { -// -// private final WeatherEsService weatherEsService; -// private final SseWeatherController sseWeatherController; -// -// // 5분마다 날씨 데이터를 SSE 클라이언트에 push -// @Scheduled(fixedRate = 10_000) // 5분 = 300초 -// public void pushWeatherToClients() { -// var weatherList = weatherEsService.getAllWeatherFromES(); -// sseWeatherController.sendToClients(weatherList); -// } -// -//} diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java index b6587a1..5fa5050 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java @@ -73,7 +73,7 @@ public JsonNode getAccidentData() { throw new InvalidRequestException(e.getMessage(), e); } catch (Exception e) { System.out.println(e); - throw new RuntimeException("예상치 못한 오류", e); + throw new RuntimeException("사고 현황 : 예상치 못한 오류", e); } } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java index d201a06..a532ece 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java @@ -88,7 +88,7 @@ public static JsonNode getTrafficData() { throw new InvalidRequestException(e.getMessage(), e); }catch (Exception e){ System.out.println(e); - throw new RuntimeException("예상치 못한 오류",e); + throw new RuntimeException("도로 현황 데이터 : 예상치 못한 오류",e); } } } \ No newline at end of file diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java index 96ea715..1c41ac4 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java @@ -23,43 +23,43 @@ private String getTodayIndexUrl() { return String.format("http://elasticsearch.seoultravel.life/seoul_citydata_parking_%s/_search", today); } - // 특정 지역 주차장 정보 - public Map getParkFromES(String areaId) { - String url = getTodayIndexUrl(); - log.info("Requesting park info for areaId: {}", areaId); - - Map term = Map.of("parking.area_nm", areaId); - Map query = Map.of("term", term); - Map body = Map.of("size", 100, "query", query); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - - HttpEntity> entity = new HttpEntity<>(body, headers); - log.info("Request body: {}", body); - - ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class); - log.info("Response status: {}", response.getStatusCode()); - - List> hits = (List>) - ((Map) response.getBody().get("hits")).get("hits"); - - if (hits.isEmpty()) { - log.warn("No results found for areaId: {}", areaId); - return null; - } - - Map source = (Map) hits.get(0).get("_source"); - Map parking = (Map) source.get("parking"); - - log.info("Parking data: {}", parking); - return parking; - } +// // 특정 지역 주차장 정보 +// public Map getParkFromES(String areaId) { +// String url = getTodayIndexUrl(); +// log.info("Requesting park info for areaId: {}", areaId); +// +// Map term = Map.of("parking.area_nm", areaId); +// Map query = Map.of("term", term); +// Map body = Map.of("size", 100, "query", query); +// +// HttpHeaders headers = new HttpHeaders(); +// headers.setContentType(MediaType.APPLICATION_JSON); +// +// HttpEntity> entity = new HttpEntity<>(body, headers); +// log.info("Request body: {}", body); +// +// ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class); +// log.info("Response status: {}", response.getStatusCode()); +// +// List> hits = (List>) +// ((Map) response.getBody().get("hits")).get("hits"); +// +// if (hits.isEmpty()) { +// log.warn("No results found for areaId: {}", areaId); +// return null; +// } +// +// Map source = (Map) hits.get(0).get("_source"); +// Map parking = (Map) source.get("parking"); +// +// log.info("Parking data: {}", parking); +// return parking; +// } // 전체 지역 주차장 정보 public List> getAllParkFromES() { String url = getTodayIndexUrl(); - log.info("Requesting all park info"); + // log.info("Requesting all park info"); Map body = Map.of( "size", 0, @@ -107,13 +107,13 @@ public List> getAllParkFromES() { if (parkData != null) { results.add(parkData); - log.info("Added park data for area: {}", parkData.get("area_nm")); +// log.info("Added park data for area: {}", parkData.get("area_nm")); } else { log.warn("Parking data is null for a hit"); } } - log.info("Returning {} park results", results.size()); + log.info("주차 데이터 : {} park results", results.size()); return results; } } From 8f3a27daa994ccbc06749561dedcd826b78eca88 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 16 May 2025 14:22:39 +0900 Subject: [PATCH 309/361] =?UTF-8?q?Feature:=20=ED=86=B5=ED=95=A9=EA=B2=80?= =?UTF-8?q?=EC=83=89=20API=20lat,lon=20=EB=B3=80=EC=88=98=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 --- .../controller/StreamController.java | 2 +- .../com/example/placeservice/dto/PlaceDocument.java | 4 ++++ .../placeservice/service/PlaceDocumentMapper.java | 10 ++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java index 6a59eab..5c90677 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java @@ -29,7 +29,7 @@ public class StreamController { private final AccidentEsService accidentEsService; @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public SseEmitter streamWeather() { + public SseEmitter stream() { SseEmitter emitter = new SseEmitter(0L); // 타임아웃 없음 emitters.add(emitter); diff --git a/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java b/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java index 6ad5003..8b6a684 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java +++ b/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java @@ -3,6 +3,8 @@ import lombok.Builder; import lombok.Getter; +import java.math.BigDecimal; + @Builder @Getter public class PlaceDocument { @@ -12,4 +14,6 @@ public class PlaceDocument { private String type; private String address; private String place_id; + private BigDecimal lat; + private BigDecimal lon; } diff --git a/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java index 766aadb..d6c9d4e 100644 --- a/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java +++ b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java @@ -14,6 +14,8 @@ public PlaceDocument toPlaceDocument(Accommodation item) { .address(item.getAddress()) .type("accommodation") .place_id(String.valueOf(item.getAccommodationId())) + .lat(item.getLat()) + .lon(item.getLon()) .build(); } @@ -23,6 +25,8 @@ public PlaceDocument toPlaceDocument(Cafe item) { .address(item.getAddress()) .type("cafe") .place_id(String.valueOf(item.getId())) + .lat(item.getLat()) + .lon(item.getLon()) .build(); } @@ -32,6 +36,8 @@ public PlaceDocument toPlaceDocument(CulturalEvent item) { .address(item.getAddress()) .type("culturalevent") .place_id(String.valueOf(item.getEventId())) + .lat(item.getLat()) + .lon(item.getLon()) .build(); } @@ -41,6 +47,8 @@ public PlaceDocument toPlaceDocument(Restaurant item) { .address(item.getAddress()) .type("restaurant") .place_id(String.valueOf(item.getRestaurantId())) + .lat(item.getLat()) + .lon(item.getLon()) .build(); } @@ -50,6 +58,8 @@ public PlaceDocument toPlaceDocument(Attraction item) { .address(item.getAddress()) .type("attraction") .place_id(String.valueOf(item.getAttractionId())) + .lat(item.getLat()) + .lon(item.getLon()) .build(); } } From ee54b87a2360ea549a8ad0f5dd62d0323b5c4cb5 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Fri, 16 May 2025 14:44:49 +0900 Subject: [PATCH 310/361] =?UTF-8?q?=EC=9E=A5=EC=86=8C=20=EC=B6=94=EC=B2=9C?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=ED=8C=8C=EC=9D=BC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SuggestController.java | 89 +++++++++++++++++++ .../dto/SuggestFastRequestDto.java | 31 +++++++ .../dto/SuggestFastResponseDto.java | 32 +++++++ .../userservice/dto/SuggestRequestDto.java | 22 +++++ 4 files changed, 174 insertions(+) create mode 100644 user-service/src/main/java/com/example/userservice/controller/SuggestController.java create mode 100644 user-service/src/main/java/com/example/userservice/dto/SuggestFastRequestDto.java create mode 100644 user-service/src/main/java/com/example/userservice/dto/SuggestFastResponseDto.java create mode 100644 user-service/src/main/java/com/example/userservice/dto/SuggestRequestDto.java diff --git a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java new file mode 100644 index 0000000..4b5112d --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java @@ -0,0 +1,89 @@ +package com.example.userservice.controller; + +import com.example.userservice.dto.SuggestFastResponseDto; +import com.example.userservice.dto.SuggestRequestDto; +import com.example.userservice.dto.SuggestFastRequestDto; +import com.example.userservice.entity.Member; +import com.example.userservice.service.MemberService; +import org.springframework.data.elasticsearch.core.suggest.response.Suggest; +import org.springframework.http.*; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +@RestController +@RequestMapping("/suggest") +public class SuggestController { + private final MemberService memberService; + private final RestTemplate restTemplate; + + public SuggestController(MemberService memberService) { + this.memberService = memberService; + restTemplate = new RestTemplate(); + } + + @PostMapping("/{user_id}") + public ResponseEntity suggest(@PathVariable String user_id, @RequestBody SuggestRequestDto suggestRequestDto) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated() || "anonymousUser".equals(authentication.getName())) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("로그인이 필요합니다."); + } + + // 2. 권한 확인 (요청한 user_id와 인증된 사용자가 동일한지) + if (!authentication.getName().equals(user_id)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("자신의 정보에 대해서만 요청할 수 있습니다."); + } + + Member member; + try { + member = memberService.findByUserId(user_id); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("해당 ID의 회원을 찾을 수 없습니다: " + user_id); + } + + SuggestFastRequestDto suggestFastRequestDto = new SuggestFastRequestDto(); + + suggestFastRequestDto.setQuestionType(suggestRequestDto.getQuestionType()); + suggestFastRequestDto.setStartTime(suggestRequestDto.getStartTime()); + suggestFastRequestDto.setFinishTime(suggestRequestDto.getFinishTime()); + suggestFastRequestDto.setStartPlace(suggestRequestDto.getStartPlace()); + suggestFastRequestDto.setOptionalRequest(suggestRequestDto.getOptionalRequest()); + suggestFastRequestDto.setBirthYear(member.getBirthYear()); + suggestFastRequestDto.setGender(member.getGender()); + suggestFastRequestDto.setMbti(member.getMbti()); + + // FastApi 서버로 POST 요청을 보내기 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(suggestFastRequestDto, headers); + + String fastApiUrl = ""; + try { + ResponseEntity response = restTemplate.postForEntity(fastApiUrl, entity, SuggestFastResponseDto.class); + + if (response.getStatusCode() == HttpStatus.OK) { + SuggestFastResponseDto suggestFastResponseDto = response.getBody(); + System.out.println("FastAPI 응답: " + suggestFastResponseDto); + return ResponseEntity.ok(suggestFastResponseDto); + } + else { + System.err.println("FastAPI 요청 실패: " + response.getStatusCode()); + return ResponseEntity.status(response.getStatusCode()).body("FastAPI 서버로부터 오류 응답: " + response.getStatusCode()); + } + } catch (HttpClientErrorException e) { + System.err.println("클라이언트 오류: " + e.getStatusCode() + " - " + e.getResponseBodyAsString()); + return ResponseEntity.status(e.getStatusCode()).body("FastAPI 호출 중 클라이언트 오류: " + e.getResponseBodyAsString()); + } catch (HttpServerErrorException e) { + System.err.println("서버 오류: " + e.getStatusCode() + " - " + e.getResponseBodyAsString()); + return ResponseEntity.status(e.getStatusCode()).body("FastAPI 호출 중 서버 오류: " + e.getResponseBodyAsString()); + } catch (RestClientException e) { + System.err.println("RestClientException: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body("FastAPI 서비스 통신 오류: " + e.getMessage()); + } + } +} diff --git a/user-service/src/main/java/com/example/userservice/dto/SuggestFastRequestDto.java b/user-service/src/main/java/com/example/userservice/dto/SuggestFastRequestDto.java new file mode 100644 index 0000000..9e5dff7 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/dto/SuggestFastRequestDto.java @@ -0,0 +1,31 @@ +package com.example.userservice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class SuggestFastRequestDto { + @JsonProperty("question_type") + private int questionType; + + @JsonProperty("start_time") + private String startTime; + + @JsonProperty("finish_time") + private String finishTime; + + @JsonProperty("start_place") + private String startPlace; + + @JsonProperty("optional_request") + private String optionalRequest; + + @JsonProperty("birth_year") + private int birthYear; + + @JsonProperty("gender") + private String gender; + + @JsonProperty("mbti") + private String mbti; +} diff --git a/user-service/src/main/java/com/example/userservice/dto/SuggestFastResponseDto.java b/user-service/src/main/java/com/example/userservice/dto/SuggestFastResponseDto.java new file mode 100644 index 0000000..4d0d298 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/dto/SuggestFastResponseDto.java @@ -0,0 +1,32 @@ +package com.example.userservice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class SuggestFastResponseDto { + @JsonProperty("start_time") + private String startTime; + + @JsonProperty("finish_time") + private String finishTime; + + @JsonProperty("start_place") + private String startPlace; + + @JsonProperty("optional_request") + private String optionalRequest; + + @JsonProperty("birth_year") + private int birthYear; + + @JsonProperty("gender") + private String gender; + + @JsonProperty("mbti") + private String mbti; + + @JsonProperty("answer") + private String answer; + + @JsonProperty("created_at") + private String createdAt; +} diff --git a/user-service/src/main/java/com/example/userservice/dto/SuggestRequestDto.java b/user-service/src/main/java/com/example/userservice/dto/SuggestRequestDto.java new file mode 100644 index 0000000..ada6fe2 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/dto/SuggestRequestDto.java @@ -0,0 +1,22 @@ +package com.example.userservice.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class SuggestRequestDto { + @JsonProperty("question_type") + private int questionType; + + @JsonProperty("start_time") + private String startTime; + + @JsonProperty("finish_time") + private String finishTime; + + @JsonProperty("start_place") + private String startPlace; + + @JsonProperty("optional_request") + private String optionalRequest; +} From 3aca2f637d17f982efefa7f02248bfe10e985bf3 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Fri, 16 May 2025 17:06:13 +0900 Subject: [PATCH 311/361] =?UTF-8?q?cafe=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- place-service/build.gradle | 3 + .../placeservice/config/CacheConfig.java | 30 +++++ .../controller/CafeController.java | 46 +------- .../placeservice/dto/cafe/CafeDto.java | 33 +++++- .../placeservice/dto/cafe/CafeListDto.java | 17 +++ .../com/example/placeservice/entity/Cafe.java | 2 +- .../repository/CafeRepository.java | 8 ++ .../placeservice/service/CafeService.java | 111 +++++++++++++++--- 8 files changed, 193 insertions(+), 57 deletions(-) create mode 100644 place-service/src/main/java/com/example/placeservice/config/CacheConfig.java create mode 100644 place-service/src/main/java/com/example/placeservice/dto/cafe/CafeListDto.java diff --git a/place-service/build.gradle b/place-service/build.gradle index d1b8b5a..a0534fc 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -36,6 +36,9 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + // 캐싱 + implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' + implementation 'org.springframework.boot:spring-boot-starter-cache' // JWT 처리 implementation 'io.jsonwebtoken:jjwt-api:0.12.6' diff --git a/place-service/src/main/java/com/example/placeservice/config/CacheConfig.java b/place-service/src/main/java/com/example/placeservice/config/CacheConfig.java new file mode 100644 index 0000000..fbb62ef --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/config/CacheConfig.java @@ -0,0 +1,30 @@ +package com.example.placeservice.config; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.TimeUnit; + +@Configuration +@EnableCaching +public class CacheConfig { + + @Bean + public Caffeine caffeineConfig() { + return Caffeine.newBuilder() + .expireAfterWrite(10, TimeUnit.MINUTES) + .initialCapacity(100) + .maximumSize(500); + } + + @Bean + public CacheManager cacheManager(Caffeine caffeine) { + CaffeineCacheManager cacheManager = new CaffeineCacheManager(); + cacheManager.setCaffeine(caffeine); + return cacheManager; + } +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java index 0fd7250..9c0220d 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java @@ -1,6 +1,5 @@ package com.example.placeservice.controller; -import com.example.placeservice.dto.cafe.CafeDto; import com.example.placeservice.entity.Cafe; import com.example.placeservice.service.CafeService; import com.fasterxml.jackson.databind.ObjectMapper; @@ -10,7 +9,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -32,51 +30,17 @@ public class CafeController { @GetMapping("/cafe/list") public ResponseEntity>> getCafeList() { log.info("카페 목록 조회"); - List cafes = cafeRepository.findAll(); - - // ID를 기준으로 정렬 - List> cafeList = cafes.stream() - .sorted(Comparator.comparing(Cafe::getId)) - .map(cafe -> { - Map cafeMap = new LinkedHashMap<>(); - cafeMap.put("cafe_id", cafe.getId().toString()); - cafeMap.put("cafe_name", cafe.getName()); - cafeMap.put("lat", cafe.getLat().toString()); - cafeMap.put("lon", cafe.getLon().toString()); - return cafeMap; - }) - .collect(Collectors.toList()); - - return ResponseEntity.ok(cafeList); + // 캐싱된 서비스 메소드 호출로 변경 + return ResponseEntity.ok(cafeService.getCachedCafeList()); } - - @GetMapping("/info/cafe/{cafeId}") public ResponseEntity> getCafeInfo(@PathVariable Long cafeId) { log.info("카페 ID {}의 상세 정보 조회", cafeId); + // 캐싱된 서비스 메소드 호출로 변경 + Map cafeInfo = cafeService.getCachedCafeInfo(cafeId); - // 카페 ID로 상세 정보 조회 - Optional cafeOptional = cafeRepository.findById(cafeId); - - if (cafeOptional.isPresent()) { - Cafe cafe = cafeOptional.get(); - Map cafeInfo = new LinkedHashMap<>(); - cafeInfo.put("cafe_id", cafe.getId()); - cafeInfo.put("cafe_name", cafe.getName()); - cafeInfo.put("address", cafe.getAddress()); - cafeInfo.put("phone", cafe.getPhone()); - cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl()); - cafeInfo.put("lat", cafe.getLat()); - cafeInfo.put("lon", cafe.getLon()); - - // 지역(Area) 정보 추가 - if (cafe.getArea() != null) { - cafeInfo.put("area_id", cafe.getArea().getAreaId()); - cafeInfo.put("area_name", cafe.getArea().getName()); - } - - // data 래퍼 없이 직접 반환 + if (cafeInfo != null) { return ResponseEntity.ok(cafeInfo); } diff --git a/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeDto.java b/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeDto.java index 60264c9..3b4279c 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeDto.java @@ -1,6 +1,6 @@ package com.example.placeservice.dto.cafe; - +import com.example.placeservice.entity.Cafe; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -14,6 +14,8 @@ @AllArgsConstructor public class CafeDto { private Long id; + private Long areaId; + private String areaName; private String name; private String address; private BigDecimal lat; @@ -21,4 +23,33 @@ public class CafeDto { private String phone; private String kakaomapUrl; private String categoryCode; + + /** + * Cafe 엔티티를 CafeDto로 변환하는 정적 팩토리 메소드 + */ + public static CafeDto fromEntity(Cafe entity) { + if (entity == null) { + return null; + } + + Long areaIdValue = null; + String areaNameValue = null; + if (entity.getArea() != null) { + areaIdValue = entity.getArea().getAreaId(); + areaNameValue = entity.getArea().getName(); + } + + return new CafeDto( + entity.getId(), + areaIdValue, + areaNameValue, + entity.getName(), + entity.getAddress(), + entity.getLat(), + entity.getLon(), + entity.getPhone(), + entity.getKakaomapUrl(), + entity.getCategoryCode() + ); + } } \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeListDto.java b/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeListDto.java new file mode 100644 index 0000000..c2cc8aa --- /dev/null +++ b/place-service/src/main/java/com/example/placeservice/dto/cafe/CafeListDto.java @@ -0,0 +1,17 @@ +package com.example.placeservice.dto.cafe; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CafeListDto { + private Long id; + private String name; + private BigDecimal lat; + private BigDecimal lon; +} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/entity/Cafe.java b/place-service/src/main/java/com/example/placeservice/entity/Cafe.java index b7e6db9..7cefe05 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Cafe.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Cafe.java @@ -43,7 +43,7 @@ public class Cafe { @Column(name = "category_code") private String categoryCode; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "area_id") private Area area; } \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java index 400ffd1..ae67f5f 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/CafeRepository.java @@ -2,6 +2,7 @@ import com.example.placeservice.entity.Accommodation; import com.example.placeservice.entity.Cafe; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -21,4 +22,11 @@ public interface CafeRepository extends JpaRepository { Optional findByKakaomapUrl(String placeCode); List findByNameContainingIgnoreCase(String keyword); List findByAddressContainingIgnoreCase(String keyword); + + // 성능 개선을 위한 추가 메소드 + @EntityGraph(attributePaths = {"area"}) + List findAll(); + + @EntityGraph(attributePaths = {"area"}) + Optional findById(Long id); } \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java index ffdb843..11efdc6 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CafeService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java @@ -7,15 +7,13 @@ import com.example.placeservice.repository.CafeRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; @Slf4j @@ -31,7 +29,62 @@ public class CafeService { private boolean processingCompleted = false; /** - * 모든 장소에 대해 주변 카페 데이터 처리 + * 캐싱된 카페 목록 조회 + */ + @Cacheable(value = "cafeList") + @Transactional(readOnly = true) + public List> getCachedCafeList() { + log.info("캐싱된 카페 목록 조회"); + List cafes = cafeRepository.findAll(); + + // ID를 기준으로 정렬 + return cafes.stream() + .sorted(Comparator.comparing(Cafe::getId)) + .map(cafe -> { + Map cafeMap = new LinkedHashMap<>(); + cafeMap.put("cafe_id", cafe.getId().toString()); + cafeMap.put("cafe_name", cafe.getName()); + cafeMap.put("lat", cafe.getLat().toString()); + cafeMap.put("lon", cafe.getLon().toString()); + return cafeMap; + }) + .collect(Collectors.toList()); + } + + /** + * 캐싱된 카페 상세 정보 조회 + */ + @Cacheable(value = "cafeInfo", key = "#cafeId") + @Transactional(readOnly = true) + public Map getCachedCafeInfo(Long cafeId) { + log.info("캐싱된 카페 상세 정보 조회: {}", cafeId); + Optional cafeOptional = cafeRepository.findById(cafeId); + + if (cafeOptional.isPresent()) { + Cafe cafe = cafeOptional.get(); + Map cafeInfo = new LinkedHashMap<>(); + cafeInfo.put("cafe_id", cafe.getId()); + cafeInfo.put("cafe_name", cafe.getName()); + cafeInfo.put("address", cafe.getAddress()); + cafeInfo.put("phone", cafe.getPhone()); + cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl()); + cafeInfo.put("lat", cafe.getLat()); + cafeInfo.put("lon", cafe.getLon()); + + // 지역(Area) 정보 추가 + if (cafe.getArea() != null) { + cafeInfo.put("area_id", cafe.getArea().getAreaId()); + cafeInfo.put("area_name", cafe.getArea().getName()); + } + + return cafeInfo; + } + + return null; + } + + /** + * 모든 지역에 대한 카페 데이터 처리 */ @Async @Transactional @@ -90,7 +143,7 @@ public void processAllAreas() { } /** - * 단일 장소의 주변 카페 정보 처리 + * 단일 장소의 주변 카페 정보 처리 - 벌크 저장으로 최적화 */ @Transactional public void processSingleArea(Area area) { @@ -142,13 +195,15 @@ public void processSingleArea(Area area) { area.getName(), cafes.size(), filteredCafes.size()); // 최대 15개의 카페만 저장 (음식점과 동일한 로직 적용) - int savedCount = 0; int maxCafesToSave = 15; + // 벌크 저장을 위한 리스트 + List cafesToSave = new ArrayList<>(); + // 필터링된 카페 정보 저장 for (KakaoApiService.CafeResponse.Document cafeDoc : filteredCafes) { // 최대 카페 저장 개수 제한 - if (savedCount >= maxCafesToSave) { + if (cafesToSave.size() >= maxCafesToSave) { log.info("최대 저장 개수({})에 도달하여 저장 중단", maxCafesToSave); break; } @@ -176,17 +231,18 @@ public void processSingleArea(Area area) { .build(); log.info("카페 객체 생성 완료: {}", cafe.getName()); - Cafe savedCafe = cafeRepository.save(cafe); - log.info("카페 저장 완료: {}, 저장된 ID: {}", savedCafe.getName(), savedCafe.getId()); - - savedCount++; + cafesToSave.add(cafe); // 리스트에 추가 } catch (Exception e) { log.error("카페 저장 중 오류 발생: {}, 오류: {}", cafeDoc.getPlaceName(), e.getMessage(), e); } } - log.info("지역 '{}' (ID: {})에 총 {}개의 카페 저장 완료", - area.getName(), area.getAreaId(), savedCount); + // 벌크 저장 수행 + if (!cafesToSave.isEmpty()) { + List savedCafes = cafeRepository.saveAll(cafesToSave); + log.info("지역 '{}' (ID: {})에 총 {}개의 카페 일괄 저장 완료", + area.getName(), area.getAreaId(), savedCafes.size()); + } } catch (Exception e) { log.error("장소 ID {} 처리 중 오류: {}", area.getAreaId(), e.getMessage(), e); @@ -197,6 +253,7 @@ public void processSingleArea(Area area) { /** * 특정 지역의 카페 목록 조회 */ + @Cacheable(value = "cafesByArea", key = "#areaId") @Transactional(readOnly = true) public List getCafesByAreaId(Long areaId) { log.info("지역 ID {}의 카페 목록 조회", areaId); @@ -211,6 +268,7 @@ public List getCafesByAreaId(Long areaId) { /** * 모든 카페 목록 조회 */ + @Cacheable(value = "allCafes") @Transactional(readOnly = true) public List getAllCafes() { log.info("모든 카페 목록 조회"); @@ -225,6 +283,7 @@ public List getAllCafes() { /** * ID, 이름, 위도, 경도만 포함한 모든 카페 목록 조회 */ + @Cacheable(value = "simpleCafes") @Transactional(readOnly = true) public List> getAllCafesWithLimitedInfo() { log.info("제한된 정보로 모든 카페 목록 조회"); @@ -259,6 +318,10 @@ private CafeDto convertToDto(Cafe cafe) { .build(); } + /** + * 카카오맵 URL로 카페 상세 정보 조회 + */ + @Cacheable(value = "cafeByPlaceCode", key = "#placeCode") @Transactional(readOnly = true) public Map getCafeDetailByPlaceCode(String placeCode) { log.info("장소 코드로 카페 상세 정보 조회: {}", placeCode); @@ -283,4 +346,24 @@ public Map getCafeDetailByPlaceCode(String placeCode) { log.info("장소 코드 {}에 해당하는 카페를 찾을 수 없음", placeCode); return null; } + + /** + * 지역 ID로 카페 목록 조회 (응답 형식 맞춤) + */ + @Cacheable(value = "cafesByAreaFormatted", key = "#areaId") + @Transactional(readOnly = true) + public Map getCafesByAreaIdFormatted(Long areaId) { + log.info("지역 ID {}의 카페 목록 조회 (응답 형식 맞춤)", areaId); + + List cafes = cafeRepository.findByAreaId(areaId); + List cafeDtos = cafes.stream() + .map(this::convertToDto) + .collect(Collectors.toList()); + + Map response = new HashMap<>(); + response.put("type", "cafe"); + response.put("content", cafeDtos); + + return response; + } } \ No newline at end of file From 1edc01735849cac50b3c8b08359ab331f52b09b5 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Sat, 17 May 2025 09:27:52 +0900 Subject: [PATCH 312/361] =?UTF-8?q?cafe=20=EC=84=B1=EB=8A=A5=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 --- .../java/com/example/placeservice/service/KakaoApiService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java index 7f028a2..28e0c26 100644 --- a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java @@ -28,7 +28,7 @@ public class KakaoApiService { @Value("${kakao.api.key}") private String kakaoApiKey; - private static final String KAKAO_API_BASE_URL = "https://dapi.kakao.com/v2/local"; + private static final String KAKAO_API_BASE_URL = "https://api.kakao.com/v2/local"; @Data @JsonIgnoreProperties(ignoreUnknown = true) From 4f90a9144ab1c9a11f91d0aeffd4b6b577b9921e Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Sat, 17 May 2025 09:40:00 +0900 Subject: [PATCH 313/361] =?UTF-8?q?=EC=9D=B4=EC=A0=84=20=EC=97=AC=ED=96=89?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EC=B2=9C=20=EA=B8=B0=EB=A1=9D=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SuggestController.java | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java index 4b5112d..a2c441f 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java @@ -5,7 +5,6 @@ import com.example.userservice.dto.SuggestFastRequestDto; import com.example.userservice.entity.Member; import com.example.userservice.service.MemberService; -import org.springframework.data.elasticsearch.core.suggest.response.Suggest; import org.springframework.http.*; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -15,11 +14,14 @@ import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; +import java.util.List; + @RestController @RequestMapping("/suggest") public class SuggestController { private final MemberService memberService; private final RestTemplate restTemplate; + private final String fastApiUrl = "http://localhost:8000/suggest/"; public SuggestController(MemberService memberService) { this.memberService = memberService; @@ -27,7 +29,7 @@ public SuggestController(MemberService memberService) { } @PostMapping("/{user_id}") - public ResponseEntity suggest(@PathVariable String user_id, @RequestBody SuggestRequestDto suggestRequestDto) { + public ResponseEntity suggestTourPlan(@PathVariable String user_id, @RequestBody SuggestRequestDto suggestRequestDto) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !authentication.isAuthenticated() || "anonymousUser".equals(authentication.getName())) { @@ -61,8 +63,7 @@ public ResponseEntity suggest(@PathVariable String user_id, @RequestBody Sugg HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity entity = new HttpEntity<>(suggestFastRequestDto, headers); - - String fastApiUrl = ""; + try { ResponseEntity response = restTemplate.postForEntity(fastApiUrl, entity, SuggestFastResponseDto.class); @@ -86,4 +87,41 @@ public ResponseEntity suggest(@PathVariable String user_id, @RequestBody Sugg return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body("FastAPI 서비스 통신 오류: " + e.getMessage()); } } + + @GetMapping("/{user_id") + public ResponseEntity getPrevChatsForPlan(@PathVariable String user_id) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated() || "anonymousUser".equals(authentication.getName())) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("로그인이 필요합니다."); + } + + // 2. 권한 확인 (요청한 user_id와 인증된 사용자가 동일한지) + if (!authentication.getName().equals(user_id)) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("자신의 정보에 대해서만 요청할 수 있습니다."); + } + + Member member; + try { + member = memberService.findByUserId(user_id); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("해당 ID의 회원을 찾을 수 없습니다: " + user_id); + } + + try { + ResponseEntity responseEntity = restTemplate.getForEntity(fastApiUrl, SuggestFastResponseDto[].class); + SuggestFastResponseDto[] responseArray = responseEntity.getBody(); + + if (responseArray == null) { + // FastAPI 응답 본문이 null인 경우 (예: FastAPI가 204 No Content를 반환했거나, 문제가 있는 경우) + // 빈 배열 또는 적절한 오류 응답을 반환할 수 있습니다. + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("FastAPI로부터 응답 데이터를 받지 못했습니다."); + } + + return ResponseEntity.ok(responseArray); + } catch (HttpClientErrorException | HttpServerErrorException e) { + return ResponseEntity.status(e.getStatusCode()).body("FastAPI 호출 중 오류 발생: " + e.getMessage()); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("내부 서버 오류가 발생했습니다: " + e.getMessage()); + } + } } From 97198cf82990eab7fc760bc4bb52df21f943c940 Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Sat, 17 May 2025 09:43:04 +0900 Subject: [PATCH 314/361] =?UTF-8?q?cafe=20=EC=84=B1=EB=8A=A5=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 --- .../java/com/example/placeservice/service/KakaoApiService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java index 28e0c26..7f028a2 100644 --- a/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java +++ b/place-service/src/main/java/com/example/placeservice/service/KakaoApiService.java @@ -28,7 +28,7 @@ public class KakaoApiService { @Value("${kakao.api.key}") private String kakaoApiKey; - private static final String KAKAO_API_BASE_URL = "https://api.kakao.com/v2/local"; + private static final String KAKAO_API_BASE_URL = "https://dapi.kakao.com/v2/local"; @Data @JsonIgnoreProperties(ignoreUnknown = true) From 9a04adefa351fba08dad3f6624ff8f1b120fdbfe Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Sat, 17 May 2025 10:00:03 +0900 Subject: [PATCH 315/361] =?UTF-8?q?SuggestController=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/userservice/controller/SuggestController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java index a2c441f..0d90bac 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java @@ -65,7 +65,7 @@ public ResponseEntity suggestTourPlan(@PathVariable String user_id, @RequestB HttpEntity entity = new HttpEntity<>(suggestFastRequestDto, headers); try { - ResponseEntity response = restTemplate.postForEntity(fastApiUrl, entity, SuggestFastResponseDto.class); + ResponseEntity response = restTemplate.postForEntity(fastApiUrl + "${user_id}", entity, SuggestFastResponseDto.class); if (response.getStatusCode() == HttpStatus.OK) { SuggestFastResponseDto suggestFastResponseDto = response.getBody(); @@ -108,7 +108,7 @@ public ResponseEntity getPrevChatsForPlan(@PathVariable String user_id) { } try { - ResponseEntity responseEntity = restTemplate.getForEntity(fastApiUrl, SuggestFastResponseDto[].class); + ResponseEntity responseEntity = restTemplate.getForEntity(fastApiUrl + "${user_id}", SuggestFastResponseDto[].class); SuggestFastResponseDto[] responseArray = responseEntity.getBody(); if (responseArray == null) { From f8feb37ff20837fa37829ade4b27df0909b5ea9d Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Sat, 17 May 2025 10:12:28 +0900 Subject: [PATCH 316/361] =?UTF-8?q?cafe=20=EC=84=B1=EB=8A=A5=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 --- .../placeservice/config/CacheConfig.java | 30 --------- .../controller/CafeController.java | 19 +++--- .../placeservice/service/CafeService.java | 62 +++++-------------- 3 files changed, 21 insertions(+), 90 deletions(-) delete mode 100644 place-service/src/main/java/com/example/placeservice/config/CacheConfig.java diff --git a/place-service/src/main/java/com/example/placeservice/config/CacheConfig.java b/place-service/src/main/java/com/example/placeservice/config/CacheConfig.java deleted file mode 100644 index fbb62ef..0000000 --- a/place-service/src/main/java/com/example/placeservice/config/CacheConfig.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.placeservice.config; - -import com.github.benmanes.caffeine.cache.Caffeine; -import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.cache.caffeine.CaffeineCacheManager; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.util.concurrent.TimeUnit; - -@Configuration -@EnableCaching -public class CacheConfig { - - @Bean - public Caffeine caffeineConfig() { - return Caffeine.newBuilder() - .expireAfterWrite(10, TimeUnit.MINUTES) - .initialCapacity(100) - .maximumSize(500); - } - - @Bean - public CacheManager cacheManager(Caffeine caffeine) { - CaffeineCacheManager cacheManager = new CaffeineCacheManager(); - cacheManager.setCaffeine(caffeine); - return cacheManager; - } -} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java index 9c0220d..bf49fc1 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/CafeController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/CafeController.java @@ -1,10 +1,8 @@ package com.example.placeservice.controller; -import com.example.placeservice.entity.Cafe; +import com.example.placeservice.dto.cafe.CafeListDto; import com.example.placeservice.service.CafeService; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; -import com.example.placeservice.repository.CafeRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -12,8 +10,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.*; -import java.util.stream.Collectors; +import java.util.List; +import java.util.Map; @Slf4j @RestController @@ -21,24 +19,21 @@ @RequiredArgsConstructor public class CafeController { - private final CafeRepository cafeRepository; private final CafeService cafeService; /** - * 전체 카페 목록 조회 + * 전체 카페 목록 조회 - 레스토랑 스타일로 단순화 (캐싱 없음) */ @GetMapping("/cafe/list") - public ResponseEntity>> getCafeList() { + public ResponseEntity> getCafeList() { log.info("카페 목록 조회"); - // 캐싱된 서비스 메소드 호출로 변경 - return ResponseEntity.ok(cafeService.getCachedCafeList()); + return ResponseEntity.ok(cafeService.getSimpleCafeList()); } @GetMapping("/info/cafe/{cafeId}") public ResponseEntity> getCafeInfo(@PathVariable Long cafeId) { log.info("카페 ID {}의 상세 정보 조회", cafeId); - // 캐싱된 서비스 메소드 호출로 변경 - Map cafeInfo = cafeService.getCachedCafeInfo(cafeId); + Map cafeInfo = cafeService.getCafeInfo(cafeId); if (cafeInfo != null) { return ResponseEntity.ok(cafeInfo); diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java index 11efdc6..a01c08e 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CafeService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java @@ -1,13 +1,13 @@ package com.example.placeservice.service; import com.example.placeservice.dto.cafe.CafeDto; +import com.example.placeservice.dto.cafe.CafeListDto; import com.example.placeservice.entity.Area; import com.example.placeservice.entity.Cafe; import com.example.placeservice.repository.AreaRepository; import com.example.placeservice.repository.CafeRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.annotation.Cacheable; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,35 +29,27 @@ public class CafeService { private boolean processingCompleted = false; /** - * 캐싱된 카페 목록 조회 + * 레스토랑 스타일로 단순화된 카페 목록 조회 (캐싱 없음) */ - @Cacheable(value = "cafeList") @Transactional(readOnly = true) - public List> getCachedCafeList() { - log.info("캐싱된 카페 목록 조회"); - List cafes = cafeRepository.findAll(); - - // ID를 기준으로 정렬 - return cafes.stream() - .sorted(Comparator.comparing(Cafe::getId)) - .map(cafe -> { - Map cafeMap = new LinkedHashMap<>(); - cafeMap.put("cafe_id", cafe.getId().toString()); - cafeMap.put("cafe_name", cafe.getName()); - cafeMap.put("lat", cafe.getLat().toString()); - cafeMap.put("lon", cafe.getLon().toString()); - return cafeMap; - }) + public List getSimpleCafeList() { + log.info("심플한 카페 목록 조회 - 레스토랑 스타일 (캐싱 없음)"); + return cafeRepository.findAll().stream() + .map(cafe -> new CafeListDto( + cafe.getId(), + cafe.getName(), + cafe.getLat(), + cafe.getLon() + )) .collect(Collectors.toList()); } /** - * 캐싱된 카페 상세 정보 조회 + * 카페 상세 정보 조회 (캐싱 없음) */ - @Cacheable(value = "cafeInfo", key = "#cafeId") @Transactional(readOnly = true) - public Map getCachedCafeInfo(Long cafeId) { - log.info("캐싱된 카페 상세 정보 조회: {}", cafeId); + public Map getCafeInfo(Long cafeId) { + log.info("카페 상세 정보 조회 (캐싱 없음): {}", cafeId); Optional cafeOptional = cafeRepository.findById(cafeId); if (cafeOptional.isPresent()) { @@ -253,7 +245,6 @@ public void processSingleArea(Area area) { /** * 특정 지역의 카페 목록 조회 */ - @Cacheable(value = "cafesByArea", key = "#areaId") @Transactional(readOnly = true) public List getCafesByAreaId(Long areaId) { log.info("지역 ID {}의 카페 목록 조회", areaId); @@ -268,7 +259,6 @@ public List getCafesByAreaId(Long areaId) { /** * 모든 카페 목록 조회 */ - @Cacheable(value = "allCafes") @Transactional(readOnly = true) public List getAllCafes() { log.info("모든 카페 목록 조회"); @@ -280,28 +270,6 @@ public List getAllCafes() { .collect(Collectors.toList()); } - /** - * ID, 이름, 위도, 경도만 포함한 모든 카페 목록 조회 - */ - @Cacheable(value = "simpleCafes") - @Transactional(readOnly = true) - public List> getAllCafesWithLimitedInfo() { - log.info("제한된 정보로 모든 카페 목록 조회"); - List cafes = cafeRepository.findAll(); - log.info("총 {}개의 카페 조회됨", cafes.size()); - - return cafes.stream() - .map(cafe -> { - Map cafeMap = new HashMap<>(); - cafeMap.put("id", cafe.getId()); - cafeMap.put("name", cafe.getName()); - cafeMap.put("lat", cafe.getLat()); - cafeMap.put("lon", cafe.getLon()); - return cafeMap; - }) - .collect(Collectors.toList()); - } - /** * Cafe 엔티티를 DTO로 변환 */ @@ -321,7 +289,6 @@ private CafeDto convertToDto(Cafe cafe) { /** * 카카오맵 URL로 카페 상세 정보 조회 */ - @Cacheable(value = "cafeByPlaceCode", key = "#placeCode") @Transactional(readOnly = true) public Map getCafeDetailByPlaceCode(String placeCode) { log.info("장소 코드로 카페 상세 정보 조회: {}", placeCode); @@ -350,7 +317,6 @@ public Map getCafeDetailByPlaceCode(String placeCode) { /** * 지역 ID로 카페 목록 조회 (응답 형식 맞춤) */ - @Cacheable(value = "cafesByAreaFormatted", key = "#areaId") @Transactional(readOnly = true) public Map getCafesByAreaIdFormatted(Long areaId) { log.info("지역 ID {}의 카페 목록 조회 (응답 형식 맞춤)", areaId); From 6c42cc2a6c67d63590cd8737e18050fdbfcd2ce6 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Sat, 17 May 2025 10:30:06 +0900 Subject: [PATCH 317/361] =?UTF-8?q?jipzin=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user-service/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/user-service/build.gradle b/user-service/build.gradle index bfb371e..7afc833 100644 --- a/user-service/build.gradle +++ b/user-service/build.gradle @@ -52,6 +52,9 @@ dependencies { implementation 'io.github.cdimascio:java-dotenv:5.2.2' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + implementation 'org.springframework.cloud:spring-cloud-starter-zipkin' + implementation 'org.springframework.cloud:spring-cloud-starter-sleuth' + } tasks.named('bootBuildImage') { From eaf42ff6a9242362635c9eb19d9e39627215e616 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Sat, 17 May 2025 10:40:20 +0900 Subject: [PATCH 318/361] =?UTF-8?q?zipkin=20=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user-service/build.gradle | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/user-service/build.gradle b/user-service/build.gradle index 7afc833..b24d6e9 100644 --- a/user-service/build.gradle +++ b/user-service/build.gradle @@ -29,32 +29,39 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' - + // Spring Boot 기본 implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.apache.kafka:kafka-streams' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.session:spring-session-data-redis' implementation 'org.springframework.kafka:spring-kafka' + implementation 'org.apache.kafka:kafka-streams' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' - compileOnly 'org.projectlombok:lombok' - developmentOnly 'org.springframework.boot:spring-boot-devtools' + implementation 'io.github.cdimascio:java-dotenv:5.2.2' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + + // Micrometer 기반 Zipkin 연동 (대체 Sleuth + Zipkin) + implementation 'io.micrometer:micrometer-tracing-bridge-brave' + implementation 'io.zipkin.reporter2:zipkin-reporter-brave' + + // DB runtimeOnly 'org.postgresql:postgresql' + + // Lombok + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + + // 개발 환경 + developmentOnly 'org.springframework.boot:spring-boot-devtools' + + // 테스트 testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.kafka:spring-kafka-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - implementation 'io.github.cdimascio:java-dotenv:5.2.2' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' - - implementation 'org.springframework.cloud:spring-cloud-starter-zipkin' - implementation 'org.springframework.cloud:spring-cloud-starter-sleuth' - } tasks.named('bootBuildImage') { From 712b9d9b5f53ae5915fbbd10f34266d76d9576e7 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Sat, 17 May 2025 14:02:20 +0900 Subject: [PATCH 319/361] =?UTF-8?q?zipkin=20=EC=98=A4=EB=A5=98=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 --- user-service/build.gradle | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/user-service/build.gradle b/user-service/build.gradle index b24d6e9..0933db3 100644 --- a/user-service/build.gradle +++ b/user-service/build.gradle @@ -13,6 +13,14 @@ java { } } + +//dependencyManagement { +// imports { +// mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.1" +// } +//} + + configurations { compileOnly { extendsFrom annotationProcessor @@ -62,6 +70,10 @@ dependencies { testImplementation 'org.springframework.kafka:spring-kafka-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + //implementation 'org.springframework.cloud:spring-cloud-starter-sleuth:2.2.4.RELEASE' + + implementation 'net.logstash.logback:logstash-logback-encoder:7.4' } tasks.named('bootBuildImage') { From 4c99468eb25a410181a877a10c43ee65942cbe24 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Sat, 17 May 2025 14:48:00 +0900 Subject: [PATCH 320/361] =?UTF-8?q?=EC=84=9C=EC=9A=B8=EC=8B=9C=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=B3=80=EA=B2=BD=20=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- place-service/build.gradle | 1 - .../placeservice/config/DataInitializer.java | 20 ----------- .../example/placeservice/init/DataInit.java | 8 ++--- .../service/AccommodationService.java | 2 +- place-service/src/main/resources/data.sql | 36 ++++++++++++------- .../controller/SuggestController.java | 4 +-- 6 files changed, 31 insertions(+), 40 deletions(-) delete mode 100644 place-service/src/main/java/com/example/placeservice/config/DataInitializer.java diff --git a/place-service/build.gradle b/place-service/build.gradle index d1b8b5a..eba7eb4 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -33,7 +33,6 @@ dependencies { implementation 'javax.xml.bind:jaxb-api:2.3.1' // JAXB Runtime (XML 파싱을 위해 필요) implementation 'org.glassfish.jaxb:jaxb-runtime:4.0.3' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' diff --git a/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java b/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java deleted file mode 100644 index 550051d..0000000 --- a/place-service/src/main/java/com/example/placeservice/config/DataInitializer.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.placeservice.config; - -import com.example.placeservice.service.AccommodationService; -import lombok.RequiredArgsConstructor; -import org.springframework.boot.CommandLineRunner; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class DataInitializer implements CommandLineRunner { - private final AccommodationService accommodationService; - - @Override - public void run(String... args) throws Exception { - // API 호출 및 XML 파싱 - accommodationService.saveAccommodations(); - - System.out.println("데이터 초기화가 완료되었습니다."); - } -} \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/init/DataInit.java b/place-service/src/main/java/com/example/placeservice/init/DataInit.java index fe247da..412f2af 100644 --- a/place-service/src/main/java/com/example/placeservice/init/DataInit.java +++ b/place-service/src/main/java/com/example/placeservice/init/DataInit.java @@ -1,11 +1,9 @@ package com.example.placeservice.init; +import com.example.placeservice.repository.AccommodationRepository; import com.example.placeservice.repository.CafeRepository; import com.example.placeservice.repository.RestaurantRepository; -import com.example.placeservice.service.AttractionService; -import com.example.placeservice.service.CafeService; -import com.example.placeservice.service.CulturalEventService; -import com.example.placeservice.service.RestaurantService; +import com.example.placeservice.service.*; import lombok.RequiredArgsConstructor; import org.springframework.boot.CommandLineRunner; import org.springframework.core.io.ClassPathResource; @@ -21,6 +19,7 @@ @RequiredArgsConstructor public class DataInit implements CommandLineRunner { + private final AccommodationService accommodationService; private final AttractionService attractionService; private final CulturalEventService culturalEventService; private final RestaurantRepository restaurantRepository; @@ -49,6 +48,7 @@ public void run(String... args) throws Exception { } // 2. 장소 데이터 삽입 + accommodationService.saveAccommodations(); attractionService.fetchDataFromVisitSeoul(); culturalEventService.fetchAndSaveAllEvents(); diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java index 75b7b78..90cf5bc 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java @@ -128,7 +128,7 @@ public void saveAccommodations() throws JsonProcessingException { // 5. 수집된 숙소 정보 일괄 저장 if (!accommodationsToSave.isEmpty()) { accommodationRepository.saveAll(accommodationsToSave); // saveAll로 성능 향상 - System.out.println("저장 성공"); + System.out.println("숙박업소 저장 성공"); } else { System.out.println("신규 저장 숙박업소가 없습니다."); } diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index 6d15ed5..963f5c7 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -20,13 +20,13 @@ INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES ('POI019', '구로디지털단지역', 'Guro Digital Complex station', '인구밀집지역', 37.48349491938897, 126.89557915984915), ('POI020', '구로역', 'Guro station', '인구밀집지역', 37.501517014484925, 126.88285531403136), ('POI021', '군자역', 'Gunja station', '인구밀집지역', 37.555916439038526, 127.08008933356267), -('POI022', '남구로역', 'Namguro station', '인구밀집지역', 37.484184300878496, 126.88539683418863), +/*('POI022', '남구로역', 'Namguro station', '인구밀집지역', 37.484184300878496, 126.88539683418863),*/ ('POI023', '대림역', 'Daerim station', '인구밀집지역', 37.492818237753816, 126.89567307890546), ('POI024', '동대문역', 'Dongdaemun station', '인구밀집지역', 37.57127393968462, 127.0096854515538), ('POI025', '뚝섬역', 'Ttukseom station', '인구밀집지역', 37.548249670448335, 127.04603079475764), ('POI026', '미아사거리역', 'Miasageori station', '인구밀집지역', 37.61244898963818, 127.0305835226811), ('POI027', '발산역', 'Balsan station', '인구밀집지역', 37.55796811527854, 126.84179723439551), -('POI028', '북한산우이역', 'Bukhansan Ui station', '인구밀집지역', 37.66281154490492, 127.01070580250341), +/*('POI028', '북한산우이역', 'Bukhansan Ui station', '인구밀집지역', 37.66281154490492, 127.01070580250341),*/ ('POI029', '사당역', 'Sadang station', '인구밀집지역', 37.47635049189678, 126.98129519152415), ('POI030', '삼각지역', 'Samgakji station', '인구밀집지역', 37.535323919468816, 126.97392611189508), ('POI031', '서울대입구역', 'Seoul National University station', '인구밀집지역', 37.48060991955129, 126.95360407634334), @@ -55,28 +55,28 @@ INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES ('POI054', '혜화역', 'Hyehwa station', '인구밀집지역', 37.58237426286851, 127.0014404389915), ('POI055', '홍대입구역(2호선)', 'Hongik University station(Line 2)', '인구밀집지역', 37.5560688250626, 126.92329766229786), ('POI056', '회기역', 'Hoegi station', '인구밀집지역', 37.590538258503685, 127.05700039395965), -('POI057', '4·19 카페거리', '4·19 Cafe street', '발달상권', 37.64481185893745, 127.00739701908337), +/*('POI057', '4·19 카페거리', '4·19 Cafe street', '발달상권', 37.64481185893745, 127.00739701908337),*/ ('POI058', '가락시장', 'Garak Market', '발달상권', 37.49445476721721, 127.11054976163454), ('POI059', '가로수길', 'Garosu-gil', '발달상권', 37.522662622516556, 127.02543305960263), ('POI060', '광장(전통)시장', 'Gwangjang(Traditional) Market', '발달상권', 37.569817036518465, 127.00014893169192), ('POI061', '김포공항', 'Gimpo Airport', '발달상권', 37.56228914604604, 126.80289965159135), -('POI062', '낙산공원·이화마을', 'Naksan Park·Ihwa Village', '발달상권', 37.580295002146926, 127.00692469978675), +/*('POI062', '낙산공원·이화마을', 'Naksan Park·Ihwa Village', '발달상권', 37.580295002146926, 127.00692469978675),*/ ('POI063', '노량진', 'Noryangjin', '발달상권', 37.513690699957316, 126.94907070192423), ('POI064', '덕수궁길·정동길', 'Deoksugung-gil·Jeongdong-gil', '발달상권', 37.5659744042236, 126.97244482634493), -('POI065', '방배역 먹자골목', 'Bangbae food alley', '발달상권', 37.48234102283986, 126.99597969571558), +/*('POI065', '방배역 먹자골목', 'Bangbae food alley', '발달상권', 37.48234102283986, 126.99597969571558),*/ ('POI066', '북촌한옥마을', 'Bukchon Hanok Village', '발달상권', 37.58425649416171, 126.98386049085175), ('POI067', '서촌', 'Seochon', '발달상권', 37.58018833392947, 126.9666234325548), ('POI068', '성수카페거리', 'Seongsu Cafe Street', '발달상권', 37.54227657813654, 127.05560237183015), -('POI069', '수유리 먹자골목', 'Suyuri food alley', '발달상권', 37.64270823057644, 127.02733855833522), -('POI070', '쌍문동 맛집거리', 'Ssangmun-dong restaurant street', '발달상권', 37.6477285443038, 127.03242528481013), +/*('POI069', '수유리 먹자골목', 'Suyuri food alley', '발달상권', 37.64270823057644, 127.02733855833522),*/ +('POI070', '쌍문역', 'Ssangmun-dong station', '인구밀집지역', 37.6477285443038, 127.03242528481013), ('POI071', '압구정로데오거리', 'Apgujeong Rodeo Street', '발달상권', 37.52560441860466, 127.04040171511626), ('POI072', '여의도', 'Yeouido', '발달상권', 37.52549260943663, 126.92338937129487), ('POI073', '연남동', 'Yeonnam-dong', '발달상권', 37.561426426204264, 126.92406273242905), ('POI074', '영등포 타임스퀘어', 'Yeongdeungpo Time square', '발달상권', 37.51653179760669, 126.90523931614634), -('POI075', '외대앞', 'Hankuk University of Foreign Studies', '발달상권', 37.59442412843283, 127.06153833102397), +/*('POI075', '외대앞', 'Hankuk University of Foreign Studies', '발달상권', 37.59442412843283, 127.06153833102397),*/ ('POI076', '용리단길', 'Yongnidan-gil', '발달상권', 37.5311056439386, 126.97147359358237), ('POI077', '이태원 앤틱가구거리', 'Itaewon Antiques street', '발달상권', 37.53187270743738, 126.99343376513602), -('POI078', '인사동·익선동', 'Insa-dong·Ikseon-dong', '발달상권', 37.5741577489609, 126.98736374162822), +('POI078', '인사동', 'Insa-dong', '발달상권', 37.5741577489609, 126.98736374162822), ('POI079', '창동 신경제 중심지', 'Changdong New Economic Center', '발달상권', 37.656167961335676, 127.05328808787348), ('POI080', '청담동 명품거리', 'Cheongdam-dong Luxury Fashion street', '발달상권', 37.52645537152884, 127.04354058968364), ('POI081', '청량리 제기동 일대 전통시장', 'Traditional market in Cheongnyangni Jegi-dong', '발달상권', 37.579977738703214, 127.0382790126348), @@ -95,11 +95,11 @@ INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES ('POI094', '망원한강공원', 'Mangwon Hangang Park', '공원', 37.55480087656761, 126.89823749216892), ('POI095', '반포한강공원', 'Banpo Hangang Park', '공원', 37.51065462735107, 126.99457550649858), ('POI096', '북서울꿈의숲', 'Dream Forest', '공원', 37.62244979881656, 127.04069568441815), -('POI097', '불광천', 'Bulgwangcheon River', '공원', 37.58940646530798, 126.91194122899687), +/*('POI097', '불광천', 'Bulgwangcheon River', '공원', 37.58940646530798, 126.91194122899687),*/ ('POI098', '서리풀공원·몽마르뜨공원', 'Seoripul Park·Montmartre Park', '공원', 37.49022054658995, 127.00173655775917), +('POI099', '서울광장', 'Seoul Plaza', '공원', 37.5655010296151, 126.97798702124584), ('POI100', '서울대공원', 'Seoul Grand Park', '공원', 37.42839607329216, 127.01863790236284), ('POI101', '서울숲공원', 'Seoul Forest', '공원', 37.54402680167043, 127.03673083133076), -('POI099', '서울광장', 'Seoul Plaza', '공원', 37.5655010296151, 126.97798702124584), ('POI102', '아차산', 'Achasan', '공원', 37.56307021845223, 127.10047918048643), ('POI103', '양화한강공원', 'Yanghwa Hangang Park', '공원', 37.54029653900909, 126.89963558394624), ('POI104', '어린이대공원', 'Children''s Grand Park', '공원', 37.54957100980681, 127.0824313180458), @@ -114,5 +114,17 @@ INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES ('POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264), ('POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616), ('POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821), -('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286) +('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286), +('POI117', '신정네거리역', 'Shinjeongnegeori Station', '인구밀집지역', 37.521121, 126.855010), +('POI118', '잠실새내역', 'Jamsil Saenae Station', '인구밀집지역', 37.510417, 127.083309), +('POI119', '잠실역', 'Jamsil Station', '인구밀집지역', 37.512077, 127.101158), +('POI120', '잠실롯데타워 일대', 'Jamsil Lotte Tower area', '발달상권', 37.511679, 127.104408), +('POI121', '송리단길·호수단길', 'Songridan-gil·Hosudan-gil', '발달상권', 37.508292, 127.106789), +('POI122', '신촌 스타광장', 'Shinchon Star Plaza', '발달상권', 37.556476, 126.936840), +('POI123', '보라매공원', 'Boramae Park', '공원', 37.492817, 126.919840), +('POI124', '서대문독립공원', 'Seodaemun Independence Park', '공원', 37.574265, 126.956435), +('POI125', '안양천', 'Anyangcheon River', '공원', 37.519953, 126.879723), +('POI126', '여의서로', 'Yeouiseoro', '공원', 37.533417, 126.911992), +('POI127', '올림픽공원', 'Olympic Park', '공원', 37.519754, 127.123176), +('POI128', '홍제폭포', 'Hongje Waterfall', '공원', 37.580613, 126.937246) ON CONFLICT (seoul_id) DO NOTHING; \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java index 0d90bac..aa46b2f 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java @@ -65,7 +65,7 @@ public ResponseEntity suggestTourPlan(@PathVariable String user_id, @RequestB HttpEntity entity = new HttpEntity<>(suggestFastRequestDto, headers); try { - ResponseEntity response = restTemplate.postForEntity(fastApiUrl + "${user_id}", entity, SuggestFastResponseDto.class); + ResponseEntity response = restTemplate.postForEntity(fastApiUrl + user_id, entity, SuggestFastResponseDto.class); if (response.getStatusCode() == HttpStatus.OK) { SuggestFastResponseDto suggestFastResponseDto = response.getBody(); @@ -108,7 +108,7 @@ public ResponseEntity getPrevChatsForPlan(@PathVariable String user_id) { } try { - ResponseEntity responseEntity = restTemplate.getForEntity(fastApiUrl + "${user_id}", SuggestFastResponseDto[].class); + ResponseEntity responseEntity = restTemplate.getForEntity(fastApiUrl + user_id, SuggestFastResponseDto[].class); SuggestFastResponseDto[] responseArray = responseEntity.getBody(); if (responseArray == null) { From 3db1610f3e9ffb59e6011d3f6a168e973cb020eb Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Sat, 17 May 2025 14:57:30 +0900 Subject: [PATCH 321/361] =?UTF-8?q?=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=8B=9C=EA=B0=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/example/userservice/entity/TokenRedis.java | 4 ++-- .../java/com/example/userservice/jwt/JwtTokenProvider.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java b/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java index 19affd3..e0b2e9d 100644 --- a/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java +++ b/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java @@ -8,7 +8,7 @@ @Getter @AllArgsConstructor -@RedisHash(value = "token", timeToLive = 60 * 60 * 24 * 7) // 리프레시토큰과 expiretime 일치 +@RedisHash(value = "token", timeToLive = 60 * 45) // 리프레시토큰 유효기간 45분 public class TokenRedis { @Id @@ -18,4 +18,4 @@ public class TokenRedis { private String accessToken; private String refreshToken; -} \ No newline at end of file +} diff --git a/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java b/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java index ae40772..dac2573 100644 --- a/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java +++ b/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java @@ -42,7 +42,7 @@ public String generateAccessToken(String userId) { // nickname 대신 userId public String generateRefreshToken(String userId) { // nickname 대신 userId 사용 Date now = new Date(); - Date expiryDate = new Date(now.getTime() + 1000L * 60 * 60 * 24 * 7); // 7일 + Date expiryDate = new Date(now.getTime() + 1000 * 60 * 45); // 45분 return Jwts.builder() .setSubject(userId) // userId로 변경 From e457e9bcf07dc8c6ee684932b746bed293d8b3cb Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Sat, 17 May 2025 15:02:47 +0900 Subject: [PATCH 322/361] =?UTF-8?q?zipkin=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user-service/build.gradle | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/user-service/build.gradle b/user-service/build.gradle index 0933db3..b54536a 100644 --- a/user-service/build.gradle +++ b/user-service/build.gradle @@ -51,10 +51,8 @@ dependencies { implementation 'io.github.cdimascio:java-dotenv:5.2.2' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' - // Micrometer 기반 Zipkin 연동 (대체 Sleuth + Zipkin) - implementation 'io.micrometer:micrometer-tracing-bridge-brave' - implementation 'io.zipkin.reporter2:zipkin-reporter-brave' - + implementation 'org.springframework.cloud:spring-cloud-starter-sleuth:3.1.3' + implementation 'org.springframework.cloud:spring-cloud-starter-zipkin:2.2.8.RELEASE' // DB runtimeOnly 'org.postgresql:postgresql' From 7154961ff7ca080f7afe69125d411557a7128773 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Sat, 17 May 2025 15:35:14 +0900 Subject: [PATCH 323/361] =?UTF-8?q?area=20=EC=B6=94=EA=B0=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20=EC=B6=94=EC=B2=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/placeservice/entity/Area.java | 1 - place-service/src/main/resources/data.sql | 258 +++++++++--------- .../controller/SuggestController.java | 2 +- 3 files changed, 130 insertions(+), 131 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/entity/Area.java b/place-service/src/main/java/com/example/placeservice/entity/Area.java index 808d938..5c04beb 100644 --- a/place-service/src/main/java/com/example/placeservice/entity/Area.java +++ b/place-service/src/main/java/com/example/placeservice/entity/Area.java @@ -12,7 +12,6 @@ @Data public class Area { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "area_id") private Long areaId; diff --git a/place-service/src/main/resources/data.sql b/place-service/src/main/resources/data.sql index 963f5c7..8e1d998 100644 --- a/place-service/src/main/resources/data.sql +++ b/place-service/src/main/resources/data.sql @@ -1,130 +1,130 @@ -INSERT INTO area (seoul_id, name, name_eng, category, lat, lon) VALUES -('POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), -('POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), -('POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), -('POI004', '이태원 관광특구', 'Itaewon Special Tourist Zone', '관광특구', 37.53475718815058, 126.99591956660342), -('POI005', '잠실 관광특구', 'Jamsil Special Tourist Zone', '관광특구', 37.51868923685722, 127.11694379878114), -('POI006', '종로·청계 관광특구', 'Jongno Cheonggye Special Toruist Zone', '관광특구', 37.56984229024498, 126.99866678943675), -('POI007', '홍대 관광특구', 'HongDae Culture & Arts Special Tourist Zone', '관광특구', 37.55506170187393, 126.92159115672915), -('POI008', '경복궁', 'Gyeongbokgung Palace', '고궁·문화유산', 37.5809150911806, 126.97764726625209), -('POI009', '광화문·덕수궁', 'Gwanghwamun & Deoksugung Palace', '고궁·문화유산', 37.56847081447724, 126.97620594802834), -('POI010', '보신각', 'Bosingak', '고궁·문화유산', 37.57089797616037, 126.98337607511172), -('POI011', '서울 암사동 유적', 'Amsa Prehistoric Settlement Site', '고궁·문화유산', 37.56102809827398, 127.13078491934499), -('POI012', '창덕궁·종묘', 'Changdeokgung Palace & Jongmyo Shrine', '고궁·문화유산', 37.5795304952381, 126.99416802285715), -('POI013', '가산디지털단지역', 'Gasan Digital Complex station', '인구밀집지역', 37.48267268376068, 126.88014526210826), -('POI014', '강남역', 'Gangnam station', '인구밀집지역', 37.50019794543511, 127.02822145618981), -('POI015', '건대입구역', 'Konkuk University station', '인구밀집지역', 37.540005312302846, 127.06718185804415), -('POI016', '고덕역', 'Godeok station', '인구밀집지역', 37.553377141111774, 127.15503341723486), -('POI017', '고속터미널역', 'Express Bus Terminal station', '인구밀집지역', 37.50472316731823, 127.00542276570697), -('POI018', '교대역', 'Seoul National University of Education station', '인구밀집지역', 37.49185444557083, 127.01456630812736), -('POI019', '구로디지털단지역', 'Guro Digital Complex station', '인구밀집지역', 37.48349491938897, 126.89557915984915), -('POI020', '구로역', 'Guro station', '인구밀집지역', 37.501517014484925, 126.88285531403136), -('POI021', '군자역', 'Gunja station', '인구밀집지역', 37.555916439038526, 127.08008933356267), -/*('POI022', '남구로역', 'Namguro station', '인구밀집지역', 37.484184300878496, 126.88539683418863),*/ -('POI023', '대림역', 'Daerim station', '인구밀집지역', 37.492818237753816, 126.89567307890546), -('POI024', '동대문역', 'Dongdaemun station', '인구밀집지역', 37.57127393968462, 127.0096854515538), -('POI025', '뚝섬역', 'Ttukseom station', '인구밀집지역', 37.548249670448335, 127.04603079475764), -('POI026', '미아사거리역', 'Miasageori station', '인구밀집지역', 37.61244898963818, 127.0305835226811), -('POI027', '발산역', 'Balsan station', '인구밀집지역', 37.55796811527854, 126.84179723439551), -/*('POI028', '북한산우이역', 'Bukhansan Ui station', '인구밀집지역', 37.66281154490492, 127.01070580250341),*/ -('POI029', '사당역', 'Sadang station', '인구밀집지역', 37.47635049189678, 126.98129519152415), -('POI030', '삼각지역', 'Samgakji station', '인구밀집지역', 37.535323919468816, 126.97392611189508), -('POI031', '서울대입구역', 'Seoul National University station', '인구밀집지역', 37.48060991955129, 126.95360407634334), -('POI032', '서울식물원·마곡나루역', 'Seoul Botanic Park·Magongnaru station', '인구밀집지역', 37.56745047572558, 126.82986126533689), -('POI033', '서울역', 'Seoul station', '인구밀집지역', 37.55662336923077, 126.97216878769233), -('POI034', '선릉역', 'Seolleung station', '인구밀집지역', 37.50584166753116, 127.0489100692203), -('POI035', '성신여대입구역', 'Sungshin Women''s University station', '인구밀집지역', 37.59262801654558, 127.01658994286346), -('POI036', '수유역', 'Suyu station', '인구밀집지역', 37.641215514705884, 127.02565393627451), -('POI037', '신논현역·논현역', 'Sinnonhyeon·Nonhyeon station', '인구밀집지역', 37.50848054554439, 127.02291493850254), -('POI038', '신도림역', 'Sindorim station', '인구밀집지역', 37.50941493666961, 126.89096014067083), -('POI039', '신림역', 'Sillim station', '인구밀집지역', 37.48396968904173, 126.92987028995233), -('POI040', '신촌·이대역', 'Sinchon·Ewha Womans University station', '인구밀집지역', 37.55564146783625, 126.93761215497075), -('POI041', '양재역', 'Yangjae station', '인구밀집지역', 37.48451422353994, 127.03551115644439), -('POI042', '역삼역', 'Yeoksam station', '인구밀집지역', 37.50011177778084, 127.03721339389585), -('POI043', '연신내역', 'Yeonsinnae station', '인구밀집지역', 37.61903547457627, 126.92180184322034), -('POI044', '오목교역·목동운동장', 'Omokgyo·Mok-dong Stadium station', '인구밀집지역', 37.53174788866626, 126.87924768246015), -('POI045', '왕십리역', 'Wangsimni station', '인구밀집지역', 37.561636745463275, 127.03857508984515), -('POI046', '용산역', 'Yongsan station', '인구밀집지역', 37.53059674809017, 126.95953798376128), -('POI047', '이태원역', 'Itaewon station', '인구밀집지역', 37.534847968613946, 126.99261390234051), -('POI048', '장지역', 'Jangji station', '인구밀집지역', 37.47861018962738, 127.12424263037641), -('POI049', '장한평역', 'Janghanpyeong station', '인구밀집지역', 37.562849061361526, 127.06235115915185), -('POI050', '천호역', 'Cheonho station', '인구밀집지역', 37.53921065146112, 127.12470374198884), -('POI051', '총신대입구(이수)역', 'Chongshin University(Isu) station', '인구밀집지역', 37.48677821828824, 126.9807539648491), -('POI052', '충정로역', 'Chungjeongno station', '인구밀집지역', 37.559524839900476, 126.9642059673645), -('POI053', '합정역', 'Hapjeong station', '인구밀집지역', 37.54857307399022, 126.91084543497917), -('POI054', '혜화역', 'Hyehwa station', '인구밀집지역', 37.58237426286851, 127.0014404389915), -('POI055', '홍대입구역(2호선)', 'Hongik University station(Line 2)', '인구밀집지역', 37.5560688250626, 126.92329766229786), -('POI056', '회기역', 'Hoegi station', '인구밀집지역', 37.590538258503685, 127.05700039395965), -/*('POI057', '4·19 카페거리', '4·19 Cafe street', '발달상권', 37.64481185893745, 127.00739701908337),*/ -('POI058', '가락시장', 'Garak Market', '발달상권', 37.49445476721721, 127.11054976163454), -('POI059', '가로수길', 'Garosu-gil', '발달상권', 37.522662622516556, 127.02543305960263), -('POI060', '광장(전통)시장', 'Gwangjang(Traditional) Market', '발달상권', 37.569817036518465, 127.00014893169192), -('POI061', '김포공항', 'Gimpo Airport', '발달상권', 37.56228914604604, 126.80289965159135), -/*('POI062', '낙산공원·이화마을', 'Naksan Park·Ihwa Village', '발달상권', 37.580295002146926, 127.00692469978675),*/ -('POI063', '노량진', 'Noryangjin', '발달상권', 37.513690699957316, 126.94907070192423), -('POI064', '덕수궁길·정동길', 'Deoksugung-gil·Jeongdong-gil', '발달상권', 37.5659744042236, 126.97244482634493), -/*('POI065', '방배역 먹자골목', 'Bangbae food alley', '발달상권', 37.48234102283986, 126.99597969571558),*/ -('POI066', '북촌한옥마을', 'Bukchon Hanok Village', '발달상권', 37.58425649416171, 126.98386049085175), -('POI067', '서촌', 'Seochon', '발달상권', 37.58018833392947, 126.9666234325548), -('POI068', '성수카페거리', 'Seongsu Cafe Street', '발달상권', 37.54227657813654, 127.05560237183015), -/*('POI069', '수유리 먹자골목', 'Suyuri food alley', '발달상권', 37.64270823057644, 127.02733855833522),*/ -('POI070', '쌍문역', 'Ssangmun-dong station', '인구밀집지역', 37.6477285443038, 127.03242528481013), -('POI071', '압구정로데오거리', 'Apgujeong Rodeo Street', '발달상권', 37.52560441860466, 127.04040171511626), -('POI072', '여의도', 'Yeouido', '발달상권', 37.52549260943663, 126.92338937129487), -('POI073', '연남동', 'Yeonnam-dong', '발달상권', 37.561426426204264, 126.92406273242905), -('POI074', '영등포 타임스퀘어', 'Yeongdeungpo Time square', '발달상권', 37.51653179760669, 126.90523931614634), -/*('POI075', '외대앞', 'Hankuk University of Foreign Studies', '발달상권', 37.59442412843283, 127.06153833102397),*/ -('POI076', '용리단길', 'Yongnidan-gil', '발달상권', 37.5311056439386, 126.97147359358237), -('POI077', '이태원 앤틱가구거리', 'Itaewon Antiques street', '발달상권', 37.53187270743738, 126.99343376513602), -('POI078', '인사동', 'Insa-dong', '발달상권', 37.5741577489609, 126.98736374162822), -('POI079', '창동 신경제 중심지', 'Changdong New Economic Center', '발달상권', 37.656167961335676, 127.05328808787348), -('POI080', '청담동 명품거리', 'Cheongdam-dong Luxury Fashion street', '발달상권', 37.52645537152884, 127.04354058968364), -('POI081', '청량리 제기동 일대 전통시장', 'Traditional market in Cheongnyangni Jegi-dong', '발달상권', 37.579977738703214, 127.0382790126348), -('POI082', '해방촌·경리단길', 'Haebangchon·Gyeongnidan-gil', '발달상권', 37.54143829714048, 126.98836800969178), -('POI083', 'DDP(동대문디자인플라자)', 'DDP(Dongdaemun Design Plaza)', '발달상권', 37.56654201564661, 127.00993045294622), -('POI084', 'DMC(디지털미디어시티)', 'DMC(Digital Media City)', '발달상권', 37.577407110655734, 126.8936691147541), -('POI085', '강서한강공원', 'Gangseo Hangang Park', '공원', 37.5858708295553, 126.82002478578644), -('POI086', '고척돔', 'Gocheok Dome', '공원', 37.49776790791694, 126.86713336195083), -('POI087', '광나루한강공원', 'Gwangnaru Hangang Park', '공원', 37.550866089267345, 127.12370647922764), -('POI088', '광화문광장', 'Gwanghwamun Square', '공원', 37.57505062349029, 126.97680679071502), -('POI089', '국립중앙박물관·용산가족공원', 'The National Museum of Korea·Yongsan Family Park', '공원', 37.52202119313341, 126.9824679850178), -('POI090', '난지한강공원', 'Nanji Hangang Park', '공원', 37.566316740172454, 126.87770001597329), -('POI091', '남산공원', 'Namsan Park', '공원', 37.555070650369004, 126.99365012730628), -('POI092', '노들섬', 'Nodeul island', '공원', 37.517556872131706, 126.95871397855296), -('POI093', '뚝섬한강공원', 'Ttukseom Hangang Park', '공원', 37.52882503002481, 127.07460742502332), -('POI094', '망원한강공원', 'Mangwon Hangang Park', '공원', 37.55480087656761, 126.89823749216892), -('POI095', '반포한강공원', 'Banpo Hangang Park', '공원', 37.51065462735107, 126.99457550649858), -('POI096', '북서울꿈의숲', 'Dream Forest', '공원', 37.62244979881656, 127.04069568441815), -/*('POI097', '불광천', 'Bulgwangcheon River', '공원', 37.58940646530798, 126.91194122899687),*/ -('POI098', '서리풀공원·몽마르뜨공원', 'Seoripul Park·Montmartre Park', '공원', 37.49022054658995, 127.00173655775917), -('POI099', '서울광장', 'Seoul Plaza', '공원', 37.5655010296151, 126.97798702124584), -('POI100', '서울대공원', 'Seoul Grand Park', '공원', 37.42839607329216, 127.01863790236284), -('POI101', '서울숲공원', 'Seoul Forest', '공원', 37.54402680167043, 127.03673083133076), -('POI102', '아차산', 'Achasan', '공원', 37.56307021845223, 127.10047918048643), -('POI103', '양화한강공원', 'Yanghwa Hangang Park', '공원', 37.54029653900909, 126.89963558394624), -('POI104', '어린이대공원', 'Children''s Grand Park', '공원', 37.54957100980681, 127.0824313180458), -('POI105', '여의도한강공원', 'Yeouido Hangang Park', '공원', 37.52868909088513, 126.92627587984478), -('POI106', '월드컵공원', 'World Cup Park', '공원', 37.5660391150387, 126.88740201899091), -('POI107', '응봉산', 'Eungbongsan', '공원', 37.54797346368485, 127.02904643771303), -('POI108', '이촌한강공원', 'Ichon Hangang Park', '공원', 37.52139035897837, 126.96356631928725), -('POI109', '잠실종합운동장', 'Jamsil (Seoul) Sports Complex', '공원', 37.51610665876664, 127.07230325047283), -('POI110', '잠실한강공원', 'Jamsil Hangang Park', '공원', 37.51981181577704, 127.08507449607517), -('POI111', '잠원한강공원', 'Jamwon Hangang Park', '공원', 37.52336245003122, 127.01397131593632), -('POI112', '청계산', 'Cheonggyesan', '공원', 37.443365424959026, 127.0469697170313), -('POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264), -('POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616), -('POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821), -('POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286), -('POI117', '신정네거리역', 'Shinjeongnegeori Station', '인구밀집지역', 37.521121, 126.855010), -('POI118', '잠실새내역', 'Jamsil Saenae Station', '인구밀집지역', 37.510417, 127.083309), -('POI119', '잠실역', 'Jamsil Station', '인구밀집지역', 37.512077, 127.101158), -('POI120', '잠실롯데타워 일대', 'Jamsil Lotte Tower area', '발달상권', 37.511679, 127.104408), -('POI121', '송리단길·호수단길', 'Songridan-gil·Hosudan-gil', '발달상권', 37.508292, 127.106789), -('POI122', '신촌 스타광장', 'Shinchon Star Plaza', '발달상권', 37.556476, 126.936840), -('POI123', '보라매공원', 'Boramae Park', '공원', 37.492817, 126.919840), -('POI124', '서대문독립공원', 'Seodaemun Independence Park', '공원', 37.574265, 126.956435), -('POI125', '안양천', 'Anyangcheon River', '공원', 37.519953, 126.879723), -('POI126', '여의서로', 'Yeouiseoro', '공원', 37.533417, 126.911992), -('POI127', '올림픽공원', 'Olympic Park', '공원', 37.519754, 127.123176), -('POI128', '홍제폭포', 'Hongje Waterfall', '공원', 37.580613, 126.937246) +INSERT INTO area (area_id, seoul_id, name, name_eng, category, lat, lon) VALUES +(1, 'POI001', '강남 MICE 관광특구', 'Gangnam MICE Special Tourist Zone', '관광특구', 37.5112843816696, 127.06005155384705), +(2, 'POI002', '동대문 관광특구', 'Dongdaemun Fashion Town Special Tourist Zone', '관광특구', 37.567214101532834, 127.00755049391576), +(3, 'POI003', '명동 관광특구', 'Myeong-dong Namdaemun Bukchang-dong Da-dong Mugyo-dong Special Tourist Zone', '관광특구', 37.56414013472864, 126.98288393316864), +(4, 'POI004', '이태원 관광특구', 'Itaewon Special Tourist Zone', '관광특구', 37.53475718815058, 126.99591956660342), +(5, 'POI005', '잠실 관광특구', 'Jamsil Special Tourist Zone', '관광특구', 37.51868923685722, 127.11694379878114), +(6, 'POI006', '종로·청계 관광특구', 'Jongno Cheonggye Special Toruist Zone', '관광특구', 37.56984229024498, 126.99866678943675), +(7, 'POI007', '홍대 관광특구', 'HongDae Culture & Arts Special Tourist Zone', '관광특구', 37.55506170187393, 126.92159115672915), +(8, 'POI008', '경복궁', 'Gyeongbokgung Palace', '고궁·문화유산', 37.5809150911806, 126.97764726625209), +(9, 'POI009', '광화문·덕수궁', 'Gwanghwamun & Deoksugung Palace', '고궁·문화유산', 37.56847081447724, 126.97620594802834), +(10, 'POI010', '보신각', 'Bosingak', '고궁·문화유산', 37.57089797616037, 126.98337607511172), +(11, 'POI011', '서울 암사동 유적', 'Amsa Prehistoric Settlement Site', '고궁·문화유산', 37.56102809827398, 127.13078491934499), +(12, 'POI012', '창덕궁·종묘', 'Changdeokgung Palace & Jongmyo Shrine', '고궁·문화유산', 37.5795304952381, 126.99416802285715), +(13, 'POI013', '가산디지털단지역', 'Gasan Digital Complex station', '인구밀집지역', 37.48267268376068, 126.88014526210826), +(14, 'POI014', '강남역', 'Gangnam station', '인구밀집지역', 37.50019794543511, 127.02822145618981), +(15, 'POI015', '건대입구역', 'Konkuk University station', '인구밀집지역', 37.540005312302846, 127.06718185804415), +(16, 'POI016', '고덕역', 'Godeok station', '인구밀집지역', 37.553377141111774, 127.15503341723486), +(17, 'POI017', '고속터미널역', 'Express Bus Terminal station', '인구밀집지역', 37.50472316731823, 127.00542276570697), +(18, 'POI018', '교대역', 'Seoul National University of Education station', '인구밀집지역', 37.49185444557083, 127.01456630812736), +(19, 'POI019', '구로디지털단지역', 'Guro Digital Complex station', '인구밀집지역', 37.48349491938897, 126.89557915984915), +(20, 'POI020', '구로역', 'Guro station', '인구밀집지역', 37.501517014484925, 126.88285531403136), +(21, 'POI021', '군자역', 'Gunja station', '인구밀집지역', 37.555916439038526, 127.08008933356267), +/*(22, 'POI022', '남구로역', 'Namguro station', '인구밀집지역', 37.484184300878496, 126.88539683418863),*/ +(23, 'POI023', '대림역', 'Daerim station', '인구밀집지역', 37.492818237753816, 126.89567307890546), +(24, 'POI024', '동대문역', 'Dongdaemun station', '인구밀집지역', 37.57127393968462, 127.0096854515538), +(25, 'POI025', '뚝섬역', 'Ttukseom station', '인구밀집지역', 37.548249670448335, 127.04603079475764), +(26, 'POI026', '미아사거리역', 'Miasageori station', '인구밀집지역', 37.61244898963818, 127.0305835226811), +(27, 'POI027', '발산역', 'Balsan station', '인구밀집지역', 37.55796811527854, 126.84179723439551), +/*(28, 'POI028', '북한산우이역', 'Bukhansan Ui station', '인구밀집지역', 37.66281154490492, 127.01070580250341),*/ +(29, 'POI029', '사당역', 'Sadang station', '인구밀집지역', 37.47635049189678, 126.98129519152415), +(30, 'POI030', '삼각지역', 'Samgakji station', '인구밀집지역', 37.535323919468816, 126.97392611189508), +(31, 'POI031', '서울대입구역', 'Seoul National University station', '인구밀집지역', 37.48060991955129, 126.95360407634334), +(32, 'POI032', '서울식물원·마곡나루역', 'Seoul Botanic Park·Magongnaru station', '인구밀집지역', 37.56745047572558, 126.82986126533689), +(33, 'POI033', '서울역', 'Seoul station', '인구밀집지역', 37.55662336923077, 126.97216878769233), +(34, 'POI034', '선릉역', 'Seolleung station', '인구밀집지역', 37.50584166753116, 127.0489100692203), +(35, 'POI035', '성신여대입구역', 'Sungshin Women''s University station', '인구밀집지역', 37.59262801654558, 127.01658994286346), +(36, 'POI036', '수유역', 'Suyu station', '인구밀집지역', 37.641215514705884, 127.02565393627451), +(37, 'POI037', '신논현역·논현역', 'Sinnonhyeon·Nonhyeon station', '인구밀집지역', 37.50848054554439, 127.02291493850254), +(38, 'POI038', '신도림역', 'Sindorim station', '인구밀집지역', 37.50941493666961, 126.89096014067083), +(39, 'POI039', '신림역', 'Sillim station', '인구밀집지역', 37.48396968904173, 126.92987028995233), +(40, 'POI040', '신촌·이대역', 'Sinchon·Ewha Womans University station', '인구밀집지역', 37.55564146783625, 126.93761215497075), +(41, 'POI041', '양재역', 'Yangjae station', '인구밀집지역', 37.48451422353994, 127.03551115644439), +(42, 'POI042', '역삼역', 'Yeoksam station', '인구밀집지역', 37.50011177778084, 127.03721339389585), +(43, 'POI043', '연신내역', 'Yeonsinnae station', '인구밀집지역', 37.61903547457627, 126.92180184322034), +(44, 'POI044', '오목교역·목동운동장', 'Omokgyo·Mok-dong Stadium station', '인구밀집지역', 37.53174788866626, 126.87924768246015), +(45, 'POI045', '왕십리역', 'Wangsimni station', '인구밀집지역', 37.561636745463275, 127.03857508984515), +(46, 'POI046', '용산역', 'Yongsan station', '인구밀집지역', 37.53059674809017, 126.95953798376128), +(47, 'POI047', '이태원역', 'Itaewon station', '인구밀집지역', 37.534847968613946, 126.99261390234051), +(48, 'POI048', '장지역', 'Jangji station', '인구밀집지역', 37.47861018962738, 127.12424263037641), +(49, 'POI049', '장한평역', 'Janghanpyeong station', '인구밀집지역', 37.562849061361526, 127.06235115915185), +(50, 'POI050', '천호역', 'Cheonho station', '인구밀집지역', 37.53921065146112, 127.12470374198884), +(51, 'POI051', '총신대입구(이수)역', 'Chongshin University(Isu) station', '인구밀집지역', 37.48677821828824, 126.9807539648491), +(52, 'POI052', '충정로역', 'Chungjeongno station', '인구밀집지역', 37.559524839900476, 126.9642059673645), +(53, 'POI053', '합정역', 'Hapjeong station', '인구밀집지역', 37.54857307399022, 126.91084543497917), +(54, 'POI054', '혜화역', 'Hyehwa station', '인구밀집지역', 37.58237426286851, 127.0014404389915), +(55, 'POI055', '홍대입구역(2호선)', 'Hongik University station(Line 2)', '인구밀집지역', 37.5560688250626, 126.92329766229786), +(56, 'POI056', '회기역', 'Hoegi station', '인구밀집지역', 37.590538258503685, 127.05700039395965), +/*(57, 'POI057', '4·19 카페거리', '4·19 Cafe street', '발달상권', 37.64481185893745, 127.00739701908337),*/ +(58, 'POI058', '가락시장', 'Garak Market', '발달상권', 37.49445476721721, 127.11054976163454), +(59, 'POI059', '가로수길', 'Garosu-gil', '발달상권', 37.522662622516556, 127.02543305960263), +(60, 'POI060', '광장(전통)시장', 'Gwangjang(Traditional) Market', '발달상권', 37.569817036518465, 127.00014893169192), +(61, 'POI061', '김포공항', 'Gimpo Airport', '발달상권', 37.56228914604604, 126.80289965159135), +/*(62, 'POI062', '낙산공원·이화마을', 'Naksan Park·Ihwa Village', '발달상권', 37.580295002146926, 127.00692469978675),*/ +(63, 'POI063', '노량진', 'Noryangjin', '발달상권', 37.513690699957316, 126.94907070192423), +(64, 'POI064', '덕수궁길·정동길', 'Deoksugung-gil·Jeongdong-gil', '발달상권', 37.5659744042236, 126.97244482634493), +/*(65, 'POI065', '방배역 먹자골목', 'Bangbae food alley', '발달상권', 37.48234102283986, 126.99597969571558),*/ +(66, 'POI066', '북촌한옥마을', 'Bukchon Hanok Village', '발달상권', 37.58425649416171, 126.98386049085175), +(67, 'POI067', '서촌', 'Seochon', '발달상권', 37.58018833392947, 126.9666234325548), +(68, 'POI068', '성수카페거리', 'Seongsu Cafe Street', '발달상권', 37.54227657813654, 127.05560237183015), +/*(69, 'POI069', '수유리 먹자골목', 'Suyuri food alley', '발달상권', 37.64270823057644, 127.02733855833522),*/ +(70, 'POI070', '쌍문역', 'Ssangmun-dong station', '인구밀집지역', 37.6477285443038, 127.03242528481013), +(71, 'POI071', '압구정로데오거리', 'Apgujeong Rodeo Street', '발달상권', 37.52560441860466, 127.04040171511626), +(72, 'POI072', '여의도', 'Yeouido', '발달상권', 37.52549260943663, 126.92338937129487), +(73, 'POI073', '연남동', 'Yeonnam-dong', '발달상권', 37.561426426204264, 126.92406273242905), +(74, 'POI074', '영등포 타임스퀘어', 'Yeongdeungpo Time square', '발달상권', 37.51653179760669, 126.90523931614634), +/*(75, 'POI075', '외대앞', 'Hankuk University of Foreign Studies', '발달상권', 37.59442412843283, 127.06153833102397),*/ +(76, 'POI076', '용리단길', 'Yongnidan-gil', '발달상권', 37.5311056439386, 126.97147359358237), +(77, 'POI077', '이태원 앤틱가구거리', 'Itaewon Antiques street', '발달상권', 37.53187270743738, 126.99343376513602), +(78, 'POI078', '인사동', 'Insa-dong', '발달상권', 37.5741577489609, 126.98736374162822), +(79, 'POI079', '창동 신경제 중심지', 'Changdong New Economic Center', '발달상권', 37.656167961335676, 127.05328808787348), +(80, 'POI080', '청담동 명품거리', 'Cheongdam-dong Luxury Fashion street', '발달상권', 37.52645537152884, 127.04354058968364), +(81, 'POI081', '청량리 제기동 일대 전통시장', 'Traditional market in Cheongnyangni Jegi-dong', '발달상권', 37.579977738703214, 127.0382790126348), +(82, 'POI082', '해방촌·경리단길', 'Haebangchon·Gyeongnidan-gil', '발달상권', 37.54143829714048, 126.98836800969178), +(83, 'POI083', 'DDP(동대문디자인플라자)', 'DDP(Dongdaemun Design Plaza)', '발달상권', 37.56654201564661, 127.00993045294622), +(84, 'POI084', 'DMC(디지털미디어시티)', 'DMC(Digital Media City)', '발달상권', 37.577407110655734, 126.8936691147541), +(85, 'POI085', '강서한강공원', 'Gangseo Hangang Park', '공원', 37.5858708295553, 126.82002478578644), +(86, 'POI086', '고척돔', 'Gocheok Dome', '공원', 37.49776790791694, 126.86713336195083), +(87, 'POI087', '광나루한강공원', 'Gwangnaru Hangang Park', '공원', 37.550866089267345, 127.12370647922764), +(88, 'POI088', '광화문광장', 'Gwanghwamun Square', '공원', 37.57505062349029, 126.97680679071502), +(89, 'POI089', '국립중앙박물관·용산가족공원', 'The National Museum of Korea·Yongsan Family Park', '공원', 37.52202119313341, 126.9824679850178), +(90, 'POI090', '난지한강공원', 'Nanji Hangang Park', '공원', 37.566316740172454, 126.87770001597329), +(91, 'POI091', '남산공원', 'Namsan Park', '공원', 37.555070650369004, 126.99365012730628), +(92, 'POI092', '노들섬', 'Nodeul island', '공원', 37.517556872131706, 126.95871397855296), +(93, 'POI093', '뚝섬한강공원', 'Ttukseom Hangang Park', '공원', 37.52882503002481, 127.07460742502332), +(94, 'POI094', '망원한강공원', 'Mangwon Hangang Park', '공원', 37.55480087656761, 126.89823749216892), +(95, 'POI095', '반포한강공원', 'Banpo Hangang Park', '공원', 37.51065462735107, 126.99457550649858), +(96, 'POI096', '북서울꿈의숲', 'Dream Forest', '공원', 37.62244979881656, 127.04069568441815), +/*(97, 'POI097', '불광천', 'Bulgwangcheon River', '공원', 37.58940646530798, 126.91194122899687),*/ +(98, 'POI098', '서리풀공원·몽마르뜨공원', 'Seoripul Park·Montmartre Park', '공원', 37.49022054658995, 127.00173655775917), +(99, 'POI100', '서울대공원', 'Seoul Grand Park', '공원', 37.42839607329216, 127.01863790236284), +(100, 'POI101', '서울숲공원', 'Seoul Forest', '공원', 37.54402680167043, 127.03673083133076), +(101, 'POI099', '서울광장', 'Seoul Plaza', '공원', 37.5655010296151, 126.97798702124584), +(102, 'POI102', '아차산', 'Achasan', '공원', 37.56307021845223, 127.10047918048643), +(103, 'POI103', '양화한강공원', 'Yanghwa Hangang Park', '공원', 37.54029653900909, 126.89963558394624), +(104, 'POI104', '어린이대공원', 'Children''s Grand Park', '공원', 37.54957100980681, 127.0824313180458), +(105, 'POI105', '여의도한강공원', 'Yeouido Hangang Park', '공원', 37.52868909088513, 126.92627587984478), +(106, 'POI106', '월드컵공원', 'World Cup Park', '공원', 37.5660391150387, 126.88740201899091), +(107, 'POI107', '응봉산', 'Eungbongsan', '공원', 37.54797346368485, 127.02904643771303), +(108, 'POI108', '이촌한강공원', 'Ichon Hangang Park', '공원', 37.52139035897837, 126.96356631928725), +(109, 'POI109', '잠실종합운동장', 'Jamsil (Seoul) Sports Complex', '공원', 37.51610665876664, 127.07230325047283), +(110, 'POI110', '잠실한강공원', 'Jamsil Hangang Park', '공원', 37.51981181577704, 127.08507449607517), +(111, 'POI111', '잠원한강공원', 'Jamwon Hangang Park', '공원', 37.52336245003122, 127.01397131593632), +(112, 'POI112', '청계산', 'Cheonggyesan', '공원', 37.443365424959026, 127.0469697170313), +(113, 'POI113', '청와대', 'Cheongwadae', '공원', 37.58508367407448, 126.97751023085264), +(114, 'POI114', '북창동 먹자골목', 'Bukchang-dong food alley', '발달상권', 37.56181572564952, 126.97747845604616), +(115, 'POI115', '남대문시장', 'Namdaemun Market', '발달상권', 37.559671004373214, 126.97854410558821), +(116, 'POI116', '익선동', 'Ikseon-dong', '발달상권', 37.5725556, 126.9895286), +(117, 'POI117', '신정네거리역', 'Shinjeongnegeori Station', '인구밀집지역', 37.521121, 126.855010), +(118, 'POI118', '잠실새내역', 'Jamsil Saenae Station', '인구밀집지역', 37.510417, 127.083309), +(119, 'POI119', '잠실역', 'Jamsil Station', '인구밀집지역', 37.512077, 127.101158), +(120, 'POI120', '잠실롯데타워 일대', 'Jamsil Lotte Tower area', '발달상권', 37.511679, 127.104408), +(121, 'POI121', '송리단길·호수단길', 'Songridan-gil·Hosudan-gil', '발달상권', 37.508292, 127.106789), +(122, 'POI122', '신촌 스타광장', 'Shinchon Star Plaza', '발달상권', 37.556476, 126.936840), +(123, 'POI123', '보라매공원', 'Boramae Park', '공원', 37.492817, 126.919840), +(124, 'POI124', '서대문독립공원', 'Seodaemun Independence Park', '공원', 37.574265, 126.956435), +(125, 'POI125', '안양천', 'Anyangcheon River', '공원', 37.519953, 126.879723), +(126, 'POI126', '여의서로', 'Yeouiseoro', '공원', 37.533417, 126.911992), +(127, 'POI127', '올림픽공원', 'Olympic Park', '공원', 37.519754, 127.123176), +(128, 'POI128', '홍제폭포', 'Hongje Waterfall', '공원', 37.580613, 126.937246) ON CONFLICT (seoul_id) DO NOTHING; \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java index aa46b2f..4e903e9 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java @@ -88,7 +88,7 @@ public ResponseEntity suggestTourPlan(@PathVariable String user_id, @RequestB } } - @GetMapping("/{user_id") + @GetMapping("/{user_id}") public ResponseEntity getPrevChatsForPlan(@PathVariable String user_id) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !authentication.isAuthenticated() || "anonymousUser".equals(authentication.getName())) { From d40684499b8d541763a36df775e0c9d93742549d Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Sat, 17 May 2025 15:53:09 +0900 Subject: [PATCH 324/361] =?UTF-8?q?fastapiurl=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../userservice/controller/SuggestController.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java index 4e903e9..c657598 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java @@ -5,6 +5,7 @@ import com.example.userservice.dto.SuggestFastRequestDto; import com.example.userservice.entity.Member; import com.example.userservice.service.MemberService; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -19,9 +20,11 @@ @RestController @RequestMapping("/suggest") public class SuggestController { + @Value("${fastapi-svc}") + private String fastApiUrl; + private final MemberService memberService; private final RestTemplate restTemplate; - private final String fastApiUrl = "http://localhost:8000/suggest/"; public SuggestController(MemberService memberService) { this.memberService = memberService; @@ -65,7 +68,7 @@ public ResponseEntity suggestTourPlan(@PathVariable String user_id, @RequestB HttpEntity entity = new HttpEntity<>(suggestFastRequestDto, headers); try { - ResponseEntity response = restTemplate.postForEntity(fastApiUrl + user_id, entity, SuggestFastResponseDto.class); + ResponseEntity response = restTemplate.postForEntity(fastApiUrl + "/suggest/" + user_id, entity, SuggestFastResponseDto.class); if (response.getStatusCode() == HttpStatus.OK) { SuggestFastResponseDto suggestFastResponseDto = response.getBody(); @@ -108,7 +111,7 @@ public ResponseEntity getPrevChatsForPlan(@PathVariable String user_id) { } try { - ResponseEntity responseEntity = restTemplate.getForEntity(fastApiUrl + user_id, SuggestFastResponseDto[].class); + ResponseEntity responseEntity = restTemplate.getForEntity(fastApiUrl + "/suggest/" + user_id, SuggestFastResponseDto[].class); SuggestFastResponseDto[] responseArray = responseEntity.getBody(); if (responseArray == null) { From 109cf0908409caaf0cf30514ceb004066ad4fa59 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Sat, 17 May 2025 16:53:38 +0900 Subject: [PATCH 325/361] =?UTF-8?q?Refactor:=20user=20=EC=A6=90=EA=B2=A8?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20error=20exception=20=EB=82=B4=EC=9A=A9=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/placeservice/dto/AreaDto.java | 16 ++++++++-------- .../placeservice/service/AreaService.java | 13 ++++--------- .../userservice/config/SecurityConfig.java | 2 ++ .../CustomAccessDeniedHandler.java | 2 +- .../CustomAuthenticationEntryPoint.java | 17 ++++++++++++++--- .../userservice/service/FavoriteService.java | 9 +++++++-- 6 files changed, 36 insertions(+), 23 deletions(-) rename user-service/src/main/java/com/example/userservice/{config => exception}/CustomAccessDeniedHandler.java (95%) rename user-service/src/main/java/com/example/userservice/{config => exception}/CustomAuthenticationEntryPoint.java (67%) diff --git a/place-service/src/main/java/com/example/placeservice/dto/AreaDto.java b/place-service/src/main/java/com/example/placeservice/dto/AreaDto.java index b4e7583..79fc41e 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/AreaDto.java +++ b/place-service/src/main/java/com/example/placeservice/dto/AreaDto.java @@ -15,13 +15,13 @@ public class AreaDto { private BigDecimal lat; private BigDecimal lon; - public AreaDto(Long areaId, String seoulId, String area_name, String nameEng, String category, BigDecimal lat, BigDecimal lon) { - this.area_id = areaId; - this.seoul_id = seoulId; - this.area_name = area_name; - this.name_eng = nameEng; - this.category = category; - this.lat = lat; - this.lon = lon; + public AreaDto(Area area) { + this.area_id = area.getAreaId(); + this.seoul_id = area.getSeoulId(); + this.area_name = area.getName(); + this.name_eng = area.getNameEng(); + this.category = area.getCategory(); + this.lat = area.getLat(); + this.lon = area.getLon(); } } diff --git a/place-service/src/main/java/com/example/placeservice/service/AreaService.java b/place-service/src/main/java/com/example/placeservice/service/AreaService.java index c3edbaa..22d92a8 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AreaService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AreaService.java @@ -8,6 +8,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -19,16 +20,10 @@ public class AreaService { public List getAreaData() { try{ List areas = areaRepository.findAll(); + return areas.stream() - .map(area -> new AreaDto( - area.getAreaId(), - area.getSeoulId(), - area.getName(), - area.getNameEng(), - area.getCategory(), - area.getLat(), - area.getLon() - )).toList(); + .map(AreaDto::new) + .collect(Collectors.toList()); } catch (Exception e) { throw new RuntimeException("예상치 못한 오류",e); } diff --git a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java index 1bbc55c..1f69f38 100644 --- a/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java +++ b/user-service/src/main/java/com/example/userservice/config/SecurityConfig.java @@ -1,5 +1,7 @@ package com.example.userservice.config; +import com.example.userservice.exception.CustomAccessDeniedHandler; +import com.example.userservice.exception.CustomAuthenticationEntryPoint; import com.example.userservice.security.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; diff --git a/user-service/src/main/java/com/example/userservice/config/CustomAccessDeniedHandler.java b/user-service/src/main/java/com/example/userservice/exception/CustomAccessDeniedHandler.java similarity index 95% rename from user-service/src/main/java/com/example/userservice/config/CustomAccessDeniedHandler.java rename to user-service/src/main/java/com/example/userservice/exception/CustomAccessDeniedHandler.java index 9040c52..823433b 100644 --- a/user-service/src/main/java/com/example/userservice/config/CustomAccessDeniedHandler.java +++ b/user-service/src/main/java/com/example/userservice/exception/CustomAccessDeniedHandler.java @@ -1,4 +1,4 @@ -package com.example.userservice.config; +package com.example.userservice.exception; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/user-service/src/main/java/com/example/userservice/config/CustomAuthenticationEntryPoint.java b/user-service/src/main/java/com/example/userservice/exception/CustomAuthenticationEntryPoint.java similarity index 67% rename from user-service/src/main/java/com/example/userservice/config/CustomAuthenticationEntryPoint.java rename to user-service/src/main/java/com/example/userservice/exception/CustomAuthenticationEntryPoint.java index b5c4462..b0970e9 100644 --- a/user-service/src/main/java/com/example/userservice/config/CustomAuthenticationEntryPoint.java +++ b/user-service/src/main/java/com/example/userservice/exception/CustomAuthenticationEntryPoint.java @@ -1,4 +1,4 @@ -package com.example.userservice.config; +package com.example.userservice.exception; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; @@ -16,10 +16,21 @@ public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) - throws IOException { - sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "사용자가 없습니다.", request.getRequestURI()); + throws IOException { + + // 원인 예외 메시지 얻기 + Throwable cause = authException.getCause(); + String detailedMessage = authException.getMessage(); + if (cause != null) { + detailedMessage += " | 원인: " + cause.getMessage(); + } + // 로그에 예외 내용 남기기 + System.out.println("인증 실패 발생:"+detailedMessage+", 요청 URI: " + request.getRequestURI() + authException.getMessage()+ request.getRequestURI()+ authException); + + sendErrorResponse(response, HttpStatus.UNAUTHORIZED, detailedMessage, request.getRequestURI()); } + private void sendErrorResponse(HttpServletResponse response, HttpStatus status, String message, String path) throws IOException { response.setStatus(status.value()); diff --git a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java index 73c559c..37dd7e5 100644 --- a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java +++ b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java @@ -10,6 +10,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @@ -52,7 +53,8 @@ public List getListData(String userId) { }).toList(); } catch (Exception e) { - throw new RuntimeException(e); +// throw new RuntimeException(e); + throw new BadCredentialsException("즐겨찾기 장소 조회 실패",e); } } @@ -112,6 +114,7 @@ public ResponseEntity> deleteFavorite(String userId, String public List> getAllFavoriteData() { try{ List items = favoriteRepository.findAll(); + List favoriteDtoList = items.stream() .map(item ->{ ResponseEntity response = restTemplate.exchange( @@ -148,7 +151,9 @@ public List> getAllFavoriteData() { return result; } catch (Exception e) { - throw new RuntimeException(e); +// throw new RuntimeException(e); + throw new BadCredentialsException("즐겨찾기 장소 조회 실패",e); } + } } From 5c8b6a8bef795b28155d93047941ba865606404b Mon Sep 17 00:00:00 2001 From: ha-yun Date: Mon, 19 May 2025 09:43:30 +0900 Subject: [PATCH 326/361] =?UTF-8?q?Refactor:=20=EC=A7=80=EC=97=AD=EB=B3=84?= =?UTF-8?q?=20=EC=9E=A5=EC=86=8C=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20API=20=EB=B3=80=EC=88=98=EB=AA=85=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeservice/controller/AreaPlaceController.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java b/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java index d0dac8b..f721010 100644 --- a/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java +++ b/place-service/src/main/java/com/example/placeservice/controller/AreaPlaceController.java @@ -55,8 +55,8 @@ public ResponseEntity>> getAllPlacesByAreaId(@PathVaria for (Cafe cafe : cafes) { Map cafeMap = new LinkedHashMap<>(); - cafeMap.put("cafe_id", cafe.getId()); - cafeMap.put("cafe_name", cafe.getName()); + cafeMap.put("id", cafe.getId()); + cafeMap.put("name", cafe.getName()); cafeMap.put("address", cafe.getAddress()); cafeMap.put("phone", cafe.getPhone()); cafeMap.put("kakaomap_url", cafe.getKakaomapUrl()); @@ -153,9 +153,9 @@ public ResponseEntity>> getAllPlacesByAreaId(@PathVaria for (CulturalEvent event : events) { Map eventMap = new LinkedHashMap<>(); eventMap.put("id", event.getEventId()); - eventMap.put("category", event.getCategory()); - eventMap.put("title", event.getTitle()); + eventMap.put("name", event.getTitle()); eventMap.put("address", event.getAddress()); + eventMap.put("category", event.getCategory()); eventMap.put("target", event.getTarget()); eventMap.put("event_fee", event.getEventFee()); eventMap.put("event_img", event.getEventImg()); From 735837d509188d1a067d9716a09c3b340445920e Mon Sep 17 00:00:00 2001 From: ha-yun Date: Mon, 19 May 2025 13:15:48 +0900 Subject: [PATCH 327/361] =?UTF-8?q?Feat:=20build.gradle=20logstash=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- congestion-service/build.gradle | 2 ++ external-info-service/build.gradle | 2 ++ gateway/build.gradle | 2 ++ place-service/build.gradle | 2 ++ user-service/build.gradle | 3 ++- 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/congestion-service/build.gradle b/congestion-service/build.gradle index 02b7ced..110e6f1 100644 --- a/congestion-service/build.gradle +++ b/congestion-service/build.gradle @@ -24,6 +24,8 @@ repositories { } dependencies { + implementation 'net.logstash.logback:logstash-logback-encoder:7.4' + // JAXB (XML 파싱) implementation 'javax.xml.bind:jaxb-api:2.3.1' diff --git a/external-info-service/build.gradle b/external-info-service/build.gradle index 21e026f..ed7274a 100644 --- a/external-info-service/build.gradle +++ b/external-info-service/build.gradle @@ -24,6 +24,8 @@ repositories { } dependencies { + implementation 'net.logstash.logback:logstash-logback-encoder:7.4' + // JAXB (XML 파싱) implementation 'javax.xml.bind:jaxb-api:2.3.1' // JAXB Runtime (XML 파싱을 위해 필요) diff --git a/gateway/build.gradle b/gateway/build.gradle index 809df21..cb840a1 100644 --- a/gateway/build.gradle +++ b/gateway/build.gradle @@ -24,6 +24,8 @@ dependencyManagement { } dependencies { + implementation 'net.logstash.logback:logstash-logback-encoder:7.4' + implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.cloud:spring-cloud-starter-gateway' implementation 'org.springframework.boot:spring-boot-starter-webflux' diff --git a/place-service/build.gradle b/place-service/build.gradle index fcf3624..ca4fbb0 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -24,6 +24,8 @@ repositories { } dependencies { + implementation 'net.logstash.logback:logstash-logback-encoder:7.4' + // redis 추가 implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'com.fasterxml.jackson.core:jackson-databind' diff --git a/user-service/build.gradle b/user-service/build.gradle index b54536a..b4b59ae 100644 --- a/user-service/build.gradle +++ b/user-service/build.gradle @@ -32,6 +32,8 @@ repositories { } dependencies { + implementation 'net.logstash.logback:logstash-logback-encoder:7.4' + // JWT 처리 implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' @@ -71,7 +73,6 @@ dependencies { //implementation 'org.springframework.cloud:spring-cloud-starter-sleuth:2.2.4.RELEASE' - implementation 'net.logstash.logback:logstash-logback-encoder:7.4' } tasks.named('bootBuildImage') { From e834e16f2fdb0eef49e750c58fa182d2c3a4f8de Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Mon, 19 May 2025 13:33:02 +0900 Subject: [PATCH 328/361] =?UTF-8?q?refresh=ED=86=A0=ED=81=B0=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/FavoriteController.java | 3 + .../userservice/entity/RefreshToken.java | 2 +- .../userservice/entity/TokenRedis.java | 2 +- .../userservice/jwt/JwtTokenProvider.java | 37 +++++++----- .../security/JwtAuthenticationFilter.java | 2 +- .../example/userservice/security/JwtUtil.java | 57 +++++++------------ 6 files changed, 51 insertions(+), 52 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java b/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java index 1eeda9e..d59fd59 100644 --- a/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java +++ b/user-service/src/main/java/com/example/userservice/controller/FavoriteController.java @@ -20,6 +20,7 @@ public class FavoriteController { private final FavoriteService favoriteService; + //즐겨찾기 리스트 조회 @GetMapping("/list") public List getList() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); @@ -28,6 +29,7 @@ public List getList() { return favoriteService.getListData(userId); } + //즐겨찾기 추가 @PostMapping("/add") public ResponseEntity> addFavorite(@RequestBody FavoriteDto favoriteDto) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); @@ -36,6 +38,7 @@ public ResponseEntity> addFavorite(@RequestBody FavoriteDto return favoriteService.addFavoriteData(userId,favoriteDto); } + //즐겨찾기 삭제 @DeleteMapping("/delete/{type}/{id}") public ResponseEntity> deleteFavorite(@PathVariable String type, @PathVariable String id, HttpServletRequest request) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); diff --git a/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java index 9ae1712..78a7d5a 100644 --- a/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java +++ b/user-service/src/main/java/com/example/userservice/entity/RefreshToken.java @@ -9,7 +9,7 @@ @Getter @NoArgsConstructor -@RedisHash(value = "refreshToken", timeToLive = 2700) // 45분(2700초) 후 자동 삭제 +@RedisHash(value = "refreshToken", timeToLive = 21600) // 6시간 후 자동 삭제 public class RefreshToken { @Id diff --git a/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java b/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java index e0b2e9d..f2abb4c 100644 --- a/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java +++ b/user-service/src/main/java/com/example/userservice/entity/TokenRedis.java @@ -8,7 +8,7 @@ @Getter @AllArgsConstructor -@RedisHash(value = "token", timeToLive = 60 * 45) // 리프레시토큰 유효기간 45분 +@RedisHash(value = "token", timeToLive = 21600) // 리프레시토큰 유효기간 6시간 public class TokenRedis { @Id diff --git a/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java b/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java index dac2573..323f900 100644 --- a/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java +++ b/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java @@ -1,9 +1,9 @@ package com.example.userservice.jwt; -import io.github.cdimascio.dotenv.Dotenv; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.security.Keys; +import io.github.cdimascio.dotenv.Dotenv; // .env 파일에서 환경변수를 로드하기 위한 라이브러리 +import io.jsonwebtoken.Jwts; // JWT 생성 및 파싱을 위한 라이브러리 +import io.jsonwebtoken.SignatureAlgorithm; // 서명 알고리즘 정의 +import io.jsonwebtoken.security.Keys; // 안전한 키 생성을 위한 도구 import jakarta.annotation.PostConstruct; import org.springframework.stereotype.Component; @@ -15,40 +15,51 @@ public class JwtTokenProvider { private SecretKey secretKey; - @PostConstruct + @PostConstruct // Bean 생성 후 자동으로 호출되는 메서드 public void init() { + // .env 파일에서 환경변수를 로드 Dotenv dotenv = Dotenv.configure() .ignoreIfMalformed() .ignoreIfMissing() .load(); + + String jwtSecret = dotenv.get("JWT_SECRET_KEY"); + + if (jwtSecret == null) { throw new IllegalStateException("JWT_SECRET_KEY 환경변수가 설정되지 않았습니다!"); } + + this.secretKey = Keys.hmacShaKeyFor(jwtSecret.getBytes()); } - public String generateAccessToken(String userId) { // nickname 대신 userId 사용 + // Access Token 생성 + public String generateAccessToken(String userId) { Date now = new Date(); - Date expiryDate = new Date(now.getTime() + 1000 * 60 * 45 ); // 30분 + Date expiryDate = new Date(now.getTime() + 1000 * 60 * 45); + // JWT 생성 return Jwts.builder() - .setSubject(userId) // userId로 변경 + .setSubject(userId) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(secretKey, SignatureAlgorithm.HS512) .compact(); } - public String generateRefreshToken(String userId) { // nickname 대신 userId 사용 - Date now = new Date(); - Date expiryDate = new Date(now.getTime() + 1000 * 60 * 45); // 45분 + // Refresh Token 생성 + public String generateRefreshToken(String userId) { + Date now = new Date(); // 현재 시간 + Date expiryDate = new Date(now.getTime() + 1000 * 60 * 45); + // JWT 생성 return Jwts.builder() - .setSubject(userId) // userId로 변경 + .setSubject(userId) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(secretKey, SignatureAlgorithm.HS512) .compact(); } -} \ No newline at end of file +} diff --git a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java index 62d5f0e..7036246 100644 --- a/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java +++ b/user-service/src/main/java/com/example/userservice/security/JwtAuthenticationFilter.java @@ -31,7 +31,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final MemberRepository memberRepository; private final TokenService tokenService; - // 완전 일치 제외 경로 목록 (Swagger 포함) + // 완전 일치 제외 경로 목록 private final List excludedExactPaths = Arrays.asList( "/auth/signup", "/auth/admin/signup", diff --git a/user-service/src/main/java/com/example/userservice/security/JwtUtil.java b/user-service/src/main/java/com/example/userservice/security/JwtUtil.java index aa67e07..c4a02f3 100644 --- a/user-service/src/main/java/com/example/userservice/security/JwtUtil.java +++ b/user-service/src/main/java/com/example/userservice/security/JwtUtil.java @@ -19,8 +19,8 @@ public class JwtUtil { private SecretKey secretKey; - private Long accessTokenExpiration; // 액세스 토큰 만료 시간 (ms) - private Long refreshTokenExpiration; // 리프레시 토큰 만료 시간 (ms) + private Long accessTokenExpiration; // 액세스 토큰 유효 시간 + private Long refreshTokenExpiration; // 리프레시 토큰 유효 시간 @PostConstruct public void init() { @@ -35,11 +35,10 @@ public void init() { } this.secretKey = Keys.hmacShaKeyFor(jwtSecret.getBytes()); - // 액세스 토큰 유효 시간을 45분(2,700,000 밀리초)으로 설정 - this.accessTokenExpiration = 2700000L; // 45분 - this.refreshTokenExpiration = Long.parseLong(dotenv.get("JWT_REFRESH_EXPIRATION", "604800000")); // 기본값 7일 + this.accessTokenExpiration = 1000L * 60 * 45; // 45분 + this.refreshTokenExpiration = 1000L * 60 * 60 * 6; // 6시간 - System.out.println("JWT 설정 완료: 액세스 토큰 만료 시간 = " + (accessTokenExpiration / 60000) + "분"); + System.out.println("JWT 설정 완료: 액세스 토큰 = " + (accessTokenExpiration / 60000) + "분, 리프레시 토큰 = " + (refreshTokenExpiration / 60000) + "분"); } // 사용자 이름 추출 @@ -52,7 +51,7 @@ public Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } - // 토큰 ID 추출 (토큰 버전 관리용) + // 토큰 ID 추출 public String extractTokenId(String token) { return extractClaim(token, claims -> claims.get("tokenId", String.class)); } @@ -65,73 +64,59 @@ public T extractClaim(String token, Function claimsResolver) { // 모든 클레임 추출 private Claims extractAllClaims(String token) { - return Jwts - .parser() + return Jwts.parser() .verifyWith(secretKey) .build() .parseSignedClaims(token) .getPayload(); } - // 토큰 만료 확인 + // 토큰 만료 여부 private Boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } - // 액세스 토큰 생성 + // 액세스 토큰 생성 (UserDetails 기반) public String generateToken(UserDetails userDetails) { - Map claims = new HashMap<>(); - String token = createToken(claims, userDetails.getUsername(), accessTokenExpiration); - System.out.println("액세스 토큰 발급: 사용자 = " + userDetails.getUsername() + ", 만료 시간 = " + new Date(System.currentTimeMillis() + accessTokenExpiration)); - return token; + return createToken(new HashMap<>(), userDetails.getUsername(), accessTokenExpiration); } - // 액세스 토큰 생성 (nickname 직접 전달) + // 액세스 토큰 생성 (닉네임 기반) public String generateAccessToken(String nickname) { - Map claims = new HashMap<>(); - String token = createToken(claims, nickname, accessTokenExpiration); - System.out.println("액세스 토큰 발급: 사용자 = " + nickname + ", 만료 시간 = " + new Date(System.currentTimeMillis() + accessTokenExpiration)); - return token; + return createToken(new HashMap<>(), nickname, accessTokenExpiration); } - // 리프레시 토큰 생성 + // 리프레시 토큰 생성 (UserDetails 기반) public String generateRefreshToken(UserDetails userDetails) { - Map claims = new HashMap<>(); - String token = createToken(claims, userDetails.getUsername(), refreshTokenExpiration); - System.out.println("리프레시 토큰 발급: 사용자 = " + userDetails.getUsername() + ", 만료 시간 = " + new Date(System.currentTimeMillis() + refreshTokenExpiration)); - return token; + return createToken(new HashMap<>(), userDetails.getUsername(), refreshTokenExpiration); } - // 리프레시 토큰 생성 (nickname 직접 전달) + // 리프레시 토큰 생성 (닉네임 기반) public String generateRefreshToken(String nickname) { - Map claims = new HashMap<>(); - String token = createToken(claims, nickname, refreshTokenExpiration); - System.out.println("리프레시 토큰 발급: 사용자 = " + nickname + ", 만료 시간 = " + new Date(System.currentTimeMillis() + refreshTokenExpiration)); - return token; + return createToken(new HashMap<>(), nickname, refreshTokenExpiration); } - // 토큰 생성 메소드 + // JWT 생성 로직 private String createToken(Map claims, String subject, Long expirationTime) { - // 토큰 ID 생성 (토큰 버전 관리를 위한 고유 식별자) String tokenId = String.valueOf(System.currentTimeMillis()); return Jwts.builder() .claims(claims) .subject(subject) - .claim("tokenId", tokenId) // 토큰 ID 추가 (버전 관리용) + .claim("tokenId", tokenId) .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(System.currentTimeMillis() + expirationTime)) .signWith(secretKey, SignatureAlgorithm.HS512) .compact(); } - // 토큰 유효성 검증 (UserDetails와 비교) + // 토큰 유효성 검사 (UserDetails와 비교) public Boolean validateToken(String token, UserDetails userDetails) { final String username = extractUsername(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } - // 토큰 유효성만 검사 (사용자 정보 없이) + // 토큰 유효성 검사 (단순 검증) public Boolean validateToken(String token) { try { Jwts.parser() @@ -143,4 +128,4 @@ public Boolean validateToken(String token) { return false; } } -} \ No newline at end of file +} From 12b9da17ecfa384be22e569aa84e9161e2886e24 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Mon, 19 May 2025 13:56:43 +0900 Subject: [PATCH 329/361] =?UTF-8?q?Refactor:=20deploy.yml=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/deploy.yml | 53 +++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b4ebebb..e4cb1bb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -202,4 +202,55 @@ jobs: docker push $ECR_REGISTRY/user-service:latest kubectl set image deployment/user-service user-service=$ECR_REGISTRY/user-service:$IMAGE_TAG - kubectl rollout status deployment/user-service \ No newline at end of file + kubectl rollout status deployment/user-service + + + gateway-build: + runs-on: ubuntu-latest + env: + IMAGE_TAG: 1.${{ github.run_number }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + with: + mask-password: true + + - name: 자바 설치 + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: '17' + + - name: kubeconfig 설정 + run: | + aws eks update-kubeconfig --region ap-northeast-2 --name one-cluster + echo "Step output: ${{ steps.set-ecr.outputs.registry }}" + - name: Build & Push gateway + env: # ecr 업로드 할 때 필요한 변수들 선언 + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # ecr 경로 + run: | + cd gateway + echo "${{ secrets.BACKEND_ENV}}" > .env + gradle bootJar -x test + docker build -t $ECR_REGISTRY/gateway:$IMAGE_TAG . + docker tag $ECR_REGISTRY/gateway:$IMAGE_TAG $ECR_REGISTRY/gateway:latest + + docker push $ECR_REGISTRY/gateway:$IMAGE_TAG + docker push $ECR_REGISTRY/gateway:latest + + echo "태그 확인: gateway=$ECR_REGISTRY/gateway:$IMAGE_TAG" + + kubectl set image deployment/gateway gateway=$ECR_REGISTRY/gateway:$IMAGE_TAG + kubectl rollout status deployment/gateway + From 5968f193056c8c642d57bb1f129e4962cbab2002 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 20 May 2025 10:00:47 +0900 Subject: [PATCH 330/361] =?UTF-8?q?elastic=20url=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../congestionservice/service/CongestionService.java | 6 +++++- .../externalinfoservice/service/AccidentEsService.java | 6 +++++- .../example/externalinfoservice/service/ESRoadService.java | 6 +++++- .../example/externalinfoservice/service/ParkEsService.java | 6 +++++- .../externalinfoservice/service/WeatherEsService.java | 6 +++++- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java b/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java index bdbac83..cddcd89 100644 --- a/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java +++ b/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.apache.kafka.common.errors.InvalidRequestException; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -21,11 +22,14 @@ public class CongestionService { private static final RestTemplate restTemplate = new RestTemplate(); + @Value("elastic_url") + private static String elastic_url; + public static JsonNode getCongestion() { try{ // 오늘 날짜 구하기 String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); - String apiUrl = String.format("http://elasticsearch.seoultravel.life/seoul_citydata_congestion_%s/_search", today); + String apiUrl = String.format(elastic_url + "/seoul_citydata_congestion_%s/_search", today); // JSON Body 생성 String jsonBody = String.format("{\n" + diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java index 5fa5050..fd80232 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.apache.kafka.common.errors.InvalidRequestException; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -24,10 +25,13 @@ public class AccidentEsService { private final RestTemplate restTemplate = new RestTemplate(); // 주입 방식으로 대체해도 OK private final ObjectMapper objectMapper = new ObjectMapper(); + @Value("elastic_url") + private static String elastic_url; + public JsonNode getAccidentData() { try { String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); - String apiUrl = String.format("http://elasticsearch.seoultravel.life/seoul_citydata_accident_%s/_search", today); + String apiUrl = String.format(elastic_url + "/seoul_citydata_accident_%s/_search", today); String jsonBody = """ { diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java index a532ece..bff12a0 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.apache.kafka.common.errors.InvalidRequestException; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -22,11 +23,14 @@ public class ESRoadService { private static final RestTemplate restTemplate = new RestTemplate(); + @Value("elastic_url") + private static String elastic_url; + public static JsonNode getTrafficData() { try { // 오늘 날짜 구하기 String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); - String apiUrl = String.format("http://elasticsearch.seoultravel.life/seoul_citydata_road_%s/_search", today); + String apiUrl = String.format(elastic_url + "/seoul_citydata_road_%s/_search", today); // JSON Body 생성 String jsonBody = String.format("{\n" + diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java index 1c41ac4..3b04b3e 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @@ -17,10 +18,13 @@ public class ParkEsService { private final RestTemplate restTemplate = new RestTemplate(); + @Value("elastic_url") + private static String elastic_url; + // 오늘 날짜 기반 인덱스 URL 생성 private String getTodayIndexUrl() { String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); - return String.format("http://elasticsearch.seoultravel.life/seoul_citydata_parking_%s/_search", today); + return String.format(elastic_url + "/seoul_citydata_parking_%s/_search", today); } // // 특정 지역 주차장 정보 diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java index 9f2a0c0..3df5f44 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java @@ -1,6 +1,7 @@ package com.example.externalinfoservice.service; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @@ -15,10 +16,13 @@ public class WeatherEsService { private final RestTemplate restTemplate = new RestTemplate(); + @Value("elastic_url") + private static String elastic_url; + // 오늘 날짜 기반 인덱스 이름 생성 private String getTodayIndex() { String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); - return String.format("http://elasticsearch.seoultravel.life/seoul_citydata_weather_%s/_search", today); + return String.format(elastic_url + "/seoul_citydata_weather_%s/_search", today); } // 특정 지역 날씨 조회 From 2599da6ddf2512f527e57f23a3cc07031de389e1 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 20 May 2025 10:15:18 +0900 Subject: [PATCH 331/361] =?UTF-8?q?elastic=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/congestionservice/service/CongestionService.java | 2 +- .../example/externalinfoservice/service/AccidentEsService.java | 2 +- .../com/example/externalinfoservice/service/ESRoadService.java | 2 +- .../com/example/externalinfoservice/service/ParkEsService.java | 2 +- .../example/externalinfoservice/service/WeatherEsService.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java b/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java index cddcd89..728c2f0 100644 --- a/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java +++ b/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java @@ -22,7 +22,7 @@ public class CongestionService { private static final RestTemplate restTemplate = new RestTemplate(); - @Value("elastic_url") + @Value("${elastic_url}") private static String elastic_url; public static JsonNode getCongestion() { diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java index fd80232..d9cd9c1 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java @@ -25,7 +25,7 @@ public class AccidentEsService { private final RestTemplate restTemplate = new RestTemplate(); // 주입 방식으로 대체해도 OK private final ObjectMapper objectMapper = new ObjectMapper(); - @Value("elastic_url") + @Value("${elastic_url}") private static String elastic_url; public JsonNode getAccidentData() { diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java index bff12a0..79479e7 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java @@ -23,7 +23,7 @@ public class ESRoadService { private static final RestTemplate restTemplate = new RestTemplate(); - @Value("elastic_url") + @Value("${elastic_url}") private static String elastic_url; public static JsonNode getTrafficData() { diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java index 3b04b3e..b12b8dc 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java @@ -18,7 +18,7 @@ public class ParkEsService { private final RestTemplate restTemplate = new RestTemplate(); - @Value("elastic_url") + @Value("${elastic_url}") private static String elastic_url; // 오늘 날짜 기반 인덱스 URL 생성 diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java index 3df5f44..5653e23 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java @@ -16,7 +16,7 @@ public class WeatherEsService { private final RestTemplate restTemplate = new RestTemplate(); - @Value("elastic_url") + @Value("${elastic_url}") private static String elastic_url; // 오늘 날짜 기반 인덱스 이름 생성 From dee97f1ca4f8699bb26485adc2fa3d82408efc0e Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 20 May 2025 10:35:46 +0900 Subject: [PATCH 332/361] =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=93=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../congestionservice/controller/CongestionController.java | 3 ++- .../example/congestionservice/scheduler/PushScheduler.java | 4 ++-- .../congestionservice/service/CongestionService.java | 6 ++++-- .../externalinfoservice/service/AccidentEsService.java | 2 +- .../example/externalinfoservice/service/ESRoadService.java | 4 ++-- .../example/externalinfoservice/service/ParkEsService.java | 2 +- .../externalinfoservice/service/WeatherEsService.java | 2 +- 7 files changed, 13 insertions(+), 10 deletions(-) diff --git a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java index 0e3b65a..f78f5c2 100644 --- a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java +++ b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java @@ -24,6 +24,7 @@ public class CongestionController { private final List emitters = new CopyOnWriteArrayList<>(); private final CongestionPreviousCache congestionPreviousCache; + private final CongestionService congestionService; @GetMapping("/congestion") public SseEmitter streamCongestion() { @@ -39,7 +40,7 @@ public SseEmitter streamCongestion() { // 초기데이터 투입 try { System.out.println("혼잡도 초기 데이터 푸시 중..."); - var congestionList = CongestionService.getCongestion(); + var congestionList = congestionService.getCongestion(); emitter.send(SseEmitter.event() .name("congestion-update") diff --git a/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java b/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java index c06bed8..5e64030 100644 --- a/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java +++ b/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java @@ -19,7 +19,7 @@ public class PushScheduler { private final CongestionController congestionController; private final CongestionPreviousCache congestionPreviousCache; - + private final CongestionService congestionService; // 이전 혼잡도 상태를 저장하는 필드 추가(혼잡도 알림용) // private Map previousLevels = new HashMap<>(); @@ -30,7 +30,7 @@ public void push() { System.out.println("혼잡도 푸시 중..."); - var congestionList = CongestionService.getCongestion(); + var congestionList = congestionService.getCongestion(); congestionController.sendToClients(congestionList); // 모든 지역 혼잡도 전송 diff --git a/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java b/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java index 728c2f0..dce6816 100644 --- a/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java +++ b/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.apache.kafka.common.errors.InvalidRequestException; import org.springframework.beans.factory.annotation.Value; @@ -23,13 +24,14 @@ public class CongestionService { private static final RestTemplate restTemplate = new RestTemplate(); @Value("${elastic_url}") - private static String elastic_url; + private String elastic_url; - public static JsonNode getCongestion() { + public JsonNode getCongestion() { try{ // 오늘 날짜 구하기 String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); String apiUrl = String.format(elastic_url + "/seoul_citydata_congestion_%s/_search", today); + System.out.println(apiUrl); // JSON Body 생성 String jsonBody = String.format("{\n" + diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java index d9cd9c1..1679fdd 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java @@ -26,7 +26,7 @@ public class AccidentEsService { private final ObjectMapper objectMapper = new ObjectMapper(); @Value("${elastic_url}") - private static String elastic_url; + private String elastic_url; public JsonNode getAccidentData() { try { diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java index 79479e7..95b1091 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java @@ -24,9 +24,9 @@ public class ESRoadService { private static final RestTemplate restTemplate = new RestTemplate(); @Value("${elastic_url}") - private static String elastic_url; + private String elastic_url; - public static JsonNode getTrafficData() { + public JsonNode getTrafficData() { try { // 오늘 날짜 구하기 String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java index b12b8dc..a89003f 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ParkEsService.java @@ -19,7 +19,7 @@ public class ParkEsService { private final RestTemplate restTemplate = new RestTemplate(); @Value("${elastic_url}") - private static String elastic_url; + private String elastic_url; // 오늘 날짜 기반 인덱스 URL 생성 private String getTodayIndexUrl() { diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java index 5653e23..febc0ac 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/WeatherEsService.java @@ -17,7 +17,7 @@ public class WeatherEsService { private final RestTemplate restTemplate = new RestTemplate(); @Value("${elastic_url}") - private static String elastic_url; + private String elastic_url; // 오늘 날짜 기반 인덱스 이름 생성 private String getTodayIndex() { From ec166ede77221fbb68f7969320fab8b90bb38638 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Tue, 20 May 2025 13:35:21 +0900 Subject: [PATCH 333/361] =?UTF-8?q?zipkin=20=EA=B4=80=EB=A0=A8=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user-service/build.gradle | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/user-service/build.gradle b/user-service/build.gradle index b4b59ae..7bf2b33 100644 --- a/user-service/build.gradle +++ b/user-service/build.gradle @@ -14,6 +14,7 @@ java { } + //dependencyManagement { // imports { // mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.1" @@ -39,6 +40,12 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + + // Micrometer 기반 Tracing (Spring Boot 3.x 공식 지원) + implementation 'io.micrometer:micrometer-tracing-bridge-brave' + implementation 'io.zipkin.reporter2:zipkin-reporter-brave' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + // Spring Boot 기본 implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' @@ -53,8 +60,6 @@ dependencies { implementation 'io.github.cdimascio:java-dotenv:5.2.2' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' - implementation 'org.springframework.cloud:spring-cloud-starter-sleuth:3.1.3' - implementation 'org.springframework.cloud:spring-cloud-starter-zipkin:2.2.8.RELEASE' // DB runtimeOnly 'org.postgresql:postgresql' From 236366d6eed62dfbd0243f5179c7bae77fec50cd Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Tue, 20 May 2025 13:45:51 +0900 Subject: [PATCH 334/361] =?UTF-8?q?zipkin=20=EA=B4=80=EB=A0=A8=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- congestion-service/build.gradle | 6 ++++++ external-info-service/build.gradle | 6 ++++++ gateway/build.gradle | 6 ++++++ place-service/build.gradle | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/congestion-service/build.gradle b/congestion-service/build.gradle index 110e6f1..1cd8f34 100644 --- a/congestion-service/build.gradle +++ b/congestion-service/build.gradle @@ -39,6 +39,12 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + // Micrometer 기반 Tracing (Spring Boot 3.x 공식 지원) + implementation 'io.micrometer:micrometer-tracing-bridge-brave' + implementation 'io.zipkin.reporter2:zipkin-reporter-brave' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/external-info-service/build.gradle b/external-info-service/build.gradle index ed7274a..439c050 100644 --- a/external-info-service/build.gradle +++ b/external-info-service/build.gradle @@ -37,6 +37,12 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + // Micrometer 기반 Tracing (Spring Boot 3.x 공식 지원) + implementation 'io.micrometer:micrometer-tracing-bridge-brave' + implementation 'io.zipkin.reporter2:zipkin-reporter-brave' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + + implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-configuration-processor' diff --git a/gateway/build.gradle b/gateway/build.gradle index cb840a1..bb43c52 100644 --- a/gateway/build.gradle +++ b/gateway/build.gradle @@ -31,6 +31,12 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // Micrometer 기반 Tracing (Spring Boot 3.x 공식 지원) + implementation 'io.micrometer:micrometer-tracing-bridge-brave' + implementation 'io.zipkin.reporter2:zipkin-reporter-brave' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + } tasks.named('test') { diff --git a/place-service/build.gradle b/place-service/build.gradle index ca4fbb0..edd0d65 100644 --- a/place-service/build.gradle +++ b/place-service/build.gradle @@ -41,6 +41,12 @@ dependencies { implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' implementation 'org.springframework.boot:spring-boot-starter-cache' + + // Micrometer 기반 Tracing (Spring Boot 3.x 공식 지원) + implementation 'io.micrometer:micrometer-tracing-bridge-brave' + implementation 'io.zipkin.reporter2:zipkin-reporter-brave' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + // JWT 처리 implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' From 5602136524c010f2b04142d90c71a971ad47bb08 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Tue, 20 May 2025 14:36:17 +0900 Subject: [PATCH 335/361] =?UTF-8?q?AI=20=EC=97=AC=ED=96=89=EC=A7=80=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/userservice/dto/SuggestRequestDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user-service/src/main/java/com/example/userservice/dto/SuggestRequestDto.java b/user-service/src/main/java/com/example/userservice/dto/SuggestRequestDto.java index ada6fe2..c0eff8c 100644 --- a/user-service/src/main/java/com/example/userservice/dto/SuggestRequestDto.java +++ b/user-service/src/main/java/com/example/userservice/dto/SuggestRequestDto.java @@ -18,5 +18,5 @@ public class SuggestRequestDto { private String startPlace; @JsonProperty("optional_request") - private String optionalRequest; + private String optionalRequest = ""; } From 9ead427c4b895cb678d3425fe9beb569b3476b91 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Tue, 20 May 2025 15:10:23 +0900 Subject: [PATCH 336/361] =?UTF-8?q?Refactor:=20=ED=86=B5=ED=95=A9=EA=B2=80?= =?UTF-8?q?=EC=83=89=20API=20=EC=9D=91=EB=8B=B5=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/placeservice/dto/PlaceDocument.java | 3 +++ .../example/placeservice/service/PlaceDocumentMapper.java | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java b/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java index 8b6a684..f742c49 100644 --- a/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java +++ b/place-service/src/main/java/com/example/placeservice/dto/PlaceDocument.java @@ -16,4 +16,7 @@ public class PlaceDocument { private String place_id; private BigDecimal lat; private BigDecimal lon; + private String phone; + private String kakaomap_url; + } diff --git a/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java index d6c9d4e..bb76391 100644 --- a/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java +++ b/place-service/src/main/java/com/example/placeservice/service/PlaceDocumentMapper.java @@ -16,6 +16,8 @@ public PlaceDocument toPlaceDocument(Accommodation item) { .place_id(String.valueOf(item.getAccommodationId())) .lat(item.getLat()) .lon(item.getLon()) + .phone(item.getPhone()) + .kakaomap_url(item.getKakaomapUrl()) .build(); } @@ -27,6 +29,8 @@ public PlaceDocument toPlaceDocument(Cafe item) { .place_id(String.valueOf(item.getId())) .lat(item.getLat()) .lon(item.getLon()) + .phone(item.getPhone()) + .kakaomap_url(item.getKakaomapUrl()) .build(); } @@ -49,6 +53,8 @@ public PlaceDocument toPlaceDocument(Restaurant item) { .place_id(String.valueOf(item.getRestaurantId())) .lat(item.getLat()) .lon(item.getLon()) + .phone(item.getPhone()) + .kakaomap_url(item.getKakaomap_url()) .build(); } @@ -60,6 +66,8 @@ public PlaceDocument toPlaceDocument(Attraction item) { .place_id(String.valueOf(item.getAttractionId())) .lat(item.getLat()) .lon(item.getLon()) + .phone(item.getPhone()) + .kakaomap_url(item.getKakaomapUrl()) .build(); } } From 0ea9d33af5f4856b5b5a48cf933e1e7801a7d57c Mon Sep 17 00:00:00 2001 From: ha-yun Date: Tue, 20 May 2025 16:24:58 +0900 Subject: [PATCH 337/361] =?UTF-8?q?Refactor:=20=EC=A6=90=EA=B2=A8=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20API-plcae=20rest=ED=86=B5=EC=8B=A0=20url=EC=9D=84?= =?UTF-8?q?=20=20.env=ED=8C=8C=EC=9D=BC=EC=9D=84=20=ED=86=B5=ED=95=B4=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=EC=84=A4=EC=A0=95=EC=9C=BC=EB=A1=9C=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 --- .../example/userservice/service/FavoriteService.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java index 37dd7e5..698f130 100644 --- a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java +++ b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.JsonNode; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.BadCredentialsException; @@ -20,6 +21,8 @@ import java.util.Optional; import java.util.stream.Collectors; + + @Service @RequiredArgsConstructor public class FavoriteService { @@ -28,6 +31,9 @@ public class FavoriteService { private final MemberRepository memberRepository; private static final RestTemplate restTemplate = new RestTemplate(); + @Value("${gateway-url}") + private String gatewayUrl; + public List getListData(String userId) { try{ List items = favoriteRepository.findAllByMember_UserId(userId); @@ -36,7 +42,7 @@ public List getListData(String userId) { .map(item -> { ResponseEntity response = restTemplate.exchange( - "http://api.seoultravel.life/place/main/info/"+item.getType()+"/" + item.getPlaceId(), + gatewayUrl+"/place/main/info/"+item.getType()+"/" + item.getPlaceId(), HttpMethod.GET, null, JsonNode.class @@ -118,7 +124,7 @@ public List> getAllFavoriteData() { List favoriteDtoList = items.stream() .map(item ->{ ResponseEntity response = restTemplate.exchange( - "http://api.seoultravel.life/place/main/info/"+item.getType()+"/" + item.getPlaceId(), + gatewayUrl+"/place/main/info/"+item.getType()+"/" + item.getPlaceId(), HttpMethod.GET, null, JsonNode.class From 2f0ac0f4ae813d89fbab4f8c71353bf0d93f5133 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 21 May 2025 11:02:36 +0900 Subject: [PATCH 338/361] =?UTF-8?q?Refactor:=20user=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=88=98=EC=A0=95,?= =?UTF-8?q?=20=ED=9A=8C=EC=9B=90=20=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?aPI=20member=5Fid=20=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/example/userservice/dto/MemberDto.java | 2 +- .../java/com/example/userservice/service/MemberService.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java index 05b7eba..7295b95 100644 --- a/user-service/src/main/java/com/example/userservice/dto/MemberDto.java +++ b/user-service/src/main/java/com/example/userservice/dto/MemberDto.java @@ -15,7 +15,7 @@ public class MemberDto { @AllArgsConstructor @NoArgsConstructor public static class MemberResponse { - private String memberId; + private Long member_id; // 회원 고유 ID (시스템 내부용) private String user_id; // 로그인 ID private String nickname; // 닉네임 private Short birth_year; // 출생연도 diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java index f2e0420..89f2083 100644 --- a/user-service/src/main/java/com/example/userservice/service/MemberService.java +++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java @@ -120,7 +120,7 @@ public MemberDto.MemberResponse updateProfile(String userId, MemberDto.UpdateReq // 응답 DTO로 변환 return MemberDto.MemberResponse.builder() - .memberId(updatedMember.getUserId()) + .member_id(updatedMember.getMemberId()) .user_id(updatedMember.getUserId()) .nickname(updatedMember.getNickname()) .birth_year(updatedMember.getBirthYear()) @@ -158,6 +158,7 @@ public List getAllMembers() { return members.stream() .map(member -> MemberDto.MemberResponse.builder() + .member_id(member.getMemberId()) .user_id(member.getUserId()) .nickname(member.getNickname()) .birth_year(member.getBirthYear()) From 86362df4e6308180a53775ab32c395f48433b3dd Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 21 May 2025 15:12:01 +0900 Subject: [PATCH 339/361] =?UTF-8?q?Refactor:=20=EC=A6=90=EA=B2=A8=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=EC=B6=94=EA=B0=80=20API-=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=EB=90=9C=20=EC=9E=A5=EC=86=8C=20type/id=EB=8A=94=20=EC=95=88?= =?UTF-8?q?=EB=93=A4=EC=96=B4=EA=B0=80=EB=8F=84=EB=A1=9D=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 --- .../userservice/service/FavoriteService.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java index 698f130..5b4ca98 100644 --- a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java +++ b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java @@ -82,13 +82,26 @@ public ResponseEntity> addFavoriteData(String userId, Favori return ResponseEntity.status(400).body(response); } - // 2. Favorite 엔티티 생성 + // 3. 존재하는 장소인지 type/id 확인 + try { + ResponseEntity checkPlace = restTemplate.exchange( + gatewayUrl + "/place/main/info/" + favoriteDto.getType() + "/" + favoriteDto.getPlace_id(), + HttpMethod.GET, + null, + JsonNode.class + ); + } catch (Exception e) { + response.put("message", "잘못된 장소 type/id 입니다"); + return ResponseEntity.status(400).body(response); + } + + // 4. Favorite 엔티티 생성 Favorite favorite = new Favorite(); favorite.setType(favoriteDto.getType()); favorite.setPlaceId(favoriteDto.getPlace_id()); // id는 placeId 역할 favorite.setMember(member); - // 3. 저장 + // 5. 저장 Favorite saved = favoriteRepository.save(favorite); response.put("message", "즐겨찾기에 추가되었습니다."); From 053d2bbd9bcb7d79cec15f0d308717a601b54718 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 22 May 2025 11:01:53 +0900 Subject: [PATCH 340/361] =?UTF-8?q?Refactor:=20=ED=96=89=EC=82=AC=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0-=20DB=EC=97=90=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=20=EC=9E=88=EB=8A=94=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A4=91?= =?UTF-8?q?=20endDate=EA=B0=80=20=EC=A7=80=EB=82=9C=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/CulturalEventRepository.java | 1 + .../placeservice/service/CulturalEventService.java | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java index ff69387..029f861 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java @@ -23,4 +23,5 @@ public interface CulturalEventRepository extends JpaRepository findByAreaAreaId(@Param("areaId") Long areaId); + CulturalEvent findByTitleAndAddressAndStartDate(String title, String address, LocalDateTime startDate); } diff --git a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java index e4f4d67..50a21bf 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java @@ -60,11 +60,20 @@ private void saveEvents(List eventItems) { if (!culturalEventRepository.existsByTitleAndAddressAndStartDate( event.getTitle(), event.getAddress(), event.getStartDate())) { - // 🔹 행사 위치 기준 가장 가까운 지역 찾기 + // 행사 위치 기준 가장 가까운 지역 찾기 Area nearestArea = findNearestArea(event.getLat().doubleValue(), event.getLon().doubleValue(), areas); event.setArea(nearestArea); // 🔹 지역 설정 culturalEventRepository.save(event); + }else{ + // 이미 있는 데이터인데 종료일이 지났으면 삭제 + CulturalEvent existing = culturalEventRepository.findByTitleAndAddressAndStartDate( + event.getTitle(), event.getAddress(), event.getStartDate()); + + if (existing != null && existing.getEndDate() != null && + existing.getEndDate().isBefore(LocalDate.now().atStartOfDay())) { + culturalEventRepository.delete(existing); + } } } } From 32f18780a2e217cc1d5871a3d7c2944a69ba18e4 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 23 May 2025 15:10:08 +0900 Subject: [PATCH 341/361] =?UTF-8?q?Refactor:=20place-=ED=96=89=EC=82=AC=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=98=A4=EB=A5=98=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/deploy.yml | 20 +++++++++---------- .../repository/CulturalEventRepository.java | 2 +- .../service/CulturalEventService.java | 11 ++++++---- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e4cb1bb..226fc97 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -55,8 +55,8 @@ jobs: docker push $ECR_REGISTRY/external-info-service:$IMAGE_TAG docker push $ECR_REGISTRY/external-info-service:latest - kubectl set image deployment/external-info-service external-info-service=$ECR_REGISTRY/external-info-service:$IMAGE_TAG - kubectl rollout status deployment/external-info-service + kubectl set image deployment/external-info-service external-info-service=$ECR_REGISTRY/external-info-service:$IMAGE_TAG -n backend + kubectl rollout status deployment/external-info-service -n backend place-service-build: runs-on: ubuntu-latest @@ -105,8 +105,8 @@ jobs: echo "태그 확인: place-service=$ECR_REGISTRY/place-service:$IMAGE_TAG" - kubectl set image deployment/place-service place-service=$ECR_REGISTRY/place-service:$IMAGE_TAG - kubectl rollout status deployment/place-service + kubectl set image deployment/place-service place-service=$ECR_REGISTRY/place-service:$IMAGE_TAG -n backend + kubectl rollout status deployment/place-service -n backend congestion-service-build: runs-on: ubuntu-latest @@ -152,8 +152,8 @@ jobs: docker push $ECR_REGISTRY/congestion-service:$IMAGE_TAG docker push $ECR_REGISTRY/congestion-service:latest - kubectl set image deployment/congestion-service congestion-service=$ECR_REGISTRY/congestion-service:$IMAGE_TAG - kubectl rollout status deployment/congestion-service + kubectl set image deployment/congestion-service congestion-service=$ECR_REGISTRY/congestion-service:$IMAGE_TAG -n backend + kubectl rollout status deployment/congestion-service -n backend user-service-build: runs-on: ubuntu-latest @@ -201,8 +201,8 @@ jobs: docker push $ECR_REGISTRY/user-service:$IMAGE_TAG docker push $ECR_REGISTRY/user-service:latest - kubectl set image deployment/user-service user-service=$ECR_REGISTRY/user-service:$IMAGE_TAG - kubectl rollout status deployment/user-service + kubectl set image deployment/user-service user-service=$ECR_REGISTRY/user-service:$IMAGE_TAG -n backend + kubectl rollout status deployment/user-service -n backend gateway-build: @@ -251,6 +251,6 @@ jobs: echo "태그 확인: gateway=$ECR_REGISTRY/gateway:$IMAGE_TAG" - kubectl set image deployment/gateway gateway=$ECR_REGISTRY/gateway:$IMAGE_TAG - kubectl rollout status deployment/gateway + kubectl set image deployment/gateway gateway=$ECR_REGISTRY/gateway:$IMAGE_TAG -n backend + kubectl rollout status deployment/gateway -n backend diff --git a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java index 029f861..89d30a9 100644 --- a/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java +++ b/place-service/src/main/java/com/example/placeservice/repository/CulturalEventRepository.java @@ -23,5 +23,5 @@ public interface CulturalEventRepository extends JpaRepository findByAreaAreaId(@Param("areaId") Long areaId); - CulturalEvent findByTitleAndAddressAndStartDate(String title, String address, LocalDateTime startDate); + List findByTitleAndAddressAndStartDate(String title, String address, LocalDateTime startDate); } diff --git a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java index 50a21bf..41ec59e 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java @@ -67,12 +67,15 @@ private void saveEvents(List eventItems) { culturalEventRepository.save(event); }else{ // 이미 있는 데이터인데 종료일이 지났으면 삭제 - CulturalEvent existing = culturalEventRepository.findByTitleAndAddressAndStartDate( + List existing = culturalEventRepository.findByTitleAndAddressAndStartDate( event.getTitle(), event.getAddress(), event.getStartDate()); - if (existing != null && existing.getEndDate() != null && - existing.getEndDate().isBefore(LocalDate.now().atStartOfDay())) { - culturalEventRepository.delete(existing); + if (existing != null){ + for (CulturalEvent existingEvent : existing) { + if(existingEvent.getEndDate() != null && existingEvent.getEndDate().isBefore(LocalDate.now().atStartOfDay())){ + culturalEventRepository.delete(existingEvent); + } + } } } } From cdd325a8a2d302d826244af028b2ab313abf3b3e Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 23 May 2025 16:18:45 +0900 Subject: [PATCH 342/361] =?UTF-8?q?Refactor:=20=EC=A6=90=EA=B2=A8=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=EC=B6=94=EA=B0=80=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/userservice/service/FavoriteService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java index 5b4ca98..88c12a6 100644 --- a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java +++ b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java @@ -91,7 +91,7 @@ public ResponseEntity> addFavoriteData(String userId, Favori JsonNode.class ); } catch (Exception e) { - response.put("message", "잘못된 장소 type/id 입니다"); + response.put("message", "잘못된 장소 type/id 입니다 "+e.getMessage()); return ResponseEntity.status(400).body(response); } From dfbb8ef5704885228b487aafc475b125fcc79ee0 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 23 May 2025 17:28:11 +0900 Subject: [PATCH 343/361] =?UTF-8?q?Refactor:=20=EC=A6=90=EA=B2=A8=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20api=20-=20=EB=82=B4=EB=B6=80=20=ED=86=B5=EC=8B=A0?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/place/Accommodation.java | 32 +++++ .../userservice/entity/place/Area.java | 40 ++++++ .../userservice/entity/place/Attraction.java | 50 +++++++ .../userservice/entity/place/Cafe.java | 49 +++++++ .../userservice/entity/place/Restaurant.java | 67 +++++++++ .../place/AccommodationRepository.java | 9 ++ .../place/AttractionRepository.java | 9 ++ .../repository/place/CafeRepository.java | 8 ++ .../place/RestaurantRepository.java | 9 ++ .../userservice/service/AuthService.java | 4 +- .../userservice/service/FavoriteService.java | 136 ++++++++++++++---- 11 files changed, 381 insertions(+), 32 deletions(-) create mode 100644 user-service/src/main/java/com/example/userservice/entity/place/Accommodation.java create mode 100644 user-service/src/main/java/com/example/userservice/entity/place/Area.java create mode 100644 user-service/src/main/java/com/example/userservice/entity/place/Attraction.java create mode 100644 user-service/src/main/java/com/example/userservice/entity/place/Cafe.java create mode 100644 user-service/src/main/java/com/example/userservice/entity/place/Restaurant.java create mode 100644 user-service/src/main/java/com/example/userservice/repository/place/AccommodationRepository.java create mode 100644 user-service/src/main/java/com/example/userservice/repository/place/AttractionRepository.java create mode 100644 user-service/src/main/java/com/example/userservice/repository/place/CafeRepository.java create mode 100644 user-service/src/main/java/com/example/userservice/repository/place/RestaurantRepository.java diff --git a/user-service/src/main/java/com/example/userservice/entity/place/Accommodation.java b/user-service/src/main/java/com/example/userservice/entity/place/Accommodation.java new file mode 100644 index 0000000..4bbf6a6 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/entity/place/Accommodation.java @@ -0,0 +1,32 @@ +package com.example.userservice.entity.place; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.Getter; + +import java.math.BigDecimal; + +@Data +@Entity +@Getter +public class Accommodation { + @Id + @Column(name = "accommodation_id") + private Long accommodationId; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "area_id", referencedColumnName = "area_id", nullable = false) + private Area area; + + private String name; + private String address; + @Column(precision = 10, scale = 6) + private BigDecimal lat; + @Column(precision = 10, scale = 6) + private BigDecimal lon; + private String phone; + private String gu; + private String type; + @Column(name = "kakaomap_url") + private String kakaomapUrl; +} diff --git a/user-service/src/main/java/com/example/userservice/entity/place/Area.java b/user-service/src/main/java/com/example/userservice/entity/place/Area.java new file mode 100644 index 0000000..afb808b --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/entity/place/Area.java @@ -0,0 +1,40 @@ +package com.example.userservice.entity.place; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Entity +@Data +public class Area { + @Id + @Column(name = "area_id") + private Long areaId; + + @Column(name = "seoul_id", length = 10, nullable = false, unique = true) + private String seoulId; + + @Column(name = "name", length = 50, nullable = false) + private String name; // 지역명 + + @Column(name = "name_eng", length = 100, nullable = false) + private String nameEng; + + @Column(name = "category", length = 20, nullable = false) + private String category; // 구분 + + @Column(name = "lat", precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 + + @Column(name = "lon", precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 + + @OneToMany(mappedBy = "area", fetch = FetchType.LAZY) + @JsonIgnore + private List attractions; + + +} \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/entity/place/Attraction.java b/user-service/src/main/java/com/example/userservice/entity/place/Attraction.java new file mode 100644 index 0000000..7ae8e1a --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/entity/place/Attraction.java @@ -0,0 +1,50 @@ +package com.example.userservice.entity.place; + +import jakarta.persistence.*; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@Entity +public class Attraction { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "attraction_id") + private Long attractionId; + + @Column(name = "seoul_attraction_id", length = 10, nullable = false, unique = true) + private String seoulAttractionId; + + @Column(name="name", length = 200, nullable = false) + private String name; + + @Column(name="address", length = 200, nullable = false) + private String address; + + @Column(name = "lat", precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 + + @Column(name = "lon", precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 + + @Column(name="phone", length = 30) + private String phone; + + @Column(name="homepage_url", columnDefinition = "TEXT") + private String homepageUrl; + + @Column(name="close_day", columnDefinition = "TEXT") + private String closeDay; + + @Column(name="use_time", columnDefinition = "TEXT") + private String useTime; + + @Column(name="kakaomap_url",columnDefinition = "TEXT") + private String kakaomapUrl; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "area_id", nullable = false) + private Area area; + +} diff --git a/user-service/src/main/java/com/example/userservice/entity/place/Cafe.java b/user-service/src/main/java/com/example/userservice/entity/place/Cafe.java new file mode 100644 index 0000000..d60cba7 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/entity/place/Cafe.java @@ -0,0 +1,49 @@ +package com.example.userservice.entity.place; + + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Entity +@Table(name = "cafe") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Cafe { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "cafe_id") + private Long id; + + @Column(name = "name") + private String name; + + @Column(name = "address") + private String address; + + @Column(name = "lat", precision = 10, scale = 6) + private BigDecimal lat; // 위도 + + @Column(name = "lon", precision = 10, scale = 6) + private BigDecimal lon; // 경도 + + @Column(name = "phone") + private String phone; + + @Column(name = "kakaomap_url") + private String kakaomapUrl; + + @Column(name = "category_code") + private String categoryCode; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "area_id") + private Area area; +} \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/entity/place/Restaurant.java b/user-service/src/main/java/com/example/userservice/entity/place/Restaurant.java new file mode 100644 index 0000000..63f1a61 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/entity/place/Restaurant.java @@ -0,0 +1,67 @@ +package com.example.userservice.entity.place; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; + +@Entity +@Getter +@Setter +@Table(name = "restaurant") // 테이블명 명시 +public class Restaurant { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "restaurant_id") + private Long restaurantId; // PK + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "area_id", nullable = false) + private Area area; // Area 엔티티와 다대일 관계 + + @Column(length = 50, nullable = false) + private String name; // 음식점 이름 + + @Column(length = 200) + private String address; // 음식점 주소 + + @Column(precision = 10, scale = 6, nullable = false) + private BigDecimal lat; // 위도 + + @Column(precision = 10, scale = 6, nullable = false) + private BigDecimal lon; // 경도 + + @Column(length = 20) + private String phone; // 전화번호 + + @Column(length = 200) + private String kakaomap_url; // 카카오맵 URL + + @Column(length = 20) + private String category_code; // 카테고리 코드 (FD6: 음식점) + +// @Column(length = 30) +// private String kakao_id; // 카카오 API가 주는 id 저장 (String) + + //추가 + @Column(length = 100) + private String categoryGroupName; // (예시) 음식점 + + @Column(length = 200) + private String categoryName; // (예시) 음식점 > 한식 > 육류,고기 + + @Column(name = "kakao_id", length = 30) + private String kakao_id; + + public String getKakao_id() { + return kakao_id; + } + + public void setKakao_id(String kakao_id) { + this.kakao_id = kakao_id; + } + +} + diff --git a/user-service/src/main/java/com/example/userservice/repository/place/AccommodationRepository.java b/user-service/src/main/java/com/example/userservice/repository/place/AccommodationRepository.java new file mode 100644 index 0000000..b449b5a --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/repository/place/AccommodationRepository.java @@ -0,0 +1,9 @@ +package com.example.userservice.repository.place; + +import com.example.userservice.entity.place.Accommodation; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AccommodationRepository extends JpaRepository { + boolean existsById(Long id); + +} diff --git a/user-service/src/main/java/com/example/userservice/repository/place/AttractionRepository.java b/user-service/src/main/java/com/example/userservice/repository/place/AttractionRepository.java new file mode 100644 index 0000000..cbf1493 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/repository/place/AttractionRepository.java @@ -0,0 +1,9 @@ +package com.example.userservice.repository.place; + +import com.example.userservice.entity.place.Attraction; +import org.springframework.data.jpa.repository.JpaRepository; + + +public interface AttractionRepository extends JpaRepository { + boolean existsById(Long id); +} diff --git a/user-service/src/main/java/com/example/userservice/repository/place/CafeRepository.java b/user-service/src/main/java/com/example/userservice/repository/place/CafeRepository.java new file mode 100644 index 0000000..554c044 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/repository/place/CafeRepository.java @@ -0,0 +1,8 @@ +package com.example.userservice.repository.place; + +import com.example.userservice.entity.place.Cafe; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CafeRepository extends JpaRepository { + boolean existsById(Long id); +} \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/repository/place/RestaurantRepository.java b/user-service/src/main/java/com/example/userservice/repository/place/RestaurantRepository.java new file mode 100644 index 0000000..584243e --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/repository/place/RestaurantRepository.java @@ -0,0 +1,9 @@ +package com.example.userservice.repository.place; + +import com.example.userservice.entity.place.Restaurant; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RestaurantRepository extends JpaRepository { + boolean existsById(Long id); + +} \ No newline at end of file diff --git a/user-service/src/main/java/com/example/userservice/service/AuthService.java b/user-service/src/main/java/com/example/userservice/service/AuthService.java index cd65108..bb4ecc2 100644 --- a/user-service/src/main/java/com/example/userservice/service/AuthService.java +++ b/user-service/src/main/java/com/example/userservice/service/AuthService.java @@ -7,6 +7,7 @@ import com.example.userservice.security.JwtUtil; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -24,6 +25,7 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; +@Slf4j @Service @RequiredArgsConstructor public class AuthService { @@ -43,7 +45,7 @@ public class AuthService { * @return 토큰과 사용자 정보가 포함된 응답 */ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) { - System.out.println("로그인 시도: " + request.getUser_id()); + log.info("로그인 시도: ", request.getUser_id()); // 1. 사용자 조회 (userId로 조회) Member member = memberRepository.findByUserId(request.getUser_id()) diff --git a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java index 88c12a6..76ab9ab 100644 --- a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java +++ b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java @@ -3,11 +3,20 @@ import com.example.userservice.dto.FavoriteDto; import com.example.userservice.entity.Favorite; import com.example.userservice.entity.Member; +import com.example.userservice.entity.place.Accommodation; +import com.example.userservice.entity.place.Attraction; +import com.example.userservice.entity.place.Cafe; +import com.example.userservice.entity.place.Restaurant; import com.example.userservice.repository.FavoriteRepository; import com.example.userservice.repository.jpa.MemberRepository; +import com.example.userservice.repository.place.AccommodationRepository; +import com.example.userservice.repository.place.AttractionRepository; +import com.example.userservice.repository.place.CafeRepository; +import com.example.userservice.repository.place.RestaurantRepository; import com.fasterxml.jackson.databind.JsonNode; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; @@ -15,14 +24,11 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; - +@Slf4j @Service @RequiredArgsConstructor public class FavoriteService { @@ -31,6 +37,11 @@ public class FavoriteService { private final MemberRepository memberRepository; private static final RestTemplate restTemplate = new RestTemplate(); + private final AccommodationRepository accommodationRepository; + private final AttractionRepository attractionRepository; + private final CafeRepository cafeRepository; + private final RestaurantRepository restaurantRepository; + @Value("${gateway-url}") private String gatewayUrl; @@ -60,6 +71,7 @@ public List getListData(String userId) { } catch (Exception e) { // throw new RuntimeException(e); + log.error(e.getMessage()); throw new BadCredentialsException("즐겨찾기 장소 조회 실패",e); } } @@ -83,18 +95,37 @@ public ResponseEntity> addFavoriteData(String userId, Favori } // 3. 존재하는 장소인지 type/id 확인 - try { - ResponseEntity checkPlace = restTemplate.exchange( - gatewayUrl + "/place/main/info/" + favoriteDto.getType() + "/" + favoriteDto.getPlace_id(), - HttpMethod.GET, - null, - JsonNode.class - ); - } catch (Exception e) { - response.put("message", "잘못된 장소 type/id 입니다 "+e.getMessage()); - return ResponseEntity.status(400).body(response); + if(Objects.equals(favoriteDto.getType(), "cafe")){ + if(!cafeRepository.existsById(Long.valueOf(favoriteDto.getPlace_id()))){ + response.put("message", "잘못된 장소 type/id 입니다 "); + return ResponseEntity.status(400).body(response); + } + }else if(Objects.equals(favoriteDto.getType(), "restaurant")){ + if(!restaurantRepository.existsById(Long.valueOf(favoriteDto.getPlace_id()))){ + response.put("message", "잘못된 장소 type/id 입니다 "); + return ResponseEntity.status(400).body(response); + } + }else if(Objects.equals(favoriteDto.getType(), "attraction")){ + if(!attractionRepository.existsById(Long.valueOf(favoriteDto.getPlace_id()))){ + response.put("message", "잘못된 장소 type/id 입니다 "); + return ResponseEntity.status(400).body(response); + } + }else if(Objects.equals(favoriteDto.getType(), "accommodation")){ + if(!accommodationRepository.existsById(Long.valueOf(favoriteDto.getPlace_id()))){ + response.put("message", "잘못된 장소 type/id 입니다 "); + return ResponseEntity.status(400).body(response); + } } + +// ResponseEntity checkPlace = restTemplate.exchange( +// gatewayUrl + "/place/main/info/" + favoriteDto.getType() + "/" + favoriteDto.getPlace_id(), +// HttpMethod.GET, +// null, +// JsonNode.class +// ); +// + // 4. Favorite 엔티티 생성 Favorite favorite = new Favorite(); favorite.setType(favoriteDto.getType()); @@ -136,22 +167,64 @@ public List> getAllFavoriteData() { List favoriteDtoList = items.stream() .map(item ->{ - ResponseEntity response = restTemplate.exchange( - gatewayUrl+"/place/main/info/"+item.getType()+"/" + item.getPlaceId(), - HttpMethod.GET, - null, - JsonNode.class - ); - String name = response.getBody().get(item.getType()+"_name").asText(); - String address = response.getBody().get("address").asText(); - - return new FavoriteDto( - item.getFavoriteId(), - item.getType(), - item.getPlaceId(), - item.getMember().getUserId(), - name, - address); + + if(Objects.equals(item.getType(), "cafe")){ + Optional cafe = cafeRepository.findById(Long.valueOf(item.getPlaceId())); + String name = cafe.get().getName(); + String address = cafe.get().getAddress(); + return new FavoriteDto( + item.getFavoriteId(), + item.getType(), + item.getPlaceId(), + item.getMember().getUserId(), + name, + address); + }else if(Objects.equals(item.getType(), "restaurant")){ + Optional retaurant = restaurantRepository.findById(Long.valueOf(item.getPlaceId())); + String name = retaurant.get().getName(); + String address = retaurant.get().getAddress(); + return new FavoriteDto( + item.getFavoriteId(), + item.getType(), + item.getPlaceId(), + item.getMember().getUserId(), + name, + address); + }else if(Objects.equals(item.getType(), "attraction")){ + Optional attraction = attractionRepository.findById(Long.valueOf(item.getPlaceId())); + String name = attraction.get().getName(); + String address = attraction.get().getAddress(); + return new FavoriteDto( + item.getFavoriteId(), + item.getType(), + item.getPlaceId(), + item.getMember().getUserId(), + name, + address); + }else if(Objects.equals(item.getType(), "accommodation")){ + Optional accommodation = accommodationRepository.findById(Long.valueOf(item.getPlaceId())); + String name = accommodation.get().getName(); + String address = accommodation.get().getAddress(); + return new FavoriteDto( + item.getFavoriteId(), + item.getType(), + item.getPlaceId(), + item.getMember().getUserId(), + name, + address); + } + + +// ResponseEntity response = restTemplate.exchange( +// gatewayUrl+"/place/main/info/"+item.getType()+"/" + item.getPlaceId(), +// HttpMethod.GET, +// null, +// JsonNode.class +// ); +// String name = response.getBody().get(item.getType()+"_name").asText(); +// String address = response.getBody().get("address").asText(); + + return null; }).toList(); Map> grouped = favoriteDtoList.stream() @@ -171,6 +244,7 @@ public List> getAllFavoriteData() { } catch (Exception e) { // throw new RuntimeException(e); + log.error(e.getMessage()); throw new BadCredentialsException("즐겨찾기 장소 조회 실패",e); } From d96dd1ed34baf1757a5228130f7daa5c468cf9ac Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Mon, 26 May 2025 10:34:49 +0900 Subject: [PATCH 344/361] =?UTF-8?q?=EB=A1=9C=EA=B9=85=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CongestionController.java | 6 ++-- .../scheduler/PushScheduler.java | 11 +++---- .../service/CongestionService.java | 8 +++-- .../controller/StreamController.java | 20 +++++++------ .../service/AccidentEsService.java | 4 ++- .../service/ESRoadService.java | 4 ++- .../controller/AuthController.java | 4 ++- .../controller/MemberController.java | 16 +++++----- .../controller/SignupController.java | 6 ++-- .../controller/SuggestController.java | 12 ++++---- .../CustomAuthenticationEntryPoint.java | 4 ++- .../example/userservice/security/JwtUtil.java | 4 ++- .../userservice/service/AuthService.java | 30 +++++++++---------- .../userservice/service/TokenService.java | 22 +++++++------- 14 files changed, 86 insertions(+), 65 deletions(-) diff --git a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java index f78f5c2..b9369c1 100644 --- a/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java +++ b/congestion-service/src/main/java/com/example/congestionservice/controller/CongestionController.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -21,6 +22,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/main") +@Slf4j public class CongestionController { private final List emitters = new CopyOnWriteArrayList<>(); private final CongestionPreviousCache congestionPreviousCache; @@ -39,7 +41,7 @@ public SseEmitter streamCongestion() { // 초기데이터 투입 try { - System.out.println("혼잡도 초기 데이터 푸시 중..."); + log.info("혼잡도 초기 데이터 푸시 중..."); var congestionList = congestionService.getCongestion(); emitter.send(SseEmitter.event() @@ -75,7 +77,7 @@ public SseEmitter streamCongestion() { .name("congestion-alert") .data(changedList)); - System.out.println(".. 혼잡도 초기 데이터 푸시 완료"); + log.info(".. 혼잡도 초기 데이터 푸시 완료"); } catch (IOException | IllegalStateException e) { emitter.completeWithError(e); diff --git a/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java b/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java index 5e64030..0e59ad8 100644 --- a/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java +++ b/congestion-service/src/main/java/com/example/congestionservice/scheduler/PushScheduler.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -16,6 +17,7 @@ @Component @RequiredArgsConstructor +@Slf4j public class PushScheduler { private final CongestionController congestionController; private final CongestionPreviousCache congestionPreviousCache; @@ -28,8 +30,7 @@ public class PushScheduler { public void push() { Map previousLevels = congestionPreviousCache.getPreviousLevels(); - - System.out.println("혼잡도 푸시 중..."); + log.info("혼잡도 푸시 중..."); var congestionList = congestionService.getCongestion(); congestionController.sendToClients(congestionList); // 모든 지역 혼잡도 전송 @@ -47,7 +48,7 @@ public void push() { String previousLevel = previousLevels.get(areaName); // 혼잡도 변화 알림 로그 확인 - System.out.println(areaName+"의"+previousLevel+":ㅣㅣ"+currentLevel); + // System.out.println(areaName+"의"+previousLevel+":ㅣㅣ"+currentLevel); // 혼잡도 알림 전송(조건 : 1,2단계 -> 3단계 // 4단계) if (previousLevel == null && (currentLevel.equals("약간 붐빔") || currentLevel.equals("붐빔"))) { @@ -83,9 +84,9 @@ else if ((previousLevel.equals("여유") || previousLevel.equals("보통")) && ( // 변화 없으면 return if (changedList.isEmpty()) { - System.out.println("혼잡도 변화 없음"); + log.info("혼잡도 변화 없음"); }else{ - System.out.println("혼잡도 변화 있음 → alert-update로 SSE 전송"); + log.info("혼잡도 변화 있음 → alert-update로 SSE 전송"); congestionController.sendAlertToClients(changedList); // ✨ 바뀐 것만 보냄 } diff --git a/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java b/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java index dce6816..bfb9863 100644 --- a/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java +++ b/congestion-service/src/main/java/com/example/congestionservice/service/CongestionService.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.kafka.common.errors.InvalidRequestException; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; @@ -20,6 +21,7 @@ @Service @RequiredArgsConstructor +@Slf4j public class CongestionService { private static final RestTemplate restTemplate = new RestTemplate(); @@ -31,7 +33,7 @@ public JsonNode getCongestion() { // 오늘 날짜 구하기 String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); String apiUrl = String.format(elastic_url + "/seoul_citydata_congestion_%s/_search", today); - System.out.println(apiUrl); + // System.out.println(apiUrl); // JSON Body 생성 String jsonBody = String.format("{\n" + @@ -93,8 +95,8 @@ public JsonNode getCongestion() { }catch (InvalidRequestException e) { throw new InvalidRequestException(e.getMessage(), e); }catch (Exception e){ - System.out.println(e); - throw new RuntimeException("예상치 못한 오류",e); + log.error(String.valueOf(e)); + throw new RuntimeException("예상치 못한 오류", e); } } } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java index 5c90677..6beda5b 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/controller/StreamController.java @@ -6,6 +6,7 @@ import com.example.externalinfoservice.service.WeatherEsService; import com.fasterxml.jackson.databind.JsonNode; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -20,6 +21,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/main") +@Slf4j public class StreamController { private final List emitters = new CopyOnWriteArrayList<>(); @@ -39,7 +41,7 @@ public SseEmitter stream() { // 구독시, 초기 데이터 주입 try { - System.out.println("초기 데이터 푸시 중..."); + log.info("초기 데이터 푸시 중..."); var weatherList = weatherEsService.getAllWeatherFromES(); var trafficList = roadService.getTrafficData(); var parkList = parkEsService.getAllParkFromES(); @@ -58,10 +60,10 @@ public SseEmitter stream() { .name("accident-alert") .data(accidentList)); - System.out.println(".. 초기 데이터 푸시 완료"); + log.info(".. 초기 데이터 푸시 완료"); } catch (IOException | IllegalStateException e) { - System.out.println("SSE 오류 발생"); + log.error("SSE 오류 발생"); emitter.completeWithError(e); emitters.remove(emitter); } @@ -73,30 +75,30 @@ public SseEmitter stream() { public void sendToClients(List> weatherList, JsonNode trafficList, List> parkList, List> accidentList) { for (SseEmitter emitter : emitters) { try { - System.out.println("external 데이터 푸시 중..."); + log.info("external 데이터 푸시 중..."); emitter.send(SseEmitter.event() .name("weather-update") .data(weatherList)); - System.out.println(".. 날씨 데이터 푸시 완료"); + log.info(".. 날씨 데이터 푸시 완료"); emitter.send(SseEmitter.event() .name("traffic-update") .data(trafficList)); - System.out.println(".. 도로현황 데이터 푸시 완료"); + log.info(".. 도로현황 데이터 푸시 완료"); emitter.send(SseEmitter.event() .name("park-update") .data(parkList)); - System.out.println(".. 주차 데이터 푸시 완료"); + log.info(".. 주차 데이터 푸시 완료"); emitter.send(SseEmitter.event() .name("accident-alert") .data(accidentList)); - System.out.println(".. 사고 데이터 푸시 완료"); + log.info(".. 사고 데이터 푸시 완료"); } catch (IOException | IllegalStateException e) { - System.out.println("SSE 오류 발생"); + log.error("SSE 오류 발생"); emitter.completeWithError(e); emitters.remove(emitter); } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java index 1679fdd..7d3e9c6 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/AccidentEsService.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.kafka.common.errors.InvalidRequestException; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; @@ -20,6 +21,7 @@ @Service @RequiredArgsConstructor +@Slf4j public class AccidentEsService { private final RestTemplate restTemplate = new RestTemplate(); // 주입 방식으로 대체해도 OK @@ -76,7 +78,7 @@ public JsonNode getAccidentData() { } catch (InvalidRequestException e) { throw new InvalidRequestException(e.getMessage(), e); } catch (Exception e) { - System.out.println(e); + log.error(String.valueOf(e)); throw new RuntimeException("사고 현황 : 예상치 못한 오류", e); } } diff --git a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java index 95b1091..60c32c4 100644 --- a/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java +++ b/external-info-service/src/main/java/com/example/externalinfoservice/service/ESRoadService.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.kafka.common.errors.InvalidRequestException; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; @@ -19,6 +20,7 @@ @Service @RequiredArgsConstructor +@Slf4j public class ESRoadService { private static final RestTemplate restTemplate = new RestTemplate(); @@ -91,7 +93,7 @@ public JsonNode getTrafficData() { }catch (InvalidRequestException e) { throw new InvalidRequestException(e.getMessage(), e); }catch (Exception e){ - System.out.println(e); + log.error(String.valueOf(e)); throw new RuntimeException("도로 현황 데이터 : 예상치 못한 오류",e); } } diff --git a/user-service/src/main/java/com/example/userservice/controller/AuthController.java b/user-service/src/main/java/com/example/userservice/controller/AuthController.java index bbdde96..6c71697 100644 --- a/user-service/src/main/java/com/example/userservice/controller/AuthController.java +++ b/user-service/src/main/java/com/example/userservice/controller/AuthController.java @@ -11,6 +11,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -23,6 +24,7 @@ @RestController @RequestMapping("/auth") @RequiredArgsConstructor +@Slf4j public class AuthController { private final AuthService authService; @@ -43,7 +45,7 @@ public ResponseEntity login( HttpServletResponse response) { // 디버깅 로그 출력 - System.out.println("로그인 컨트롤러 호출: " + request.getUser_id()); + log.debug("로그인 컨트롤러 호출: {}", request.getUser_id()); AuthDto.LoginResponse loginResponse = authService.login(request); diff --git a/user-service/src/main/java/com/example/userservice/controller/MemberController.java b/user-service/src/main/java/com/example/userservice/controller/MemberController.java index faf39c4..ca70991 100644 --- a/user-service/src/main/java/com/example/userservice/controller/MemberController.java +++ b/user-service/src/main/java/com/example/userservice/controller/MemberController.java @@ -5,11 +5,13 @@ import com.example.userservice.service.MemberService; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor +@Slf4j public class MemberController { private final MemberService memberService; @@ -58,9 +60,9 @@ public ResponseEntity updateProfile( */ private String extractUserIdFromRequest(HttpServletRequest request) { // 요청 디버깅 로그 - System.out.println("요청 URI: " + request.getRequestURI()); - System.out.println("Authorization 헤더: " + request.getHeader("Authorization")); - System.out.println("accessToken 헤더: " + request.getHeader("accessToken")); + log.debug("요청 URI: {}", request.getRequestURI()); + log.debug("Authorization 헤더: {}", request.getHeader("Authorization")); + log.debug("accessToken 헤더: {}", request.getHeader("accessToken")); String authHeader = request.getHeader("Authorization"); String accessTokenHeader = request.getHeader("accessToken"); @@ -69,22 +71,22 @@ private String extractUserIdFromRequest(HttpServletRequest request) { // Authorization 헤더 확인 if (authHeader != null && authHeader.startsWith("Bearer ")) { jwt = authHeader.substring(7); - System.out.println("Authorization 헤더에서 토큰 추출: " + jwt); + log.debug("Authorization 헤더에서 토큰 추출: {}", jwt); } // accessToken 헤더 확인 else if (accessTokenHeader != null) { jwt = accessTokenHeader; - System.out.println("accessToken 헤더에서 토큰 추출: " + jwt); + log.debug("accessToken 헤더에서 토큰 추출: {}", jwt); } // 토큰 유효성 검사 및 사용자 ID 추출 if (jwt != null) { try { String userId = jwtUtil.extractUsername(jwt); - System.out.println("추출된 사용자 ID: " + userId); + log.debug("추출된 사용자 ID: {}", userId); return userId; } catch (Exception e) { - System.out.println("토큰 처리 중 오류 발생: " + e.getMessage()); + log.error("토큰 처리 중 오류 발생: {}", e.getMessage()); e.printStackTrace(); } } diff --git a/user-service/src/main/java/com/example/userservice/controller/SignupController.java b/user-service/src/main/java/com/example/userservice/controller/SignupController.java index 8a1a13c..3e0cb7d 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SignupController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SignupController.java @@ -6,6 +6,7 @@ import com.example.userservice.service.MemberService; import com.example.userservice.service.TokenService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -21,6 +22,7 @@ @RestController @RequiredArgsConstructor @RequestMapping("/auth") +@Slf4j public class SignupController { private final MemberService memberService; private final JwtUtil jwtUtil; @@ -32,7 +34,7 @@ public class SignupController { @PostMapping("/signup") public ResponseEntity> signup(@RequestBody MemberSign dto) { try { - System.out.println("회원가입 요청: userId=" + dto.getUserId()); + log.debug("회원가입 요청: userId={}", dto.getUserId()); Member member = memberService.registerUser(dto); @@ -83,7 +85,7 @@ public ResponseEntity signout( try { memberService.deleteMemberById(memberId); - System.out.println("회원탈퇴 완료: memberId=" + memberId); + log.debug("회원탈퇴 완료: memberId={}", memberId); return ResponseEntity.ok("회원 탈퇴가 완료되었습니다."); } catch (IllegalArgumentException e) { return ResponseEntity.badRequest().body("회원 탈퇴 실패: " + e.getMessage()); diff --git a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java index c657598..13426c0 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java @@ -5,6 +5,7 @@ import com.example.userservice.dto.SuggestFastRequestDto; import com.example.userservice.entity.Member; import com.example.userservice.service.MemberService; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.security.core.Authentication; @@ -19,6 +20,7 @@ @RestController @RequestMapping("/suggest") +@Slf4j public class SuggestController { @Value("${fastapi-svc}") private String fastApiUrl; @@ -72,21 +74,21 @@ public ResponseEntity suggestTourPlan(@PathVariable String user_id, @RequestB if (response.getStatusCode() == HttpStatus.OK) { SuggestFastResponseDto suggestFastResponseDto = response.getBody(); - System.out.println("FastAPI 응답: " + suggestFastResponseDto); + log.debug("FastAPI 응답: {}", suggestFastResponseDto); return ResponseEntity.ok(suggestFastResponseDto); } else { - System.err.println("FastAPI 요청 실패: " + response.getStatusCode()); + log.error("FastAPI 요청 실패: {}", response.getStatusCode()); return ResponseEntity.status(response.getStatusCode()).body("FastAPI 서버로부터 오류 응답: " + response.getStatusCode()); } } catch (HttpClientErrorException e) { - System.err.println("클라이언트 오류: " + e.getStatusCode() + " - " + e.getResponseBodyAsString()); + log.error("클라이언트 오류: {} - {}", e.getStatusCode(), e.getResponseBodyAsString()); return ResponseEntity.status(e.getStatusCode()).body("FastAPI 호출 중 클라이언트 오류: " + e.getResponseBodyAsString()); } catch (HttpServerErrorException e) { - System.err.println("서버 오류: " + e.getStatusCode() + " - " + e.getResponseBodyAsString()); + log.error("서버 오류: {} - {}", e.getStatusCode(), e.getResponseBodyAsString()); return ResponseEntity.status(e.getStatusCode()).body("FastAPI 호출 중 서버 오류: " + e.getResponseBodyAsString()); } catch (RestClientException e) { - System.err.println("RestClientException: " + e.getMessage()); + log.error("RestClientException: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body("FastAPI 서비스 통신 오류: " + e.getMessage()); } } diff --git a/user-service/src/main/java/com/example/userservice/exception/CustomAuthenticationEntryPoint.java b/user-service/src/main/java/com/example/userservice/exception/CustomAuthenticationEntryPoint.java index b0970e9..cd15374 100644 --- a/user-service/src/main/java/com/example/userservice/exception/CustomAuthenticationEntryPoint.java +++ b/user-service/src/main/java/com/example/userservice/exception/CustomAuthenticationEntryPoint.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; @@ -12,6 +13,7 @@ import java.util.LinkedHashMap; import java.util.Map; +@Slf4j public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override @@ -25,7 +27,7 @@ public void commence(HttpServletRequest request, HttpServletResponse response, A detailedMessage += " | 원인: " + cause.getMessage(); } // 로그에 예외 내용 남기기 - System.out.println("인증 실패 발생:"+detailedMessage+", 요청 URI: " + request.getRequestURI() + authException.getMessage()+ request.getRequestURI()+ authException); + log.error("인증 실패 발생:{}, 요청 URI: {} {}", detailedMessage, request.getRequestURI(), authException.getMessage()); sendErrorResponse(response, HttpStatus.UNAUTHORIZED, detailedMessage, request.getRequestURI()); } diff --git a/user-service/src/main/java/com/example/userservice/security/JwtUtil.java b/user-service/src/main/java/com/example/userservice/security/JwtUtil.java index c4a02f3..412d6d7 100644 --- a/user-service/src/main/java/com/example/userservice/security/JwtUtil.java +++ b/user-service/src/main/java/com/example/userservice/security/JwtUtil.java @@ -6,6 +6,7 @@ import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; @@ -16,6 +17,7 @@ import java.util.function.Function; @Component +@Slf4j public class JwtUtil { private SecretKey secretKey; @@ -38,7 +40,7 @@ public void init() { this.accessTokenExpiration = 1000L * 60 * 45; // 45분 this.refreshTokenExpiration = 1000L * 60 * 60 * 6; // 6시간 - System.out.println("JWT 설정 완료: 액세스 토큰 = " + (accessTokenExpiration / 60000) + "분, 리프레시 토큰 = " + (refreshTokenExpiration / 60000) + "분"); + log.debug("JWT 설정 완료: 액세스 토큰 = {}분, 리프레시 토큰 = {}분", accessTokenExpiration / 60000, refreshTokenExpiration / 60000); } // 사용자 이름 추출 diff --git a/user-service/src/main/java/com/example/userservice/service/AuthService.java b/user-service/src/main/java/com/example/userservice/service/AuthService.java index bb4ecc2..f7eb6b9 100644 --- a/user-service/src/main/java/com/example/userservice/service/AuthService.java +++ b/user-service/src/main/java/com/example/userservice/service/AuthService.java @@ -45,16 +45,16 @@ public class AuthService { * @return 토큰과 사용자 정보가 포함된 응답 */ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) { - log.info("로그인 시도: ", request.getUser_id()); + log.debug("로그인 시도: {}", request.getUser_id()); // 1. 사용자 조회 (userId로 조회) Member member = memberRepository.findByUserId(request.getUser_id()) .orElseThrow(() -> { - System.out.println("사용자를 찾을 수 없음: " + request.getUser_id()); + log.error("사용자를 찾을 수 없음: {}", request.getUser_id()); return new RuntimeException("사용자를 찾을 수 없습니다"); }); - System.out.println("사용자 찾음: ID=" + member.getMemberId() + ", UserID=" + member.getUserId()); + log.debug("사용자 찾음: ID={}, UserID={}", member.getMemberId(), member.getUserId()); // 2. 인증 시도 try { @@ -64,23 +64,23 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) { request.getPassword() ) ); - System.out.println("인증 성공"); + log.debug("인증 성공"); } catch (Exception e) { - System.out.println("인증 실패: " + e.getMessage()); + log.error("인증 실패: {}", e.getMessage()); throw e; } // 3. 기존 토큰이 있으면 삭제 Optional existingToken = tokenService.findRefreshTokenByMemberId(member.getMemberId()); if (existingToken.isPresent()) { - System.out.println("기존 리프레시 토큰 발견: 사용자 ID = " + member.getMemberId() + ", 삭제 진행"); + log.debug("기존 리프레시 토큰 발견: 사용자 ID = {}, 삭제 진행", member.getMemberId()); tokenService.deleteRefreshToken(member.getMemberId()); } // 기존 토큰 버전이 있으면 삭제 (이전 토큰 무효화) String versionKey = TOKEN_VERSION_PREFIX + member.getMemberId(); if (Boolean.TRUE.equals(redisTemplate.hasKey(versionKey))) { - System.out.println("기존 토큰 버전 발견: 사용자 ID = " + member.getMemberId() + ", 삭제 진행"); + log.debug("기존 토큰 버전 발견: 사용자 ID = {}, 삭제 진행", member.getMemberId()); redisTemplate.delete(versionKey); } @@ -101,7 +101,7 @@ public AuthDto.LoginResponse login(AuthDto.LoginRequest request) { String tokenId = jwtUtil.extractTokenId(accessToken); saveTokenVersion(member.getMemberId(), tokenId); - System.out.println("새 토큰 발급 완료: 액세스 토큰 유효 시간 = 45분, 사용자 ID = " + member.getMemberId() + ", 토큰 ID = " + tokenId); + log.debug("새 토큰 발급 완료: 액세스 토큰 유효 시간 = 45분, 사용자 ID = {}, 토큰 ID = {}", member.getMemberId(), tokenId); // 6. 결과 반환 return AuthDto.LoginResponse.builder() @@ -127,14 +127,14 @@ public AuthDto.LogoutResponse logout(AuthDto.LogoutRequest request) { String versionKey = TOKEN_VERSION_PREFIX + memberId; if (Boolean.TRUE.equals(redisTemplate.hasKey(versionKey))) { redisTemplate.delete(versionKey); - System.out.println("토큰 버전 삭제 완료: 사용자 ID = " + memberId); + log.debug("토큰 버전 삭제 완료: 사용자 ID = {}", memberId); } // 2. 리프레시 토큰 삭제 tokenService.deleteRefreshToken(memberId); - System.out.println("리프레시 토큰 삭제 완료: 사용자 ID = " + memberId); + log.debug("리프레시 토큰 삭제 완료: 사용자 ID = {}", memberId); - System.out.println("로그아웃 완료: 사용자 ID = " + memberId); + log.debug("로그아웃 완료: 사용자 ID = {}", memberId); // 3. 성공 리턴 return AuthDto.LogoutResponse.builder() @@ -188,7 +188,7 @@ public String refreshAccessToken(String refreshToken) { String tokenId = jwtUtil.extractTokenId(newAccessToken); saveTokenVersion(userId, tokenId); - System.out.println("액세스 토큰 재발급 완료: 유효 시간 = 45분, 사용자 ID = " + userId + ", 토큰 ID = " + tokenId); + log.debug("액세스 토큰 재발급 완료: 유효 시간 = 45분, 사용자 ID = {}, 토큰 ID = {}", userId, tokenId); return newAccessToken; } @@ -201,7 +201,7 @@ public String refreshAccessToken(String refreshToken) { private void saveTokenVersion(Long userId, String tokenId) { String key = TOKEN_VERSION_PREFIX + userId; redisTemplate.opsForValue().set(key, tokenId, TOKEN_VERSION_TTL, TimeUnit.SECONDS); - System.out.println("토큰 버전 저장: 사용자 ID = " + userId + ", 토큰 ID = " + tokenId); + log.debug("토큰 버전 저장: 사용자 ID = {}, 토큰 ID = {}", userId, tokenId); } /** @@ -220,9 +220,7 @@ public boolean isLatestToken(Long userId, String tokenId) { boolean isLatest = latestTokenId.equals(tokenId); if (!isLatest) { - System.out.println("토큰 버전 불일치: 사용자 ID = " + userId + - ", 요청 토큰 ID = " + tokenId + - ", 최신 토큰 ID = " + latestTokenId); + log.debug("토큰 버전 불일치: 사용자 ID = {}, 요청 토큰 ID = {}, 최신 토큰 ID = {}", userId, tokenId, latestTokenId); } return isLatest; diff --git a/user-service/src/main/java/com/example/userservice/service/TokenService.java b/user-service/src/main/java/com/example/userservice/service/TokenService.java index f316b9e..8d215f7 100644 --- a/user-service/src/main/java/com/example/userservice/service/TokenService.java +++ b/user-service/src/main/java/com/example/userservice/service/TokenService.java @@ -6,6 +6,7 @@ import com.example.userservice.repository.redis.RefreshTokenRepository; import com.example.userservice.security.JwtUtil; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; @@ -18,6 +19,7 @@ @Service @RequiredArgsConstructor +@Slf4j public class TokenService { private final JwtUtil jwtUtil; @@ -54,7 +56,7 @@ public String createNewAccessToken(String refreshToken) { String key = TOKEN_VERSION_PREFIX + userId; if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) { redisTemplate.delete(key); - System.out.println("기존 토큰 버전 삭제: 사용자 ID = " + userId); + log.debug("기존 토큰 버전 삭제: 사용자 ID = {}", userId); } // 사용자 권한 설정 @@ -73,7 +75,7 @@ public String createNewAccessToken(String refreshToken) { String tokenId = jwtUtil.extractTokenId(accessToken); saveTokenVersion(userId, tokenId); - System.out.println("새 액세스 토큰 생성 완료: 사용자 ID = " + userId + ", 토큰 ID = " + tokenId); + log.debug("새 액세스 토큰 생성 완료: 사용자 ID = {}, 토큰 ID = {}", userId, tokenId); return accessToken; } @@ -84,7 +86,7 @@ public String createNewAccessToken(String refreshToken) { public void saveTokenVersion(Long userId, String tokenId) { String key = TOKEN_VERSION_PREFIX + userId; redisTemplate.opsForValue().set(key, tokenId, TOKEN_VERSION_TTL, TimeUnit.SECONDS); - System.out.println("토큰 버전 업데이트: 사용자 ID = " + userId + ", 토큰 ID = " + tokenId); + log.debug("토큰 버전 업데이트: 사용자 ID = {}, 토큰 ID = {}", userId, tokenId); } /** @@ -95,15 +97,13 @@ public boolean isLatestToken(Long userId, String tokenId) { String latestTokenId = redisTemplate.opsForValue().get(key); if (latestTokenId == null) { - System.out.println("저장된 토큰 버전 없음: 사용자 ID = " + userId); + log.debug("저장된 토큰 버전 없음: 사용자 ID = {}", userId); return false; // 저장된 토큰이 없으면 로그아웃된 상태로 간주하고 인증 실패 } boolean isLatest = latestTokenId.equals(tokenId); if (!isLatest) { - System.out.println("토큰 버전 불일치: 사용자 ID = " + userId + - ", 요청 토큰 ID = " + tokenId + - ", 최신 토큰 ID = " + latestTokenId); + log.debug("토큰 버전 불일치: 사용자 ID = {}, 요청 토큰 ID = {}, 최신 토큰 ID = {}", userId, tokenId, latestTokenId); } return isLatest; @@ -115,7 +115,7 @@ public boolean isLatestToken(Long userId, String tokenId) { public void invalidateToken(Long userId) { String key = TOKEN_VERSION_PREFIX + userId; redisTemplate.delete(key); - System.out.println("토큰 무효화 완료: 사용자 ID = " + userId); + log.debug("토큰 무효화 완료: 사용자 ID = {}", userId); } /** @@ -128,7 +128,7 @@ public void saveRefreshToken(Long memberId, String refreshToken) { Optional existingToken = refreshTokenRepository.findById(String.valueOf(memberId)); if (existingToken.isPresent()) { refreshTokenRepository.deleteById(String.valueOf(memberId)); - System.out.println("기존 리프레시 토큰 삭제: 사용자 ID = " + memberId); + log.debug("기존 리프레시 토큰 삭제: 사용자 ID = {}", memberId); } // 새 토큰 저장 @@ -138,7 +138,7 @@ public void saveRefreshToken(Long memberId, String refreshToken) { .build(); refreshTokenRepository.save(token); - System.out.println("새 리프레시 토큰 저장: 사용자 ID = " + memberId); + log.debug("새 리프레시 토큰 저장: 사용자 ID = {}", memberId); } /** @@ -146,7 +146,7 @@ public void saveRefreshToken(Long memberId, String refreshToken) { */ public void deleteRefreshToken(Long memberId) { refreshTokenRepository.deleteById(String.valueOf(memberId)); - System.out.println("리프레시 토큰 삭제 완료: 사용자 ID = " + memberId); + log.debug("리프레시 토큰 삭제 완료: 사용자 ID = {}", memberId); } /** From 1282179f418218399379037058966ba3c383200e Mon Sep 17 00:00:00 2001 From: YunHwa0905 Date: Mon, 26 May 2025 10:39:53 +0900 Subject: [PATCH 345/361] JWTTokenProvider --- .../userservice/jwt/JwtTokenProvider.java | 65 ------------------- 1 file changed, 65 deletions(-) delete mode 100644 user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java diff --git a/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java b/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java deleted file mode 100644 index 323f900..0000000 --- a/user-service/src/main/java/com/example/userservice/jwt/JwtTokenProvider.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.example.userservice.jwt; - -import io.github.cdimascio.dotenv.Dotenv; // .env 파일에서 환경변수를 로드하기 위한 라이브러리 -import io.jsonwebtoken.Jwts; // JWT 생성 및 파싱을 위한 라이브러리 -import io.jsonwebtoken.SignatureAlgorithm; // 서명 알고리즘 정의 -import io.jsonwebtoken.security.Keys; // 안전한 키 생성을 위한 도구 -import jakarta.annotation.PostConstruct; -import org.springframework.stereotype.Component; - -import javax.crypto.SecretKey; -import java.util.Date; - -@Component -public class JwtTokenProvider { - - private SecretKey secretKey; - - @PostConstruct // Bean 생성 후 자동으로 호출되는 메서드 - public void init() { - // .env 파일에서 환경변수를 로드 - Dotenv dotenv = Dotenv.configure() - .ignoreIfMalformed() - .ignoreIfMissing() - .load(); - - - String jwtSecret = dotenv.get("JWT_SECRET_KEY"); - - - if (jwtSecret == null) { - throw new IllegalStateException("JWT_SECRET_KEY 환경변수가 설정되지 않았습니다!"); - } - - - this.secretKey = Keys.hmacShaKeyFor(jwtSecret.getBytes()); - } - - // Access Token 생성 - public String generateAccessToken(String userId) { - Date now = new Date(); - Date expiryDate = new Date(now.getTime() + 1000 * 60 * 45); - - // JWT 생성 - return Jwts.builder() - .setSubject(userId) - .setIssuedAt(now) - .setExpiration(expiryDate) - .signWith(secretKey, SignatureAlgorithm.HS512) - .compact(); - } - - // Refresh Token 생성 - public String generateRefreshToken(String userId) { - Date now = new Date(); // 현재 시간 - Date expiryDate = new Date(now.getTime() + 1000 * 60 * 45); - - // JWT 생성 - return Jwts.builder() - .setSubject(userId) - .setIssuedAt(now) - .setExpiration(expiryDate) - .signWith(secretKey, SignatureAlgorithm.HS512) - .compact(); - } -} From 6becebb3e76c02ebc56ef6f38314f2c2056eaa3a Mon Sep 17 00:00:00 2001 From: ha-yun Date: Mon, 26 May 2025 11:02:29 +0900 Subject: [PATCH 346/361] =?UTF-8?q?Refactor:=20place-=EB=A1=9C=EA=B9=85=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/placeservice/init/DataInit.java | 19 +- .../service/AccommodationService.java | 23 ++- .../service/AiSummaryService.java | 3 +- .../placeservice/service/AreaService.java | 3 + .../service/AttractionKakaoMapClient.java | 58 +++--- .../service/AttractionService.java | 26 ++- .../placeservice/service/CafeService.java | 176 +++++++++--------- .../service/CulturalEventService.java | 21 ++- .../service/RestaurantService.java | 6 +- 9 files changed, 180 insertions(+), 155 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/init/DataInit.java b/place-service/src/main/java/com/example/placeservice/init/DataInit.java index 412f2af..99e7b33 100644 --- a/place-service/src/main/java/com/example/placeservice/init/DataInit.java +++ b/place-service/src/main/java/com/example/placeservice/init/DataInit.java @@ -1,10 +1,10 @@ package com.example.placeservice.init; -import com.example.placeservice.repository.AccommodationRepository; import com.example.placeservice.repository.CafeRepository; import com.example.placeservice.repository.RestaurantRepository; import com.example.placeservice.service.*; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.CommandLineRunner; import org.springframework.core.io.ClassPathResource; import org.springframework.jdbc.core.JdbcTemplate; @@ -15,6 +15,7 @@ import java.nio.charset.StandardCharsets; // SB 실행 시, attraction data 저장(area 데이터가 먼저 저장되어 있어야 함) +@Slf4j @Component @RequiredArgsConstructor public class DataInit implements CommandLineRunner { @@ -41,10 +42,10 @@ public void run(String... args) throws Exception { // SQL 실행 jdbcTemplate.execute(sql); - System.out.println("data.sql 실행 완료"); + log.info("data.sql 실행 완료"); } catch (IOException e) { - e.printStackTrace(); + log.error("area 데이터 초기화 시 오류 발생 : {}",e.getMessage()); } // 2. 장소 데이터 삽입 @@ -53,20 +54,20 @@ public void run(String... args) throws Exception { culturalEventService.fetchAndSaveAllEvents(); if (restaurantRepository.count() == 0) { - System.out.println("음식점 데이터 저장 시작"); + log.info("음식점 데이터 저장 시작"); restaurantService.fetchAndSaveRestaurants(); - System.out.println("음식점 데이터 저장 완료"); + log.info("음식점 데이터 저장 완료"); } else { - System.out.println("음식점 데이터가 이미 존재합니다. 건너뜁니다."); + log.info("음식점 데이터가 이미 존재합니다. 건너뜁니다."); } // 카페 데이터 초기화 if (cafeRepository.count() == 0) { - System.out.println("카페 데이터 저장 시작"); + log.info("카페 데이터 저장 시작"); cafeService.processAllAreas(); - System.out.println("카페 데이터 저장 완료"); + log.info("카페 데이터 저장 완료"); } else { - System.out.println("카페 데이터가 이미 존재합니다. 건너뜁니다."); + log.info("카페 데이터가 이미 존재합니다. 건너뜁니다."); } } diff --git a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java index 90cf5bc..572a7fe 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AccommodationService.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.github.cdimascio.dotenv.Dotenv; import jakarta.transaction.Transactional; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -28,6 +29,7 @@ import java.util.List; import java.util.Map; +@Slf4j @Service public class AccommodationService { private final AreaRepository areaRepository; @@ -75,12 +77,12 @@ public String searchAccommodations(BigDecimal x, BigDecimal y) { return response.getBody(); // 성공 시 응답 본문(JSON 문자열) 반환 [1] } else { // API 호출은 성공했으나, 응답 코드가 200 OK가 아닌 경우 - System.out.println("Error calling Kakao API. Status code: " + response.getStatusCode() + " Response: " + response.getBody()); + log.error("Error calling Kakao API. Status code: {} Response: {}", response.getStatusCode(), response.getBody()); return null; // 실패 시 null 반환 [1] } } catch (RestClientException e) { // RestTemplate 사용 중 네트워크 오류 등 예외 발생 시 - System.out.println("Exception during Kakao Api call to " + uri + ": " + e.getMessage() + e); + log.error("Exception during Kakao Api call to {} : {}", uri, e.getMessage()); return null; // 예외 발생 시 null 반환 } } @@ -109,18 +111,19 @@ public void saveAccommodations() throws JsonProcessingException { Accommodation accommodation = mapDocumentToAccommodation(doc, area); // 매핑 함수 사용 accommodationsToSave.add(accommodation); - } else { - System.out.println("새롭게 저장할 숙박업소가 없습니다."); } +// else { +// log.info("새롭게 저장할 숙박업소가 없습니다."); +// } } } } catch (JsonProcessingException e) { - System.out.println("Json 파싱 실패"); + log.error("Json 파싱 실패 : {}", e.getMessage()); } catch (NumberFormatException e) { - System.out.println("숙박업소 Id 오류"); + log.error("숙박업소 Id 오류 : {}", e.getMessage()); } } else { - System.out.println("유효한 응답이 없습니다."); + log.error("유효한 응답이 없습니다."); } // try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } @@ -128,9 +131,9 @@ public void saveAccommodations() throws JsonProcessingException { // 5. 수집된 숙소 정보 일괄 저장 if (!accommodationsToSave.isEmpty()) { accommodationRepository.saveAll(accommodationsToSave); // saveAll로 성능 향상 - System.out.println("숙박업소 저장 성공"); + log.info("숙박업소 저장 성공"); } else { - System.out.println("신규 저장 숙박업소가 없습니다."); + log.info("신규 저장 숙박업소가 없습니다."); } } @@ -148,7 +151,7 @@ private Accommodation mapDocumentToAccommodation(KakaoApiResponse.Document doc, accommodation.setLat(new BigDecimal(doc.getY())); // String 좌표를 BigDecimal로 변환 accommodation.setLon(new BigDecimal(doc.getX())); // String 좌표를 BigDecimal로 변환 } catch (NumberFormatException e) { - System.out.println("좌표 설정 실패"); + log.warn("좌표 설정 실패 : {}", e.getMessage()); accommodation.setLat(null); accommodation.setLon(null); } diff --git a/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java b/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java index 7313664..ed605cf 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java @@ -8,6 +8,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -32,7 +33,7 @@ public AiSummaryParsedResponse getSummaryParsed(String targetType, String target .bodyToMono(AiSummaryResponse.class) .block(); - return parseSummaryContent(raw); + return parseSummaryContent(Objects.requireNonNull(raw)); } private AiSummaryParsedResponse parseSummaryContent(AiSummaryResponse raw) { diff --git a/place-service/src/main/java/com/example/placeservice/service/AreaService.java b/place-service/src/main/java/com/example/placeservice/service/AreaService.java index 22d92a8..70a134c 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AreaService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AreaService.java @@ -4,12 +4,14 @@ import com.example.placeservice.entity.Area; import com.example.placeservice.repository.AreaRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.stream.Collectors; +@Slf4j @Service @RequiredArgsConstructor @Transactional @@ -25,6 +27,7 @@ public List getAreaData() { .map(AreaDto::new) .collect(Collectors.toList()); } catch (Exception e) { + log.error(e.getMessage()); throw new RuntimeException("예상치 못한 오류",e); } } diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionKakaoMapClient.java b/place-service/src/main/java/com/example/placeservice/service/AttractionKakaoMapClient.java index 69e33bc..64ec8e3 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionKakaoMapClient.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionKakaoMapClient.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -11,7 +12,7 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; - +@Slf4j @Service @RequiredArgsConstructor public class AttractionKakaoMapClient { @@ -22,31 +23,36 @@ public class AttractionKakaoMapClient { public JsonNode searchAttractionByKeyword(String keyword) { - RestTemplate restTemplate = new RestTemplate(); - - HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", "KakaoAK " + kakaoApiKey); - - // Kakao API 호출 URL 세팅 - String builder = UriComponentsBuilder - .fromHttpUrl("https://dapi.kakao.com/v2/local/search/keyword.json") - .queryParam("query",keyword) - .queryParam("sort", "accuracy") // 정확도순 정렬 - .queryParam("page",1) // 요청 페이지 - .queryParam("size", 15) - .build(false).toString(); // 한 페이지에 최대 15개 결과 - - HttpEntity entity = new HttpEntity<>(headers); - - // REST API 호출 - ResponseEntity response = restTemplate.exchange( - builder.toString(), - HttpMethod.GET, - entity, - JsonNode.class - ); - - return response.getBody(); // JSON 응답 반환 + try { + RestTemplate restTemplate = new RestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "KakaoAK " + kakaoApiKey); + + // Kakao API 호출 URL 세팅 + String builder = UriComponentsBuilder + .fromHttpUrl("https://dapi.kakao.com/v2/local/search/keyword.json") + .queryParam("query", keyword) + .queryParam("sort", "accuracy") // 정확도순 정렬 + .queryParam("page", 1) // 요청 페이지 + .queryParam("size", 15) + .build(false).toString(); // 한 페이지에 최대 15개 결과 + + HttpEntity entity = new HttpEntity<>(headers); + + // REST API 호출 + ResponseEntity response = restTemplate.exchange( + builder.toString(), + HttpMethod.GET, + entity, + JsonNode.class + ); + + return response.getBody(); // JSON 응답 반환 + } catch (Exception e) { + log.error("관광지 카카오맵 url 조회 중 오류 발생 : {}",e.getMessage()); + return null; + } } } diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index 155c8db..a32387d 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -12,6 +12,7 @@ import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.Unmarshaller; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @@ -26,6 +27,7 @@ import static com.example.placeservice.util.GeoUtils.calculateDistanceKm; +@Slf4j @Service @RequiredArgsConstructor public class AttractionService { @@ -35,14 +37,16 @@ public class AttractionService { private final AttractionKakaoMapClient attractionKakaoMapClient; // visitSeoul 관광지 데이터 로드 후 attraction 테이블 저장 - public List fetchDataFromVisitSeoul() { + public void fetchDataFromVisitSeoul() { String url="https://www.visitseoul.net/file_save/OPENAPI/OPEN_API_kr.xml"; - List areaList = areaRepository.findAll(); // 여기서 불러와 + List areaList = areaRepository.findAll(); + + log.info("관광지 데이터 저장 시작"); try { // 1. XML 받아오기 ResponseEntity response = restTemplate.getForEntity(url, String.class); - String xml = new String(response.getBody().getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); + String xml = new String(Objects.requireNonNull(response.getBody()).getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); xml = xml.trim().replace("\uFEFF", ""); // 2. JAXB로 XML → DTO 변환 @@ -89,20 +93,20 @@ public List fetchDataFromVisitSeoul() { break; } } - }else{System.out.println("카카오 관광지 검색 결과가 없습니다");} - }else{System.out.println(table.getTitle());} - - + }else{ + log.debug("카카오 관광지 검색 결과가 없습니다 : {}", title);} + } return attraction; }) .filter(Objects::nonNull) // null 필터링 .toList(); + log.info("관광지 데이터 저장 완료 : {}",entities.size()); // 4. DB에 저장 - return attractionRepository.saveAll(entities); + attractionRepository.saveAll(entities); } catch (Exception e) { - e.printStackTrace(); // 로깅 처리 추천 + log.error("관광지 데이터 저장 중 오류 발생 : {}",e.getMessage()); throw new RuntimeException("외부 XML 데이터 파싱 실패", e); } } @@ -127,7 +131,7 @@ private Area findAreaByCondition(AttractionDto.AttractionTable table, List } } if (minDistance > 2.0) { // 2. 거리가 2km를 넘어가면 제외한다. - System.out.println(table.getTitle() + " - 가까운 지역 없음 (거리: " + minDistance + "km)"); + log.debug("{} - 가까운 지역 없음 (거리: {}km)", table.getTitle(), minDistance); return null; } return closestArea; @@ -161,6 +165,7 @@ public List getAttractionData() { .toList(); } catch (RuntimeException e) { + log.error("관광지 목록 조회 오류 발생 : {}", e.getMessage()); throw new RuntimeException("예상치 못한 오류",e); } } @@ -192,6 +197,7 @@ public AttractionInfoDto getAttractionInfoData(long attractionId) { ); } catch (RuntimeException e) { + log.error("관광지 정보 조회 오류 발생 : {}", e.getMessage()); throw new RuntimeException("예상치 못한 오류",e); } } diff --git a/place-service/src/main/java/com/example/placeservice/service/CafeService.java b/place-service/src/main/java/com/example/placeservice/service/CafeService.java index a01c08e..9f3652c 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CafeService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CafeService.java @@ -1,6 +1,5 @@ package com.example.placeservice.service; -import com.example.placeservice.dto.cafe.CafeDto; import com.example.placeservice.dto.cafe.CafeListDto; import com.example.placeservice.entity.Area; import com.example.placeservice.entity.Cafe; @@ -100,7 +99,6 @@ public void processAllAreas() { try { List areas = areaRepository.findAll(); - log.info("총 {}개 장소에 대한 카페 정보를 처리합니다.", areas.size()); // 다른 처리 중 중복 실행 방지 processingCompleted = true; @@ -122,14 +120,12 @@ public void processAllAreas() { } } - log.info("모든 장소 처리 완료. 총 처리된 지역: {}", areas.size()); - // 최종 카페 수 확인 log.info("최종 저장된 카페 수: {}", cafeRepository.count()); } catch (Exception e) { processingCompleted = false; // 오류 발생 시 플래그 재설정 - log.error("카페 데이터 처리 중 오류 발생: {}", e.getMessage(), e); + log.error("카페 데이터 처리 중 오류 발생: {}", e.getMessage()); throw e; } } @@ -139,7 +135,7 @@ public void processAllAreas() { */ @Transactional public void processSingleArea(Area area) { - log.info("processSingleArea() 시작 - 지역: '{}' (ID: {})", area.getName(), area.getAreaId()); +// log.info("processSingleArea() 시작 - 지역: '{}' (ID: {})", area.getName(), area.getAreaId()); try { // 해당 지역의 기존 카페 데이터 삭제 @@ -157,7 +153,7 @@ public void processSingleArea(Area area) { return; } - log.info("카카오 API 호출 - 위도: {}, 경도: {}, 반경: 1000m", latitude, longitude); +// log.info("카카오 API 호출 - 위도: {}, 경도: {}, 반경: 1000m", latitude, longitude); List cafes = kakaoApiService.findCafesNearby(latitude, longitude, 1000); log.info("카카오 API 응답 - 총 {}개의 장소 정보 수신", cafes.size()); @@ -165,23 +161,21 @@ public void processSingleArea(Area area) { // "음식점 > 카페"가 포함된 카페만 필터링하고 "(휴업중)" 카페는 제외 List filteredCafes = cafes.stream() .filter(cafe -> { - boolean categoryMatch = cafe.getCategoryName() != null && +// if (!categoryMatch) { +// log.debug("카테고리 불일치로 제외: {}, 카테고리: {}", +// cafe.getPlaceName(), cafe.getCategoryName());; +// } + return cafe.getCategoryName() != null && cafe.getCategoryName().contains("음식점 > 카페"); - if (!categoryMatch) { - log.debug("카테고리 불일치로 제외: {}, 카테고리: {}", - cafe.getPlaceName(), cafe.getCategoryName()); - } - return categoryMatch; }) .filter(cafe -> { - boolean notClosed = cafe.getPlaceName() == null || +// if (!notClosed) { +// log.debug("휴업중으로 제외: {}", cafe.getPlaceName()); +// } + return cafe.getPlaceName() == null || !cafe.getPlaceName().contains("(휴업중)"); - if (!notClosed) { - log.debug("휴업중으로 제외: {}", cafe.getPlaceName()); - } - return notClosed; }) - .collect(Collectors.toList()); + .toList(); log.info("'{}' 주변 총 {}개의 카페 중 {}개의 '음식점 > 카페' 카테고리(휴업중 제외)를 찾았습니다.", area.getName(), cafes.size(), filteredCafes.size()); @@ -222,7 +216,7 @@ public void processSingleArea(Area area) { .area(area) .build(); - log.info("카페 객체 생성 완료: {}", cafe.getName()); +// log.info("카페 객체 생성 완료: {}", cafe.getName()); cafesToSave.add(cafe); // 리스트에 추가 } catch (Exception e) { log.error("카페 저장 중 오류 발생: {}, 오류: {}", cafeDoc.getPlaceName(), e.getMessage(), e); @@ -245,91 +239,91 @@ public void processSingleArea(Area area) { /** * 특정 지역의 카페 목록 조회 */ - @Transactional(readOnly = true) - public List getCafesByAreaId(Long areaId) { - log.info("지역 ID {}의 카페 목록 조회", areaId); - List cafes = cafeRepository.findByAreaId(areaId); - log.info("지역 ID {}에서 총 {}개의 카페 찾음", areaId, cafes.size()); - - return cafes.stream() - .map(this::convertToDto) - .collect(Collectors.toList()); - } +// @Transactional(readOnly = true) +// public List getCafesByAreaId(Long areaId) { +// log.info("지역 ID {}의 카페 목록 조회", areaId); +// List cafes = cafeRepository.findByAreaId(areaId); +// log.info("지역 ID {}에서 총 {}개의 카페 찾음", areaId, cafes.size()); +// +// return cafes.stream() +// .map(this::convertToDto) +// .collect(Collectors.toList()); +// } /** * 모든 카페 목록 조회 */ - @Transactional(readOnly = true) - public List getAllCafes() { - log.info("모든 카페 목록 조회"); - List cafes = cafeRepository.findAll(); - log.info("총 {}개의 카페 조회됨", cafes.size()); - - return cafes.stream() - .map(this::convertToDto) - .collect(Collectors.toList()); - } +// @Transactional(readOnly = true) +// public List getAllCafes() { +// log.info("모든 카페 목록 조회"); +// List cafes = cafeRepository.findAll(); +// log.info("총 {}개의 카페 조회됨", cafes.size()); +// +// return cafes.stream() +// .map(this::convertToDto) +// .collect(Collectors.toList()); +// } /** * Cafe 엔티티를 DTO로 변환 */ - private CafeDto convertToDto(Cafe cafe) { - return CafeDto.builder() - .id(cafe.getId()) - .name(cafe.getName()) - .address(cafe.getAddress()) - .lat(cafe.getLat()) - .lon(cafe.getLon()) - .phone(cafe.getPhone()) - .kakaomapUrl(cafe.getKakaomapUrl()) - .categoryCode(cafe.getCategoryCode()) - .build(); - } +// private CafeDto convertToDto(Cafe cafe) { +// return CafeDto.builder() +// .id(cafe.getId()) +// .name(cafe.getName()) +// .address(cafe.getAddress()) +// .lat(cafe.getLat()) +// .lon(cafe.getLon()) +// .phone(cafe.getPhone()) +// .kakaomapUrl(cafe.getKakaomapUrl()) +// .categoryCode(cafe.getCategoryCode()) +// .build(); +// } /** * 카카오맵 URL로 카페 상세 정보 조회 */ - @Transactional(readOnly = true) - public Map getCafeDetailByPlaceCode(String placeCode) { - log.info("장소 코드로 카페 상세 정보 조회: {}", placeCode); - Optional cafeOptional = cafeRepository.findByKakaomapUrl(placeCode); - - if (cafeOptional.isPresent()) { - Cafe cafe = cafeOptional.get(); - log.info("카페 찾음: {}, ID: {}", cafe.getName(), cafe.getId()); - - Map cafeInfo = new HashMap<>(); - cafeInfo.put("name", cafe.getName()); - cafeInfo.put("address", cafe.getAddress()); - cafeInfo.put("phone", cafe.getPhone()); - cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl()); - cafeInfo.put("lat", cafe.getLat()); - cafeInfo.put("lon", cafe.getLon()); - cafeInfo.put("category_code", cafe.getCategoryCode()); - - return cafeInfo; - } - - log.info("장소 코드 {}에 해당하는 카페를 찾을 수 없음", placeCode); - return null; - } +// @Transactional(readOnly = true) +// public Map getCafeDetailByPlaceCode(String placeCode) { +// log.info("장소 코드로 카페 상세 정보 조회: {}", placeCode); +// Optional cafeOptional = cafeRepository.findByKakaomapUrl(placeCode); +// +// if (cafeOptional.isPresent()) { +// Cafe cafe = cafeOptional.get(); +// log.info("카페 찾음: {}, ID: {}", cafe.getName(), cafe.getId()); +// +// Map cafeInfo = new HashMap<>(); +// cafeInfo.put("name", cafe.getName()); +// cafeInfo.put("address", cafe.getAddress()); +// cafeInfo.put("phone", cafe.getPhone()); +// cafeInfo.put("kakaomap_url", cafe.getKakaomapUrl()); +// cafeInfo.put("lat", cafe.getLat()); +// cafeInfo.put("lon", cafe.getLon()); +// cafeInfo.put("category_code", cafe.getCategoryCode()); +// +// return cafeInfo; +// } +// +// log.info("장소 코드 {}에 해당하는 카페를 찾을 수 없음", placeCode); +// return null; +// } /** * 지역 ID로 카페 목록 조회 (응답 형식 맞춤) */ - @Transactional(readOnly = true) - public Map getCafesByAreaIdFormatted(Long areaId) { - log.info("지역 ID {}의 카페 목록 조회 (응답 형식 맞춤)", areaId); - - List cafes = cafeRepository.findByAreaId(areaId); - List cafeDtos = cafes.stream() - .map(this::convertToDto) - .collect(Collectors.toList()); - - Map response = new HashMap<>(); - response.put("type", "cafe"); - response.put("content", cafeDtos); - - return response; - } +// @Transactional(readOnly = true) +// public Map getCafesByAreaIdFormatted(Long areaId) { +// log.info("지역 ID {}의 카페 목록 조회 (응답 형식 맞춤)", areaId); +// +// List cafes = cafeRepository.findByAreaId(areaId); +// List cafeDtos = cafes.stream() +// .map(this::convertToDto) +// .collect(Collectors.toList()); +// +// Map response = new HashMap<>(); +// response.put("type", "cafe"); +// response.put("content", cafeDtos); +// +// return response; +// } } \ No newline at end of file diff --git a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java index 41ec59e..be74042 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java @@ -7,6 +7,7 @@ import com.example.placeservice.repository.CulturalEventRepository; import com.example.placeservice.util.GeoUtils; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; @@ -15,26 +16,28 @@ import java.util.ArrayList; import java.util.List; +@Slf4j @Service @RequiredArgsConstructor public class CulturalEventService { private final EventParserService eventParserService; private final CulturalEventRepository culturalEventRepository; - private final AreaRepository areaRepository; // 🔹 추가 + private final AreaRepository areaRepository; private final RestTemplate restTemplate; @Transactional public void fetchAndSaveAllEvents() { + log.info("행사 데이터 저장 시작"); + int[] ranges = {1, 1001, 2001, 3001, 4001, 5001}; - for (int i = 0; i < ranges.length; i++) { - int startIndex = ranges[i]; + for (int startIndex : ranges) { int endIndex = startIndex + 999; String jsonData = fetchEventData(startIndex, endIndex); List eventItems = eventParserService.parse(jsonData); - + // 행사 종료일이 오늘 이후 또는 오늘인 것만 추가 List filteredList = new ArrayList<>(); for (CulturalEventItem item : eventItems) { @@ -50,6 +53,7 @@ public void fetchAndSaveAllEvents() { saveEvents(filteredList); } } + log.info("행사 데이터 저장 완료"); } private void saveEvents(List eventItems) { @@ -99,7 +103,12 @@ private Area findNearestArea(double lat, double lon, List areas) { } private String fetchEventData(int startIndex, int endIndex) { - String url = String.format("http://openapi.seoul.go.kr:8088/7669764c417069613736734567476c/json/culturalEventInfo/%d/%d/", startIndex, endIndex); - return restTemplate.getForObject(url, String.class); + try { + String url = String.format("http://openapi.seoul.go.kr:8088/7669764c417069613736734567476c/json/culturalEventInfo/%d/%d/", startIndex, endIndex); + return restTemplate.getForObject(url, String.class); + }catch (Exception e){ + log.error("행사데이터 API 조회 중 오류 발생 :{}",e.getMessage()); + } + return null; } } diff --git a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java index b760a7c..f91931e 100644 --- a/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java +++ b/place-service/src/main/java/com/example/placeservice/service/RestaurantService.java @@ -6,6 +6,7 @@ import com.example.placeservice.repository.RestaurantRepository; import com.fasterxml.jackson.databind.JsonNode; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,6 +14,7 @@ import java.util.ArrayList; import java.util.List; +@Slf4j @Service @RequiredArgsConstructor public class RestaurantService { @@ -22,7 +24,7 @@ public class RestaurantService { private final KakaoMapClient kakaoMapClient; // 특정 Area 기준으로 주변 음식점 저장 - public List saveRestaurantsNearbyArea(Area area) { + public void saveRestaurantsNearbyArea(Area area) { List savedRestaurants = new ArrayList<>(); int page = 1; boolean isEnd = false; @@ -76,10 +78,10 @@ public List saveRestaurantsNearbyArea(Area area) { page++; } } else { + log.info("레스토랑 카카오 데이터 조회 실패"); isEnd = true; } } - return savedRestaurants; } // 전체 Area 대상 음식점 저장 From 748621c81886ee90d256fed22f16e68395a70117 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Mon, 26 May 2025 14:35:24 +0900 Subject: [PATCH 347/361] =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?(=EC=97=AC=ED=96=89=EC=A7=80=20=EC=B6=94=EC=B2=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/userservice/controller/SuggestController.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java index 13426c0..996302f 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java @@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; @@ -125,6 +126,8 @@ public ResponseEntity getPrevChatsForPlan(@PathVariable String user_id) { return ResponseEntity.ok(responseArray); } catch (HttpClientErrorException | HttpServerErrorException e) { return ResponseEntity.status(e.getStatusCode()).body("FastAPI 호출 중 오류 발생: " + e.getMessage()); + } catch (HttpMessageNotReadableException e) { + return ResponseEntity.ok("[]"); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("내부 서버 오류가 발생했습니다: " + e.getMessage()); } From 9cbf0169bc02e22557f1a200d3f8bc5df530802c Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Mon, 26 May 2025 14:57:32 +0900 Subject: [PATCH 348/361] =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?(=EC=97=AC=ED=96=89=EC=A7=80=20=EC=B6=94=EC=B2=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0)=202?= =?UTF-8?q?=EC=B0=A8=20=EC=8B=9C=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SuggestController.java | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java index 996302f..939dbd0 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java @@ -6,16 +6,16 @@ import com.example.userservice.entity.Member; import com.example.userservice.service.MemberService; import lombok.extern.slf4j.Slf4j; +import org.json.JSONException; +import org.json.JSONObject; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.ErrorResponse; import org.springframework.web.bind.annotation.*; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.HttpServerErrorException; -import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.*; import java.util.List; @@ -124,10 +124,32 @@ public ResponseEntity getPrevChatsForPlan(@PathVariable String user_id) { } return ResponseEntity.ok(responseArray); - } catch (HttpClientErrorException | HttpServerErrorException e) { - return ResponseEntity.status(e.getStatusCode()).body("FastAPI 호출 중 오류 발생: " + e.getMessage()); + } catch (HttpStatusCodeException e) { + String errorResponseBody = e.getResponseBodyAsString(); + try { + // 오류 응답 본문을 JSONObject로 파싱 + JSONObject errorJson = new JSONObject(errorResponseBody); + + // "message" 키가 존재하고, 그 값이 "data not found"인지 확인 (대소문자 무시) + if (errorJson.has("message") && "data not found".equalsIgnoreCase(errorJson.getString("message"))) { + // 만약 정확히 {"message": "data not found"} 만 있는지 확인하려면 + // errorJson.length() == 1 조건도 추가할 수 있습니다. + return ResponseEntity.ok("[]"); // 또는 원하는 특정 상태 코드 (예: HttpStatus.NOT_FOUND) + } + else { + // 다른 JSON 오류 응답 + return ResponseEntity.status(e.getStatusCode()) + .body("FastAPI 호출 중 HTTP 오류 발생 (JSON): " + errorResponseBody); + } + } catch (JSONException jsonEx) { + // 오류 응답 본문이 유효한 JSON 형식이 아닌 경우 (예: HTML 오류 페이지, 일반 텍스트) + return ResponseEntity.status(e.getStatusCode()) + .body("FastAPI 호출 중 HTTP 오류 발생 (비 JSON 응답): " + errorResponseBody); + } } catch (HttpMessageNotReadableException e) { - return ResponseEntity.ok("[]"); + // FastAPI가 2xx 성공 응답을 보냈으나, 본문이 SuggestFastResponseDto와 다른 경우 (역직렬화 실패) + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body("FastAPI 응답 형식이 예상과 다릅니다 (파싱 오류): " + e.getMessage()); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("내부 서버 오류가 발생했습니다: " + e.getMessage()); } From ba93cca3f0aae704236f68fe53c3dae46e018e55 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Mon, 26 May 2025 15:01:53 +0900 Subject: [PATCH 349/361] =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?(=EC=97=AC=ED=96=89=EC=A7=80=20=EC=B6=94=EC=B2=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0)=203?= =?UTF-8?q?=EC=B0=A8=20=EC=8B=9C=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user-service/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/user-service/build.gradle b/user-service/build.gradle index 7bf2b33..321be19 100644 --- a/user-service/build.gradle +++ b/user-service/build.gradle @@ -60,6 +60,8 @@ dependencies { implementation 'io.github.cdimascio:java-dotenv:5.2.2' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + implementation 'org.json:json:20231013' + // DB runtimeOnly 'org.postgresql:postgresql' From b940630436c6b6522f7e4514cf1d611c4b6e7906 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Mon, 26 May 2025 15:15:12 +0900 Subject: [PATCH 350/361] =?UTF-8?q?Refactor=20:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC(=EC=97=AC=ED=96=89=EC=A7=80=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=EA=B8=B0=EB=A1=9D=20=EC=97=86=EB=8A=94=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0)=203=EC=B0=A8=20=EC=8B=9C=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SuggestController.java | 62 +++++++++---------- .../example/userservice/dto/SuggestError.java | 8 +++ 2 files changed, 36 insertions(+), 34 deletions(-) create mode 100644 user-service/src/main/java/com/example/userservice/dto/SuggestError.java diff --git a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java index 939dbd0..3776caf 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java @@ -1,13 +1,15 @@ package com.example.userservice.controller; +import com.example.userservice.dto.SuggestError; import com.example.userservice.dto.SuggestFastResponseDto; import com.example.userservice.dto.SuggestRequestDto; import com.example.userservice.dto.SuggestFastRequestDto; import com.example.userservice.entity.Member; import com.example.userservice.service.MemberService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; -import org.json.JSONException; -import org.json.JSONObject; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -15,7 +17,10 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.ErrorResponse; import org.springframework.web.bind.annotation.*; -import org.springframework.web.client.*; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; import java.util.List; @@ -23,15 +28,17 @@ @RequestMapping("/suggest") @Slf4j public class SuggestController { + private final ObjectMapper objectMapper; @Value("${fastapi-svc}") private String fastApiUrl; private final MemberService memberService; private final RestTemplate restTemplate; - public SuggestController(MemberService memberService) { + public SuggestController(MemberService memberService, ObjectMapper objectMapper) { this.memberService = memberService; restTemplate = new RestTemplate(); + this.objectMapper = objectMapper; } @PostMapping("/{user_id}") @@ -113,10 +120,14 @@ public ResponseEntity getPrevChatsForPlan(@PathVariable String user_id) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body("해당 ID의 회원을 찾을 수 없습니다: " + user_id); } - try { - ResponseEntity responseEntity = restTemplate.getForEntity(fastApiUrl + "/suggest/" + user_id, SuggestFastResponseDto[].class); - SuggestFastResponseDto[] responseArray = responseEntity.getBody(); + ResponseEntity response = restTemplate.getForEntity(fastApiUrl + "/suggest/" + user_id, String.class); + + try { +// ResponseEntity responseEntity = restTemplate.getForEntity(fastApiUrl + "/suggest/" + user_id, SuggestFastResponseDto[].class); +// SuggestFastResponseDto[] responseArray = responseEntity.getBody(); + SuggestFastResponseDto[] responseArray = objectMapper.readValue(response.getBody(), SuggestFastResponseDto[].class); + if (responseArray == null) { // FastAPI 응답 본문이 null인 경우 (예: FastAPI가 204 No Content를 반환했거나, 문제가 있는 경우) // 빈 배열 또는 적절한 오류 응답을 반환할 수 있습니다. @@ -124,34 +135,17 @@ public ResponseEntity getPrevChatsForPlan(@PathVariable String user_id) { } return ResponseEntity.ok(responseArray); - } catch (HttpStatusCodeException e) { - String errorResponseBody = e.getResponseBodyAsString(); - try { - // 오류 응답 본문을 JSONObject로 파싱 - JSONObject errorJson = new JSONObject(errorResponseBody); - - // "message" 키가 존재하고, 그 값이 "data not found"인지 확인 (대소문자 무시) - if (errorJson.has("message") && "data not found".equalsIgnoreCase(errorJson.getString("message"))) { - // 만약 정확히 {"message": "data not found"} 만 있는지 확인하려면 - // errorJson.length() == 1 조건도 추가할 수 있습니다. - return ResponseEntity.ok("[]"); // 또는 원하는 특정 상태 코드 (예: HttpStatus.NOT_FOUND) - } - else { - // 다른 JSON 오류 응답 - return ResponseEntity.status(e.getStatusCode()) - .body("FastAPI 호출 중 HTTP 오류 발생 (JSON): " + errorResponseBody); - } - } catch (JSONException jsonEx) { - // 오류 응답 본문이 유효한 JSON 형식이 아닌 경우 (예: HTML 오류 페이지, 일반 텍스트) - return ResponseEntity.status(e.getStatusCode()) - .body("FastAPI 호출 중 HTTP 오류 발생 (비 JSON 응답): " + errorResponseBody); - } - } catch (HttpMessageNotReadableException e) { - // FastAPI가 2xx 성공 응답을 보냈으나, 본문이 SuggestFastResponseDto와 다른 경우 (역직렬화 실패) - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body("FastAPI 응답 형식이 예상과 다릅니다 (파싱 오류): " + e.getMessage()); + } catch (HttpClientErrorException | HttpServerErrorException e) { + return ResponseEntity.status(e.getStatusCode()).body("FastAPI 호출 중 오류 발생: " + e.getMessage()); } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("내부 서버 오류가 발생했습니다: " + e.getMessage()); + try{ + SuggestError errorResponse = objectMapper.readValue(response.getBody(), SuggestError.class); + log.warn("응답 메시지: {}", errorResponse.getMessage()); + return ResponseEntity.ok(errorResponse.getMessage()); + } catch (JsonProcessingException ex) { +// throw new RuntimeException(ex); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("내부 서버 오류가 발생했습니다: " + e.getMessage()); + } } } } diff --git a/user-service/src/main/java/com/example/userservice/dto/SuggestError.java b/user-service/src/main/java/com/example/userservice/dto/SuggestError.java new file mode 100644 index 0000000..9c5a656 --- /dev/null +++ b/user-service/src/main/java/com/example/userservice/dto/SuggestError.java @@ -0,0 +1,8 @@ +package com.example.userservice.dto; + +import lombok.Data; + +@Data +public class SuggestError { + private String message; +} From 8df73df6fcb6232e6ba0cf8a64b2853fae043cbf Mon Sep 17 00:00:00 2001 From: ha-yun Date: Mon, 26 May 2025 15:21:50 +0900 Subject: [PATCH 351/361] =?UTF-8?q?Fix:=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC(=EC=97=AC=ED=96=89=EC=A7=80=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=20=EC=97=86=EB=8A=94=20=EA=B2=BD=EC=9A=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/userservice/controller/SuggestController.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java index 3776caf..f10fe21 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java @@ -141,7 +141,12 @@ public ResponseEntity getPrevChatsForPlan(@PathVariable String user_id) { try{ SuggestError errorResponse = objectMapper.readValue(response.getBody(), SuggestError.class); log.warn("응답 메시지: {}", errorResponse.getMessage()); - return ResponseEntity.ok(errorResponse.getMessage()); + if(errorResponse.getMessage().contains("No chat history found for this user")){ + return ResponseEntity.ok("[]"); + }else{ + return ResponseEntity.ok(errorResponse.getMessage()); + } + } catch (JsonProcessingException ex) { // throw new RuntimeException(ex); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("내부 서버 오류가 발생했습니다: " + e.getMessage()); From fc91acf0f7ac1e6d5970fb85974fc09e030d8577 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Tue, 27 May 2025 11:20:52 +0900 Subject: [PATCH 352/361] =?UTF-8?q?Refactor:=20gateway=20cors=20https=20ur?= =?UTF-8?q?l=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway/src/main/java/com/example/gateway/config/Cors.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gateway/src/main/java/com/example/gateway/config/Cors.java b/gateway/src/main/java/com/example/gateway/config/Cors.java index b9b41e4..7564b9d 100644 --- a/gateway/src/main/java/com/example/gateway/config/Cors.java +++ b/gateway/src/main/java/com/example/gateway/config/Cors.java @@ -16,6 +16,8 @@ public CorsWebFilter corsWebFilter() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); // 인증 정보 포함 X config.setAllowedOrigins(List.of( + "https://localhost:5173", + "https://www.seoultravel.life", "http://localhost:5173", "http://192.168.0.186:5173", "http://58.127.241.84:5173", From e6109e58397b8ada2c2fbfd69516af5fad780965 Mon Sep 17 00:00:00 2001 From: suminmmmmmmm Date: Wed, 28 May 2025 09:49:12 +0900 Subject: [PATCH 353/361] =?UTF-8?q?=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/userservice/repository/FavoriteRepository.java | 2 ++ .../com/example/userservice/service/MemberService.java | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java b/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java index 6fbcaf3..06ca7b0 100644 --- a/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java +++ b/user-service/src/main/java/com/example/userservice/repository/FavoriteRepository.java @@ -14,4 +14,6 @@ public interface FavoriteRepository extends JpaRepository { Optional findByMemberAndTypeAndPlaceId(Member member, String type, String id); + void deleteAllByMember(Member member); + } diff --git a/user-service/src/main/java/com/example/userservice/service/MemberService.java b/user-service/src/main/java/com/example/userservice/service/MemberService.java index 89f2083..e1a3608 100644 --- a/user-service/src/main/java/com/example/userservice/service/MemberService.java +++ b/user-service/src/main/java/com/example/userservice/service/MemberService.java @@ -3,6 +3,7 @@ import com.example.userservice.dto.MemberDto; import com.example.userservice.dto.MemberSign; import com.example.userservice.entity.Member; +import com.example.userservice.repository.FavoriteRepository; import com.example.userservice.repository.jpa.MemberRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; @@ -18,6 +19,7 @@ public class MemberService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; + private final FavoriteRepository favoriteRepository; // 일반 사용자 회원가입 (기본 권한은 ROLE_USER) public Member registerUser(MemberSign dto) { @@ -143,12 +145,17 @@ public Member findByUserId(String userId) { public void deleteMemberById(Long memberId) { Member member = memberRepository.findById(memberId) .orElseThrow(() -> new IllegalArgumentException("회원이 존재하지 않습니다.")); + + //즐겨찾기 먼저 삭제 + favoriteRepository.deleteAllByMember(member); + // 회원 삭제 memberRepository.delete(member); } + /** * 모든 회원 목록 조회 * @return 모든 회원 정보 목록 From 26e95aec8aba3615330911733e4f8f02beca746d Mon Sep 17 00:00:00 2001 From: ha-yun Date: Wed, 28 May 2025 10:12:29 +0900 Subject: [PATCH 354/361] =?UTF-8?q?Refactor:=20=EC=A6=90=EA=B2=A8=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=EC=A1=B0=ED=9A=8C=20service=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/userservice/dto/FavoriteDto.java | 3 +- .../userservice/service/FavoriteService.java | 133 +++++------------- 2 files changed, 40 insertions(+), 96 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java b/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java index 91e57a9..fd469c2 100644 --- a/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java +++ b/user-service/src/main/java/com/example/userservice/dto/FavoriteDto.java @@ -1,7 +1,5 @@ package com.example.userservice.dto; -import com.example.userservice.entity.Favorite; -import jakarta.persistence.Column; import lombok.Data; @Data @@ -22,4 +20,5 @@ public FavoriteDto(Long favorite_id ,String type, String placeId, String userId, this.address = address; } + } diff --git a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java index 76ab9ab..b415d49 100644 --- a/user-service/src/main/java/com/example/userservice/service/FavoriteService.java +++ b/user-service/src/main/java/com/example/userservice/service/FavoriteService.java @@ -3,26 +3,18 @@ import com.example.userservice.dto.FavoriteDto; import com.example.userservice.entity.Favorite; import com.example.userservice.entity.Member; -import com.example.userservice.entity.place.Accommodation; -import com.example.userservice.entity.place.Attraction; -import com.example.userservice.entity.place.Cafe; -import com.example.userservice.entity.place.Restaurant; import com.example.userservice.repository.FavoriteRepository; import com.example.userservice.repository.jpa.MemberRepository; import com.example.userservice.repository.place.AccommodationRepository; import com.example.userservice.repository.place.AttractionRepository; import com.example.userservice.repository.place.CafeRepository; import com.example.userservice.repository.place.RestaurantRepository; -import com.fasterxml.jackson.databind.JsonNode; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; import java.util.*; import java.util.stream.Collectors; @@ -35,42 +27,54 @@ public class FavoriteService { private final FavoriteRepository favoriteRepository; private final MemberRepository memberRepository; - private static final RestTemplate restTemplate = new RestTemplate(); private final AccommodationRepository accommodationRepository; private final AttractionRepository attractionRepository; private final CafeRepository cafeRepository; private final RestaurantRepository restaurantRepository; - @Value("${gateway-url}") - private String gatewayUrl; +// @Value("${gateway-url}") +// private String gatewayUrl; + + public FavoriteDto FindPlace(Favorite item){ + Long placeId = Long.valueOf(item.getPlaceId()); + String type = item.getType(); + return switch (type) { + case "cafe" -> cafeRepository.findById(placeId) + .map(cafe -> buildFavoriteDto(item, cafe.getName(), cafe.getAddress())) + .orElseThrow(() -> new NoSuchElementException("Cafe not found")); + case "restaurant" -> restaurantRepository.findById(placeId) + .map(restaurant -> buildFavoriteDto(item, restaurant.getName(), restaurant.getAddress())) + .orElseThrow(() -> new NoSuchElementException("Restaurant not found")); + case "attraction" -> attractionRepository.findById(placeId) + .map(attraction -> buildFavoriteDto(item, attraction.getName(), attraction.getAddress())) + .orElseThrow(() -> new NoSuchElementException("Attraction not found")); + case "accommodation" -> accommodationRepository.findById(placeId) + .map(acc -> buildFavoriteDto(item, acc.getName(), acc.getAddress())) + .orElseThrow(() -> new NoSuchElementException("Accommodation not found")); + default -> throw new IllegalArgumentException("장소 type 오류: " + type); + }; + } + + private FavoriteDto buildFavoriteDto(Favorite item, String name, String address) { + return new FavoriteDto( + item.getFavoriteId(), + item.getType(), + item.getPlaceId(), + item.getMember().getUserId(), + name, + address + ); + } public List getListData(String userId) { try{ List items = favoriteRepository.findAllByMember_UserId(userId); return items.stream() - .map(item -> { - - ResponseEntity response = restTemplate.exchange( - gatewayUrl+"/place/main/info/"+item.getType()+"/" + item.getPlaceId(), - HttpMethod.GET, - null, - JsonNode.class - ); - String name = response.getBody().get(item.getType()+"_name").asText(); - String address = response.getBody().get("address").asText(); - return new FavoriteDto( - item.getFavoriteId(), - item.getType(), - item.getPlaceId(), - userId, - name, - address); - }).toList(); + .map(this::FindPlace).toList(); } catch (Exception e) { -// throw new RuntimeException(e); log.error(e.getMessage()); throw new BadCredentialsException("즐겨찾기 장소 조회 실패",e); } @@ -117,7 +121,7 @@ public ResponseEntity> addFavoriteData(String userId, Favori } } - +// //place-service와 REST 통신 방식 // ResponseEntity checkPlace = restTemplate.exchange( // gatewayUrl + "/place/main/info/" + favoriteDto.getType() + "/" + favoriteDto.getPlace_id(), // HttpMethod.GET, @@ -166,72 +170,14 @@ public List> getAllFavoriteData() { List items = favoriteRepository.findAll(); List favoriteDtoList = items.stream() - .map(item ->{ - - if(Objects.equals(item.getType(), "cafe")){ - Optional cafe = cafeRepository.findById(Long.valueOf(item.getPlaceId())); - String name = cafe.get().getName(); - String address = cafe.get().getAddress(); - return new FavoriteDto( - item.getFavoriteId(), - item.getType(), - item.getPlaceId(), - item.getMember().getUserId(), - name, - address); - }else if(Objects.equals(item.getType(), "restaurant")){ - Optional retaurant = restaurantRepository.findById(Long.valueOf(item.getPlaceId())); - String name = retaurant.get().getName(); - String address = retaurant.get().getAddress(); - return new FavoriteDto( - item.getFavoriteId(), - item.getType(), - item.getPlaceId(), - item.getMember().getUserId(), - name, - address); - }else if(Objects.equals(item.getType(), "attraction")){ - Optional attraction = attractionRepository.findById(Long.valueOf(item.getPlaceId())); - String name = attraction.get().getName(); - String address = attraction.get().getAddress(); - return new FavoriteDto( - item.getFavoriteId(), - item.getType(), - item.getPlaceId(), - item.getMember().getUserId(), - name, - address); - }else if(Objects.equals(item.getType(), "accommodation")){ - Optional accommodation = accommodationRepository.findById(Long.valueOf(item.getPlaceId())); - String name = accommodation.get().getName(); - String address = accommodation.get().getAddress(); - return new FavoriteDto( - item.getFavoriteId(), - item.getType(), - item.getPlaceId(), - item.getMember().getUserId(), - name, - address); - } - - -// ResponseEntity response = restTemplate.exchange( -// gatewayUrl+"/place/main/info/"+item.getType()+"/" + item.getPlaceId(), -// HttpMethod.GET, -// null, -// JsonNode.class -// ); -// String name = response.getBody().get(item.getType()+"_name").asText(); -// String address = response.getBody().get("address").asText(); - - return null; - }).toList(); + .map(this::FindPlace).toList(); Map> grouped = favoriteDtoList.stream() .collect(Collectors.groupingBy(FavoriteDto::getUser_id)); // 원하는 형태로 변환 - List> result = grouped.entrySet().stream() + + return grouped.entrySet().stream() .map(entry -> { Map map = new HashMap<>(); map.put("user_id", entry.getKey()); @@ -240,8 +186,6 @@ public List> getAllFavoriteData() { }) .toList(); - return result; - } catch (Exception e) { // throw new RuntimeException(e); log.error(e.getMessage()); @@ -249,4 +193,5 @@ public List> getAllFavoriteData() { } } + } From 9c758166461e4d6515a473e2f6e87db16e8247e7 Mon Sep 17 00:00:00 2001 From: ByeongukYun Date: Wed, 28 May 2025 10:26:50 +0900 Subject: [PATCH 355/361] =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=ED=83=88=ED=87=B4?= =?UTF-8?q?=20=EC=8B=9C=20=EC=B6=94=EC=B2=9C=20=EB=82=B4=EC=97=AD=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SignupController.java | 18 +++++++++++++++++- .../controller/SuggestController.java | 5 ----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/user-service/src/main/java/com/example/userservice/controller/SignupController.java b/user-service/src/main/java/com/example/userservice/controller/SignupController.java index 3e0cb7d..fe6dd1b 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SignupController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SignupController.java @@ -1,18 +1,22 @@ package com.example.userservice.controller; import com.example.userservice.dto.MemberSign; +import com.example.userservice.dto.SuggestFastRequestDto; +import com.example.userservice.dto.SuggestFastResponseDto; import com.example.userservice.entity.Member; import com.example.userservice.security.JwtUtil; import com.example.userservice.service.MemberService; import com.example.userservice.service.TokenService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; import java.util.Collections; import java.util.HashMap; @@ -27,6 +31,10 @@ public class SignupController { private final MemberService memberService; private final JwtUtil jwtUtil; private final TokenService tokenService; + private final RestTemplate restTemplate; + + @Value("${fastapi-svc}") + private String fastApiUrl; /** * 일반 사용자 회원가입 API @@ -83,6 +91,14 @@ public ResponseEntity signout( return ResponseEntity.status(403).body("본인만 탈퇴할 수 있습니다."); } + // fast-api 서버에 데이터 삭제 요청 + try { + ResponseEntity responseEntity = restTemplate.getForEntity(fastApiUrl + "/suggest/" + userId + "/delete", Void.class); + } catch (Exception e) { + return ResponseEntity.badRequest().body("탈퇴 전 AI 여행지 추천 내역 삭제 실패 : " + e.getMessage()); + } + log.debug("AI 여행지 추천 내역 삭제 완료"); + try { memberService.deleteMemberById(memberId); log.debug("회원탈퇴 완료: memberId={}", memberId); diff --git a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java index f10fe21..ab4a6cd 100644 --- a/user-service/src/main/java/com/example/userservice/controller/SuggestController.java +++ b/user-service/src/main/java/com/example/userservice/controller/SuggestController.java @@ -7,23 +7,18 @@ import com.example.userservice.entity.Member; import com.example.userservice.service.MemberService; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; -import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.ErrorResponse; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; -import java.util.List; - @RestController @RequestMapping("/suggest") @Slf4j From cf52db0d282d67c0c2c87bfc0ed0c1a9de9a6c95 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Thu, 29 May 2025 09:38:47 +0900 Subject: [PATCH 356/361] =?UTF-8?q?Refactor:=20=ED=96=89=EC=82=AC=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20Lat=20null=EA=B0=92=EC=9D=BC=20?= =?UTF-8?q?=EB=95=8C=20=EC=A0=80=EC=9E=A5=20=EC=95=88=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CulturalEventService.java | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java index be74042..82260ca 100644 --- a/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java +++ b/place-service/src/main/java/com/example/placeservice/service/CulturalEventService.java @@ -58,31 +58,35 @@ public void fetchAndSaveAllEvents() { private void saveEvents(List eventItems) { List events = eventParserService.toEntityList(eventItems); - List areas = areaRepository.findAll(); // 🔹 모든 지역 좌표 불러오기 - - for (CulturalEvent event : events) { - if (!culturalEventRepository.existsByTitleAndAddressAndStartDate( - event.getTitle(), event.getAddress(), event.getStartDate())) { - - // 행사 위치 기준 가장 가까운 지역 찾기 - Area nearestArea = findNearestArea(event.getLat().doubleValue(), event.getLon().doubleValue(), areas); - event.setArea(nearestArea); // 🔹 지역 설정 - - culturalEventRepository.save(event); - }else{ - // 이미 있는 데이터인데 종료일이 지났으면 삭제 - List existing = culturalEventRepository.findByTitleAndAddressAndStartDate( - event.getTitle(), event.getAddress(), event.getStartDate()); - - if (existing != null){ - for (CulturalEvent existingEvent : existing) { - if(existingEvent.getEndDate() != null && existingEvent.getEndDate().isBefore(LocalDate.now().atStartOfDay())){ - culturalEventRepository.delete(existingEvent); + List areas = areaRepository.findAll(); + + for (CulturalEvent event : events) { + if (!culturalEventRepository.existsByTitleAndAddressAndStartDate( + event.getTitle(), event.getAddress(), event.getStartDate())) { + try{ + // 행사 위치 기준 가장 가까운 지역 찾기 + Area nearestArea = findNearestArea(event.getLat().doubleValue(), event.getLon().doubleValue(), areas); + event.setArea(nearestArea); // 🔹 지역 설정 + + culturalEventRepository.save(event); + }catch (Exception e){ + log.error(e.getMessage()+event.getTitle()); + } + }else{ + // 이미 있는 데이터인데 종료일이 지났으면 삭제 + List existing = culturalEventRepository.findByTitleAndAddressAndStartDate( + event.getTitle(), event.getAddress(), event.getStartDate()); + + if (existing != null){ + for (CulturalEvent existingEvent : existing) { + if(existingEvent.getEndDate() != null && existingEvent.getEndDate().isBefore(LocalDate.now().atStartOfDay())){ + culturalEventRepository.delete(existingEvent); + } } } } } - } + } private Area findNearestArea(double lat, double lon, List areas) { From cae24e5ffc7f1b7bad3e20b766bfc4d036de847a Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 30 May 2025 14:05:29 +0900 Subject: [PATCH 357/361] =?UTF-8?q?Refactor:=20ai=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EC=9A=94=EC=95=BD=20=EC=88=98=EC=A0=95=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/PlaceDocumentRepository.java | 22 ----------------- .../service/AiSummaryService.java | 24 ++++++++++++------- 2 files changed, 15 insertions(+), 31 deletions(-) delete mode 100644 place-service/src/main/java/com/example/placeservice/repository/PlaceDocumentRepository.java diff --git a/place-service/src/main/java/com/example/placeservice/repository/PlaceDocumentRepository.java b/place-service/src/main/java/com/example/placeservice/repository/PlaceDocumentRepository.java deleted file mode 100644 index 96a280f..0000000 --- a/place-service/src/main/java/com/example/placeservice/repository/PlaceDocumentRepository.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.placeservice.repository; - -import com.example.placeservice.dto.PlaceDocument; -import org.springframework.stereotype.Repository; - -import java.util.ArrayList; -import java.util.List; - -@Repository -public class PlaceDocumentRepository { - - private final List placeDocuments = new ArrayList<>(); - - public List findAll() { - return placeDocuments; - } - - public void saveAll(List documents) { - placeDocuments.clear(); - placeDocuments.addAll(documents); - } -} diff --git a/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java b/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java index ed605cf..197ef71 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java @@ -2,6 +2,7 @@ import com.example.placeservice.dto.AiSummaryResponse; import com.example.placeservice.dto.AiSummaryParsedResponse; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; @@ -12,6 +13,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +@Slf4j @Service public class AiSummaryService { @@ -25,15 +27,19 @@ public AiSummaryService(@Value("${fastapi-svc}") String fastapiUrl) { } public AiSummaryParsedResponse getSummaryParsed(String targetType, String targetId) { - AiSummaryResponse raw = webClient.get() - .uri(uriBuilder -> uriBuilder - .path("/summary/{target_type}/{target_id}") - .build(targetType, targetId)) - .retrieve() - .bodyToMono(AiSummaryResponse.class) - .block(); - - return parseSummaryContent(Objects.requireNonNull(raw)); + try { + AiSummaryResponse raw = webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/summary/{target_type}/{target_id}") + .build(targetType, targetId)) + .retrieve() + .bodyToMono(AiSummaryResponse.class) + .block(); + return parseSummaryContent(Objects.requireNonNull(raw)); + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } } private AiSummaryParsedResponse parseSummaryContent(AiSummaryResponse raw) { From 621a04103f40b471b3ed9a0b3f5545c8e6a6d4e6 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 30 May 2025 14:23:31 +0900 Subject: [PATCH 358/361] =?UTF-8?q?Refactor:=20ai=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EC=9A=94=EC=95=BD=20=EC=88=98=EC=A0=95=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AiSummaryService.java | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java b/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java index 197ef71..b15d198 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java @@ -27,35 +27,36 @@ public AiSummaryService(@Value("${fastapi-svc}") String fastapiUrl) { } public AiSummaryParsedResponse getSummaryParsed(String targetType, String targetId) { - try { - AiSummaryResponse raw = webClient.get() - .uri(uriBuilder -> uriBuilder - .path("/summary/{target_type}/{target_id}") - .build(targetType, targetId)) - .retrieve() - .bodyToMono(AiSummaryResponse.class) - .block(); - return parseSummaryContent(Objects.requireNonNull(raw)); - } catch (Exception e) { - log.error(e.getMessage(), e); - return null; - } + AiSummaryResponse raw = webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/summary/{target_type}/{target_id}") + .build(targetType, targetId)) + .retrieve() + .bodyToMono(AiSummaryResponse.class) + .block(); + return parseSummaryContent(Objects.requireNonNull(raw)); } private AiSummaryParsedResponse parseSummaryContent(AiSummaryResponse raw) { - String content = raw.getContent(); + log.info(raw.toString()); + log.info(raw.getContent()); + if (raw.getContent() != null && raw.getContent().contains("Summary not found")) { + return AiSummaryParsedResponse.builder().build(); + }else { + String content = raw.getContent(); - List posKeywords = extractKeywords(content, "\\[긍정 키워드\\](.*)"); - List negKeywords = extractKeywords(content, "\\[부정 키워드\\](.*)"); - int posCount = extractInt(content, "\\[긍정 라벨 수\\]\\s*(\\d+)"); - int negCount = extractInt(content, "\\[부정 라벨 수\\]\\s*(\\d+)"); + List posKeywords = extractKeywords(content, "\\[긍정 키워드\\](.*)"); + List negKeywords = extractKeywords(content, "\\[부정 키워드\\](.*)"); + int posCount = extractInt(content, "\\[긍정 라벨 수\\]\\s*(\\d+)"); + int negCount = extractInt(content, "\\[부정 라벨 수\\]\\s*(\\d+)"); - return AiSummaryParsedResponse.builder() - .positiveKeywords(posKeywords) - .negativeKeywords(negKeywords) - .positiveCount(posCount) - .negativeCount(negCount) - .build(); + return AiSummaryParsedResponse.builder() + .positiveKeywords(posKeywords) + .negativeKeywords(negKeywords) + .positiveCount(posCount) + .negativeCount(negCount) + .build(); + } } private List extractKeywords(String content, String pattern) { From 776bc8b298384a964800c22099251514aca081cb Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 30 May 2025 14:34:23 +0900 Subject: [PATCH 359/361] =?UTF-8?q?Refactor:=20ai=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EC=9A=94=EC=95=BD=20=EC=88=98=EC=A0=95=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../placeservice/service/AiSummaryService.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java b/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java index b15d198..1ce4860 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java @@ -38,13 +38,8 @@ public AiSummaryParsedResponse getSummaryParsed(String targetType, String target } private AiSummaryParsedResponse parseSummaryContent(AiSummaryResponse raw) { - log.info(raw.toString()); - log.info(raw.getContent()); - if (raw.getContent() != null && raw.getContent().contains("Summary not found")) { - return AiSummaryParsedResponse.builder().build(); - }else { - String content = raw.getContent(); - + String content = raw.getContent(); + try { List posKeywords = extractKeywords(content, "\\[긍정 키워드\\](.*)"); List negKeywords = extractKeywords(content, "\\[부정 키워드\\](.*)"); int posCount = extractInt(content, "\\[긍정 라벨 수\\]\\s*(\\d+)"); @@ -56,6 +51,10 @@ private AiSummaryParsedResponse parseSummaryContent(AiSummaryResponse raw) { .positiveCount(posCount) .negativeCount(negCount) .build(); + }catch (Exception e){ + log.debug(content); + log.error(e.getMessage()); + return AiSummaryParsedResponse.builder().build(); } } From 2c2aaa5b387f5914cbe131ad122fe5a461e6b189 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 30 May 2025 14:42:26 +0900 Subject: [PATCH 360/361] =?UTF-8?q?Refactor:=20ai=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EC=9A=94=EC=95=BD=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=97=86?= =?UTF-8?q?=EC=9D=84=20=EC=8B=9C=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20?= =?UTF-8?q?500=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/placeservice/service/AiSummaryService.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java b/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java index 1ce4860..20b6911 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AiSummaryService.java @@ -8,6 +8,7 @@ import org.springframework.web.reactive.function.client.WebClient; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.regex.Matcher; @@ -54,7 +55,12 @@ private AiSummaryParsedResponse parseSummaryContent(AiSummaryResponse raw) { }catch (Exception e){ log.debug(content); log.error(e.getMessage()); - return AiSummaryParsedResponse.builder().build(); + return AiSummaryParsedResponse.builder() + .positiveKeywords(Collections.singletonList("")) + .negativeKeywords(Collections.singletonList("")) + .positiveCount(0) + .negativeCount(0) + .build(); } } From 7dcf612cbc6eb75cf259b12c54fc07b72d13c089 Mon Sep 17 00:00:00 2001 From: ha-yun Date: Fri, 30 May 2025 17:34:06 +0900 Subject: [PATCH 361/361] =?UTF-8?q?Refactor:=20=EA=B4=80=EA=B4=91=EC=A7=80?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20homepage=5Furl=20=EC=A0=91?= =?UTF-8?q?=EC=86=8D=20=EA=B0=80=EB=8A=A5=ED=95=9C=20=EB=A7=81=ED=81=AC?= =?UTF-8?q?=EB=A7=8C=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AttractionService.java | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java index a32387d..bdb140e 100644 --- a/place-service/src/main/java/com/example/placeservice/service/AttractionService.java +++ b/place-service/src/main/java/com/example/placeservice/service/AttractionService.java @@ -21,6 +21,8 @@ import java.io.StringReader; import java.math.BigDecimal; +import java.net.HttpURLConnection; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Objects; @@ -61,13 +63,12 @@ public void fetchDataFromVisitSeoul() { // seoul_attraction_id 중복 체크 if (attractionRepository.existsBySeoulAttractionId(table.getId())) return null; - if (Integer.valueOf(table.getId()) >= 31345 && Integer.valueOf(table.getId()) <= 53595) return null; + if (Integer.parseInt(table.getId()) >= 31345 && Integer.parseInt(table.getId()) <= 53595) return null; // 👇: 조건에 따라 Area 객체를 지정 Area area = findAreaByCondition(table, areaList); // 예: 주소나 지역코드 등으로 판단 if (area == null) return null; - Attraction attraction = new Attraction(); attraction.setSeoulAttractionId(table.getId()); attraction.setName(table.getTitle()); @@ -75,7 +76,13 @@ public void fetchDataFromVisitSeoul() { attraction.setLat(new BigDecimal(table.getMapY())); attraction.setLon(new BigDecimal(table.getMapX())); attraction.setPhone(table.getTel()); - attraction.setHomepageUrl(table.getHomepage()); + + + if(normalizeAndValidateUrl(table.getHomepage()) != null){ + attraction.setHomepageUrl(normalizeAndValidateUrl(table.getHomepage())); + } + +// attraction.setHomepageUrl(table.getHomepage()); attraction.setCloseDay(table.getCloseDay()); attraction.setUseTime(table.getUseTime()); attraction.setArea(area); @@ -111,7 +118,6 @@ public void fetchDataFromVisitSeoul() { } } - // 1. 관광지 기준 가장 가까운 area를 찾는다 private Area findAreaByCondition(AttractionDto.AttractionTable table, List areaList) { double lat = Double.parseDouble(table.getMapY()); double lon = Double.parseDouble(table.getMapX()); @@ -201,4 +207,32 @@ public AttractionInfoDto getAttractionInfoData(long attractionId) { throw new RuntimeException("예상치 못한 오류",e); } } + + // 접속이 가능한 홈페이지인지 확인 + public String normalizeAndValidateUrl(String rawUrl) { + if (rawUrl == null || rawUrl.isBlank()) return null; + + // 기본적인 정제 + String url = rawUrl.trim(); + if (!url.startsWith("http")) { + url = "https://" + url; + } + + try { + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestMethod("HEAD"); + conn.setConnectTimeout(1000); + conn.setReadTimeout(1000); + int code = conn.getResponseCode(); + + if (code >= 200 && code < 400) { + return url; + }else{ + return null; + } + } catch (Exception e) { + return null; // 실패 시 null 반환 + } + + } }