diff --git a/Jenkinsfile b/Jenkinsfile
index f197d3f..8174385 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -1,64 +1,88 @@
pipeline {
agent any
environment {
- JAVA_HOME = '/usr/lib/jvm/java-17-openjdk-amd64'
- PATH = "${env.JAVA_HOME}/bin:${env.PATH}"
+ JAVA_HOME = '/usr/lib/jvm/java-17-openjdk-amd64'
+ PATH = "${env.JAVA_HOME}/bin:${env.PATH}"
+ DB_URL = 'jdbc:mariadb://localhost:3306/travel'
+ DB_USERNAME = 'root'
+ DB_PASSWORD = '1q!1q!'
+ JENKINS_SERVER = "172.17.0.2"
+ NGINX_MINIJIN = "172.17.0.3"
+ BUILD_PJASYPT = credentials('Pjasypt')
+ DEPLOY_DJASYPT = credentials('Djasypt')
}
stages {
stage('Checkout') {
steps {
- // 저장소의 해당 브랜치를 체크아웃합니다.
git branch: env.BRANCH_NAME, url: 'https://github.com/team-MiniJin/BE.git'
+ sh 'java -version'
+ sh './gradlew -v'
+ echo "Building branch: ${env.BRANCH_NAME}"
+ sh '''
+ JDBC_DRIVER_PATH="/usr/local/lib/mariadb-java-client-3.3.3.jar"
+ WORK_DIR="/tmp"
+ javac -cp .:$JDBC_DRIVER_PATH $WORK_DIR/TestJDBC.java
+ java -cp .:$WORK_DIR:$JDBC_DRIVER_PATH TestJDBC
+ '''
}
}
stage('Build') {
+ when { branch 'develop' }
steps {
- script {
- if (env.BRANCH_NAME == 'develop' || env.BRANCH_NAME.startsWith('feature/')) {
- // 빌드 단계
- sh './gradlew build'
- }
- }
+ sh "./gradlew clean build ${BUILD_PJASYPT}"
}
}
stage('Test') {
+ when { branch pattern: 'develop|feature/.*' }
steps {
- script {
- if (env.BRANCH_NAME == 'develop' || env.BRANCH_NAME.startsWith('feature/')) {
- // 테스트 단계
- sh './gradlew test'
- }
- }
+ sh "./gradlew test ${BUILD_PJASYPT}"
}
}
stage('Release') {
- when {
- branch 'release/*'
- }
+ when { branch 'release/*' }
steps {
- // 릴리스 단계
sh './gradlew publish'
}
}
stage('Hotfix') {
- when {
- branch 'hotfix/*'
- }
+ when { branch 'hotfix/*' }
steps {
- // 핫픽스 단계
sh './gradlew build'
}
}
- /*
stage('Deploy') {
when {
- branch 'main'
+ anyOf {
+ branch 'develop'
+ changeRequest()
+ }
}
steps {
- // 배포 단계
- sh './gradlew deploy'
+ script {
+ echo "Using SSH credentials: jenkins-ssh-key"
+ sshagent (credentials: ['jenkins-ssh-key']) {
+ try {
+ sh """
+ echo "Starting SSH connection..."
+ ssh -i ~/.ssh/jenkins_agent_key root@$NGINX_MINIJIN 'echo "SSH connection successful"'
+ echo "SSH connection established successfully."
+ echo "Killing 8080, java service"
+ ssh -i ~/.ssh/jenkins_agent_key root@$NGINX_MINIJIN '/home/user/kill_java.sh'
+ echo "Killing Complete 8080, java service"
+ echo "Transferring file..."
+ scp -i ~/.ssh/jenkins_agent_key /var/jenkins_home/workspace/minijin_BE_develop/build/libs/travel-0.0.1-SNAPSHOT.jar root@${env.NGINX_MINIJIN}:/home/user/
+ echo "File transferred successfully."
+ echo "Deploying the application..."
+ ssh -i ~/.ssh/jenkins_agent_key -o StrictHostKeyChecking=no root@${env.NGINX_MINIJIN} "nohup java ${env.DEPLOY_DJASYPT} -jar /home/user/travel-0.0.1-SNAPSHOT.jar --server.port=8080 > /home/user/travel.log 2>&1 &"
+ echo "Application deployed successfully."
+ """
+ } catch (Exception e) {
+ echo "SSH connection or file transfer failed: ${e}"
+ error "Stopping pipeline due to SSH failure"
+ }
+ }
+ }
}
}
- */
}
-}
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 1012bcd..abfdfc8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,48 +1,62 @@
plugins {
- id 'java'
- id 'org.springframework.boot' version '3.3.0'
- id 'io.spring.dependency-management' version '1.1.5'
+ id 'java'
+ id 'org.springframework.boot' version '3.3.0'
+ id 'io.spring.dependency-management' version '1.1.5'
}
group = 'com.minizin.travel'
version = '0.0.1-SNAPSHOT'
java {
- sourceCompatibility = '17'
+ sourceCompatibility = '17'
}
configurations {
- compileOnly {
- extendsFrom annotationProcessor
- }
+ compileOnly {
+ extendsFrom annotationProcessor
+ }
}
repositories {
- mavenCentral()
+ mavenCentral()
}
dependencies {
- // Framework
- implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
- implementation 'org.springframework.boot:spring-boot-starter-security'
- implementation 'org.springframework.boot:spring-boot-starter-validation'
- implementation 'org.springframework.boot:spring-boot-starter-web'
- //jwt
- implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
- implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
- implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
- // lombok
- compileOnly 'org.projectlombok:lombok'
- annotationProcessor 'org.projectlombok:lombok'
- // DB
- runtimeOnly 'com.h2database:h2'
- runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
- //// test for
- testImplementation 'org.springframework.boot:spring-boot-starter-test'
- testImplementation 'org.springframework.security:spring-security-test'
- testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+ // Framework
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
+ implementation 'org.springframework.boot:spring-boot-starter-mail'
+ implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
+ // jasypt
+ implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5'
+ //jwt
+ implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
+ implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
+ implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
+ // lombok
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
+ // DB
+ runtimeOnly 'com.h2database:h2'
+ runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
+ implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+ // gson
+ implementation 'com.google.code.gson:gson:2.8.8'
+ // okhttp
+ implementation 'com.squareup.okhttp3:okhttp:4.9.3'
+ //API DOC
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
+ //// test for
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testImplementation 'org.springframework.security:spring-security-test'
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+ testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.3'
}
tasks.named('test') {
- useJUnitPlatform()
+ useJUnitPlatform()
+ systemProperty 'jasypt.encryptor.password', findProperty("jasypt.encryptor.password")
}
diff --git a/gradlew b/gradlew
old mode 100644
new mode 100755
diff --git a/gradlew.bat b/gradlew.bat
index 25da30d..7101f8e 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -1,92 +1,92 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-
-@if "%DEBUG%"=="" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%"=="" set DIRNAME=.
-@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
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@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/src/.DS_Store b/src/.DS_Store
index 5a16a58..e1d7135 100644
Binary files a/src/.DS_Store and b/src/.DS_Store differ
diff --git a/src/main/java/com/minizin/travel/TravelApplication.java b/src/main/java/com/minizin/travel/TravelApplication.java
index fb09f4b..d02d81a 100644
--- a/src/main/java/com/minizin/travel/TravelApplication.java
+++ b/src/main/java/com/minizin/travel/TravelApplication.java
@@ -6,8 +6,8 @@
@SpringBootApplication
public class TravelApplication {
- public static void main(String[] args) {
- SpringApplication.run(TravelApplication.class, args);
- }
+ public static void main(String[] args) {
+ SpringApplication.run(TravelApplication.class, args);
+ }
}
diff --git a/src/main/java/com/minizin/travel/config/CorsMvcConfig.java b/src/main/java/com/minizin/travel/config/CorsMvcConfig.java
new file mode 100644
index 0000000..0e9d8e6
--- /dev/null
+++ b/src/main/java/com/minizin/travel/config/CorsMvcConfig.java
@@ -0,0 +1,24 @@
+package com.minizin.travel.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+@Slf4j
+public class CorsMvcConfig implements WebMvcConfigurer {
+
+ @Override
+ public void addCorsMappings(CorsRegistry corsRegistry) {
+ log.debug("Configuring CORS mappings");
+ corsRegistry.addMapping("/**")
+ .exposedHeaders("Set-Cookie", "Authorization")
+ .allowedOrigins("http://localhost:3000", "https://fe-two-blond.vercel.app")
+ .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
+ .allowCredentials(true)
+ .maxAge(3600L);
+ }
+
+}
diff --git a/src/main/java/com/minizin/travel/config/JasyptConfig.java b/src/main/java/com/minizin/travel/config/JasyptConfig.java
new file mode 100644
index 0000000..bd61c22
--- /dev/null
+++ b/src/main/java/com/minizin/travel/config/JasyptConfig.java
@@ -0,0 +1,35 @@
+package com.minizin.travel.config;
+
+import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties;
+import org.jasypt.encryption.StringEncryptor;
+import org.jasypt.encryption.pbe.PooledPBEStringEncryptor;
+import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@EnableEncryptableProperties
+public class JasyptConfig {
+
+ @Value("${jasypt.encryptor.password}")
+ private String key;
+
+ @Bean("jasyptStringEncryptor")
+ public StringEncryptor stringEncryptor() {
+ PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
+ SimpleStringPBEConfig config = new SimpleStringPBEConfig();
+
+ config.setPassword(key);
+ config.setAlgorithm("PBEWITHHMACSHA512ANDAES_256");
+ config.setKeyObtentionIterations("1000"); // 반복할 해싱 회수
+ config.setPoolSize("1");
+ config.setProviderName("SunJCE");
+ config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
+ config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator");
+ config.setStringOutputType("base64");
+ encryptor.setConfig(config);
+
+ return encryptor;
+ }
+}
diff --git a/src/main/java/com/minizin/travel/config/JpaAuditingConfiguration.java b/src/main/java/com/minizin/travel/config/JpaAuditingConfiguration.java
new file mode 100644
index 0000000..9260aa0
--- /dev/null
+++ b/src/main/java/com/minizin/travel/config/JpaAuditingConfiguration.java
@@ -0,0 +1,10 @@
+package com.minizin.travel.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+
+@Configuration
+@EnableJpaAuditing
+public class JpaAuditingConfiguration {
+
+}
diff --git a/src/main/java/com/minizin/travel/config/MailConfig.java b/src/main/java/com/minizin/travel/config/MailConfig.java
new file mode 100644
index 0000000..bf01e4d
--- /dev/null
+++ b/src/main/java/com/minizin/travel/config/MailConfig.java
@@ -0,0 +1,51 @@
+package com.minizin.travel.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.JavaMailSenderImpl;
+
+import java.util.Properties;
+
+@Configuration
+public class MailConfig {
+
+ @Value("${spring.mail.host}")
+ private String host;
+
+ @Value("${spring.mail.port}")
+ private int port;
+
+ @Value("${spring.mail.username}")
+ private String username;
+
+ @Value("${spring.mail.password}")
+ private String password;
+
+ @Bean
+ public JavaMailSender javaMailSender() {
+ JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
+ mailSender.setHost(host);
+ mailSender.setPort(port);
+ mailSender.setUsername(username);
+ mailSender.setPassword(password);
+ mailSender.setJavaMailProperties(this.getMailProperties());
+ mailSender.setDefaultEncoding("UTF-8");
+
+ return mailSender;
+ }
+
+ private Properties getMailProperties() {
+ Properties properties = new Properties();
+
+ properties.setProperty("mail.transport.protocol", "smtp");
+ properties.setProperty("mail.smtp.auth", "true");
+ properties.setProperty("mail.smtp.starttls.enable", "true");
+ properties.setProperty("mail.debug", "true");
+ properties.setProperty("mail.smtp.ssl.trust", "smtp.naver.com");
+ properties.setProperty("mail.smtp.ssl.enable", "true");
+
+ return properties;
+ }
+}
diff --git a/src/main/java/com/minizin/travel/config/OAuth2Config.java b/src/main/java/com/minizin/travel/config/OAuth2Config.java
new file mode 100644
index 0000000..b61ce23
--- /dev/null
+++ b/src/main/java/com/minizin/travel/config/OAuth2Config.java
@@ -0,0 +1,13 @@
+package com.minizin.travel.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
+
+@Configuration
+public class OAuth2Config {
+ @Bean
+ public DefaultOAuth2UserService defaultOAuth2UserService(){
+ return new DefaultOAuth2UserService();
+ }
+}
diff --git a/src/main/java/com/minizin/travel/config/RedisConfig.java b/src/main/java/com/minizin/travel/config/RedisConfig.java
new file mode 100644
index 0000000..892abcb
--- /dev/null
+++ b/src/main/java/com/minizin/travel/config/RedisConfig.java
@@ -0,0 +1,28 @@
+package com.minizin.travel.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+@Configuration
+public class RedisConfig {
+
+ @Value("${spring.data.redis.host}")
+ private String host;
+
+ @Value("${spring.data.redis.port}")
+ private int port;
+
+ @Bean
+ public RedisConnectionFactory redisConnectionFactory() {
+ return new LettuceConnectionFactory(host, port);
+ }
+
+ @Bean
+ public StringRedisTemplate stringRedisTemplate() {
+ return new StringRedisTemplate(this.redisConnectionFactory());
+ }
+}
diff --git a/src/main/java/com/minizin/travel/config/SecurityConfig.java b/src/main/java/com/minizin/travel/config/SecurityConfig.java
new file mode 100644
index 0000000..8ea4bf1
--- /dev/null
+++ b/src/main/java/com/minizin/travel/config/SecurityConfig.java
@@ -0,0 +1,145 @@
+package com.minizin.travel.config;
+
+import com.minizin.travel.user.auth.LoginFilter;
+import com.minizin.travel.user.jwt.JwtAuthenticationFilter;
+import com.minizin.travel.user.jwt.JwtExceptionFilter;
+import com.minizin.travel.user.jwt.TokenProvider;
+import com.minizin.travel.user.oauth2.CustomSuccessHandler;
+import com.minizin.travel.user.service.CustomOAuth2UserService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+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.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsUtils;
+
+import java.util.Collections;
+import java.util.List;
+
+@Slf4j
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+ private final AuthenticationConfiguration authenticationConfiguration;
+ private final CustomOAuth2UserService customOAuth2UserService;
+ private final CustomSuccessHandler customSuccessHandler;
+ private final TokenProvider tokenProvider;
+ private final JwtExceptionFilter jwtExceptionFilter;
+
+ // BCryptPasswordEncoder Bean 등록
+ @Bean
+ public BCryptPasswordEncoder bCryptPasswordEncoder() {
+ log.debug("Creating BCryptPasswordEncoder bean");
+ return new BCryptPasswordEncoder();
+ }
+
+ // AuthenticationManager Bean 등록
+ @Bean
+ @Lazy
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
+ log.debug("Creating AuthenticationManager bean");
+ return configuration.getAuthenticationManager();
+ }
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ log.debug("Configuring SecurityFilterChain");
+ http
+ .cors(corsCustomizer -> corsCustomizer.configurationSource(
+ (request -> {
+ log.debug("Setting CORS configuration");
+ CorsConfiguration configuration = new CorsConfiguration();
+
+ configuration.setAllowedOrigins(List.of("http://localhost:3000", "https://fe-two-blond.vercel.app"));
+ configuration.setAllowedMethods(Collections.singletonList("*"));
+ configuration.setAllowCredentials(true);
+ configuration.setAllowedHeaders(Collections.singletonList("*"));
+ configuration.setMaxAge(3600L);
+
+ configuration.setExposedHeaders(Collections.singletonList("Set-Cookie"));
+ configuration.setExposedHeaders(Collections.singletonList("Authorization"));
+
+ return configuration;
+ })
+ ));
+
+ //csrf disable
+ http
+ .csrf((csrf) -> {
+ log.debug("Disabling CSRF");
+ csrf.disable();
+ });
+
+ //From 로그인 방식 disable
+ http
+ .formLogin((formLogin) -> {
+ log.debug("Disabling formLogin");
+ formLogin.disable();
+ });
+
+ //HTTP Basic 인증 방식 disable
+ http
+ .httpBasic((httpBasic) -> {
+ log.debug("Disabling httpBasic");
+ httpBasic.disable();
+ });
+
+
+ //oauth2
+ http
+ .oauth2Login((oauth2) -> {
+ log.debug("Configuring OAuth2 login");
+ oauth2.userInfoEndpoint((userInfoEndpointConfig) ->
+ userInfoEndpointConfig.userService(customOAuth2UserService))
+ .successHandler(customSuccessHandler);
+ });
+
+ //경로별 인가 작업
+ http
+ .authorizeHttpRequests((auth) -> {
+ log.debug("Configuring URL authorization");
+ auth.requestMatchers(CorsUtils::isPreFlightRequest).permitAll();
+ auth.requestMatchers("/", "/auth/**", "/mails/auth-code/**",
+ "/users/find-id", "/users/find-password", "/tour/**",
+ "plans/others/**", "plans/popular/week",
+ "/swagger-ui/**", "/v3/api-docs/**")
+ .permitAll()
+ .anyRequest().authenticated();
+ });
+
+ // UsernamePasswordAuthenticationFilter 자리에 LoginFilter 추가
+ http
+ .addFilterAt(new LoginFilter(
+ authenticationManager(authenticationConfiguration), tokenProvider
+ ), UsernamePasswordAuthenticationFilter.class);
+
+ //JWT Authentication Filter 추가
+ http
+ .addFilterBefore(new JwtAuthenticationFilter(tokenProvider),
+ LoginFilter.class);
+ //JWT 예외 핸들러 filter 등록
+ http
+ .addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class);
+
+ //세션 설정 : STATELESS
+ http
+ .sessionManagement((session) -> {
+ log.debug("Setting session management to stateless");
+ session.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
+ });
+
+ return http.build();
+ }
+
+}
diff --git a/src/main/java/com/minizin/travel/config/SwaggerConfig.java b/src/main/java/com/minizin/travel/config/SwaggerConfig.java
new file mode 100644
index 0000000..3e3ee09
--- /dev/null
+++ b/src/main/java/com/minizin/travel/config/SwaggerConfig.java
@@ -0,0 +1,35 @@
+package com.minizin.travel.config;
+
+import io.swagger.v3.oas.models.servers.Server;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+import java.util.List;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Class: SwaggerConfig Project: travel Package: com.minizin.travel.config
+ *
+ * Description: SwaggerConfig
+ *
+ * @author dong-hoshin
+ * @date 6/25/24 21:54 Copyright (c) 2024 MiniJin
+ * @see GitHub Repository
+ */
+@Configuration
+public class SwaggerConfig {
+
+ @Bean
+ public OpenAPI api() {
+ return new OpenAPI()
+ .info(new Info()
+ .title("travel API Document")
+ .version("1.0")
+ .description("travel Service Backend API Document"))
+ .servers(List.of(
+ new Server().url("http://lyckabc.synology.me:20280"),
+ new Server().url("https://lyckabc.synology.me:23080")
+ ));
+ }
+
+}
diff --git a/src/main/java/com/minizin/travel/global/GlobalExceptionHandler.java b/src/main/java/com/minizin/travel/global/GlobalExceptionHandler.java
new file mode 100644
index 0000000..9801fbf
--- /dev/null
+++ b/src/main/java/com/minizin/travel/global/GlobalExceptionHandler.java
@@ -0,0 +1,99 @@
+package com.minizin.travel.global;
+
+import com.minizin.travel.global.enums.ValidationErrorCode;
+import com.minizin.travel.user.domain.dto.ErrorResponse;
+import com.minizin.travel.user.domain.exception.CustomMailException;
+import com.minizin.travel.user.domain.exception.CustomUserException;
+import org.hibernate.exception.ConstraintViolationException;
+import org.hibernate.exception.JDBCConnectionException;
+import org.hibernate.exception.SQLGrammarException;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.util.List;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+ @ExceptionHandler(CustomUserException.class)
+ public ResponseEntity> handleCustomUserException(CustomUserException e) {
+ return ResponseEntity.status(e.getUserErrorCode().getStatus())
+ .body(new ErrorResponse(e.getUserErrorCode().getStatus(),
+ e.getUserErrorCode().getMessage())
+ );
+ }
+
+ @ExceptionHandler(CustomMailException.class)
+ public ResponseEntity> handleCustomMailException(CustomMailException e) {
+ return ResponseEntity.status(e.getMailErrorCode().getStatus())
+ .body(new ErrorResponse(e.getMailErrorCode().getStatus(),
+ e.getMailErrorCode().getMessage())
+ );
+ }
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
+ List errors = e.getBindingResult().getFieldErrors()
+ .stream().map(error -> new ValidationErrorResponse.FieldError(
+ error.getField(),
+ error.getRejectedValue(),
+ error.getDefaultMessage()))
+ .toList();
+
+ return ResponseEntity.badRequest()
+ .body(new ValidationErrorResponse(ValidationErrorCode.INVALID_REQUEST.getStatus(),
+ ValidationErrorCode.INVALID_REQUEST.getMessage(), errors));
+ }
+
+ @ExceptionHandler(DataIntegrityViolationException.class)
+ public ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException ex) {
+ ex.printStackTrace();
+ return new ResponseEntity<>("Data integrity violation: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ @ExceptionHandler(DataAccessException.class)
+ public ResponseEntity handleDataAccessException(DataAccessException ex) {
+ ex.printStackTrace();
+ return new ResponseEntity<>("Database error occurred: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ @ExceptionHandler(JDBCConnectionException.class)
+ public ResponseEntity handleJDBCConnectionException(JDBCConnectionException ex) {
+ ex.printStackTrace();
+ return new ResponseEntity<>("Database connection error: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ @ExceptionHandler(SQLGrammarException.class)
+ public ResponseEntity handleSQLGrammarException(SQLGrammarException ex) {
+ ex.printStackTrace();
+ return new ResponseEntity<>("SQL syntax error: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ @ExceptionHandler(ConstraintViolationException.class)
+ public ResponseEntity handleConstraintViolationException(ConstraintViolationException ex) {
+ ex.printStackTrace();
+ return new ResponseEntity<>("Database constraint violation: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ @ExceptionHandler(NullPointerException.class)
+ public ResponseEntity handleNullPointerException(NullPointerException ex) {
+ ex.printStackTrace();
+ return new ResponseEntity<>("Unexpected error occurred: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ @ExceptionHandler(IllegalArgumentException.class)
+ public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex) {
+ ex.printStackTrace();
+ return new ResponseEntity<>("Invalid argument: " + ex.getMessage(), HttpStatus.BAD_REQUEST);
+ }
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity handleException(Exception ex) {
+ ex.printStackTrace();
+ return new ResponseEntity<>("An error occurred: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+}
diff --git a/src/main/java/com/minizin/travel/global/ValidationErrorResponse.java b/src/main/java/com/minizin/travel/global/ValidationErrorResponse.java
new file mode 100644
index 0000000..0e5c0d1
--- /dev/null
+++ b/src/main/java/com/minizin/travel/global/ValidationErrorResponse.java
@@ -0,0 +1,23 @@
+package com.minizin.travel.global;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+import java.util.List;
+
+@Getter
+@AllArgsConstructor
+public class ValidationErrorResponse {
+ HttpStatus status;
+ String message;
+ List fieldErrors;
+
+ @Getter
+ @AllArgsConstructor
+ public static class FieldError {
+ private String field;
+ private Object rejectedValue;
+ private String reason;
+ }
+}
diff --git a/src/main/java/com/minizin/travel/global/enums/ValidationErrorCode.java b/src/main/java/com/minizin/travel/global/enums/ValidationErrorCode.java
new file mode 100644
index 0000000..3873446
--- /dev/null
+++ b/src/main/java/com/minizin/travel/global/enums/ValidationErrorCode.java
@@ -0,0 +1,15 @@
+package com.minizin.travel.global.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@RequiredArgsConstructor
+public enum ValidationErrorCode {
+ INVALID_REQUEST(HttpStatus.BAD_REQUEST, "유효성 검사에 실패했습니다."),
+ ;
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/src/main/java/com/minizin/travel/like/controller/LikeController.java b/src/main/java/com/minizin/travel/like/controller/LikeController.java
new file mode 100644
index 0000000..215a94e
--- /dev/null
+++ b/src/main/java/com/minizin/travel/like/controller/LikeController.java
@@ -0,0 +1,46 @@
+/*
+package com.minizin.travel.like.controller;
+
+import com.minizin.travel.like.service.LikeService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequiredArgsConstructor
+public class LikeController {
+
+ private final LikeService likeService;
+
+ // #52 2024.06.13 좋아요 생성 START //
+ @PostMapping("/likes/{plan_id}")
+ public ResponseEntity> createLikePlan(@PathVariable("plan_id") Long planId) {
+
+ var result = likeService.createLikePlan(planId);
+
+ return ResponseEntity.ok(result);
+ }
+ // #52 2024.06.13 좋아요 생성 END //
+
+ // #53 2024.06.13 좋아요 조회 START //
+ @GetMapping("/likes/{cursor_id}")
+ public ResponseEntity> selectListLikedPlans(@PathVariable("cursor_id") Long cursorId) {
+
+ var result = likeService.selectListLikedPlans(cursorId);
+
+ return ResponseEntity.ok(result);
+ }
+ // #53 2024.06.13 좋아요 조회 END //
+
+ // #54 2024.06.14 좋아요 삭제 START //
+ @DeleteMapping("/likes/{like_id}")
+ public ResponseEntity> deleteLikedPlan(@PathVariable("like_id") Long likeId) {
+
+ var result = likeService.deleteLikedPlan(likeId);
+
+ return ResponseEntity.ok(result);
+ }
+ // #54 2024.06.14 좋아요 삭제 END //
+
+}
+*/
diff --git a/src/main/java/com/minizin/travel/like/dto/ResponseCreateLikePlanDto.java b/src/main/java/com/minizin/travel/like/dto/ResponseCreateLikePlanDto.java
new file mode 100644
index 0000000..a5180a2
--- /dev/null
+++ b/src/main/java/com/minizin/travel/like/dto/ResponseCreateLikePlanDto.java
@@ -0,0 +1,34 @@
+package com.minizin.travel.like.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.like.entity.Likes;
+import lombok.Builder;
+import lombok.Getter;
+
+import java.time.format.DateTimeFormatter;
+
+@Builder
+@Getter
+@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class ResponseCreateLikePlanDto {
+
+ @JsonProperty("like_id")
+ private Long id;
+
+ private Long userId;
+
+ private Long planId;
+
+ private String createdAt;
+
+ public static ResponseCreateLikePlanDto toDto(Likes like) {
+ return ResponseCreateLikePlanDto.builder()
+ .id(like.getId())
+ .userId(like.getUserId())
+ .planId(like.getPlanId())
+ .createdAt(like.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
+ .build();
+ }
+}
diff --git a/src/main/java/com/minizin/travel/like/dto/ResponseDeleteLikePlanDto.java b/src/main/java/com/minizin/travel/like/dto/ResponseDeleteLikePlanDto.java
new file mode 100644
index 0000000..89da157
--- /dev/null
+++ b/src/main/java/com/minizin/travel/like/dto/ResponseDeleteLikePlanDto.java
@@ -0,0 +1,2 @@
+package com.minizin.travel.like.dto;public class ResponseDeleteLikePlanDto {
+}
diff --git a/src/main/java/com/minizin/travel/like/dto/ResponseSelectLikedPlansDto.java b/src/main/java/com/minizin/travel/like/dto/ResponseSelectLikedPlansDto.java
new file mode 100644
index 0000000..f16f4f4
--- /dev/null
+++ b/src/main/java/com/minizin/travel/like/dto/ResponseSelectLikedPlansDto.java
@@ -0,0 +1,17 @@
+/*package com.minizin.travel.like.dto;
+
+import lombok.Builder;
+import lombok.Getter;
+
+import java.util.List;
+
+@Getter
+@Builder
+public class ResponseSelectLikedPlansDto {
+
+ List data;
+
+ private Long cursorId;
+
+}
+*/
\ No newline at end of file
diff --git a/src/main/java/com/minizin/travel/like/dto/SelectLikedPlansDto.java b/src/main/java/com/minizin/travel/like/dto/SelectLikedPlansDto.java
new file mode 100644
index 0000000..0694f82
--- /dev/null
+++ b/src/main/java/com/minizin/travel/like/dto/SelectLikedPlansDto.java
@@ -0,0 +1,64 @@
+/*
+package com.minizin.travel.like.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.plan.entity.Plan;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDate;
+
+@Builder
+@Getter
+@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+@JsonPropertyOrder({"id", "planId", "userNickname", "planName", "theme", "startDate", "endDate",
+ "planBudget", "scope", "numberOfMembers", "numberOfLikes", "numberOfScraps"})
+public class SelectLikedPlansDto {
+
+ @Setter
+ @JsonProperty("like_id")
+ private Long id;
+
+ private Long planId;
+
+ @Setter
+ private String userNickname;
+
+ private String planName;
+
+ private String theme;
+
+ private LocalDate startDate;
+
+ private LocalDate endDate;
+
+ @Setter
+ private int planBudget;
+
+ private boolean scope;
+
+ private int numberOfMembers;
+
+ private int numberOfLikes;
+
+ private int numberOfScraps;
+
+ public static SelectLikedPlansDto toDto(Plan plan) {
+ return SelectLikedPlansDto.builder()
+ .planId(plan.getId())
+ .planName(plan.getPlanName())
+ .theme(plan.getTheme())
+ .startDate(plan.getStartDate())
+ .endDate(plan.getEndDate())
+ .scope(plan.isScope())
+ .numberOfMembers(plan.getNumberOfMembers())
+ .numberOfLikes(plan.getNumberOfLikes())
+ .numberOfScraps(plan.getNumberOfScraps())
+ .build();
+ }
+}
+*/
diff --git a/src/main/java/com/minizin/travel/like/entity/Likes.java b/src/main/java/com/minizin/travel/like/entity/Likes.java
new file mode 100644
index 0000000..4f93ed7
--- /dev/null
+++ b/src/main/java/com/minizin/travel/like/entity/Likes.java
@@ -0,0 +1,30 @@
+package com.minizin.travel.like.entity;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.CreatedDate;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Builder
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class Likes {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "like_id")
+ private Long id;
+
+ private Long userId;
+
+ private Long planId;
+
+ @CreatedDate
+ private LocalDateTime createdAt;
+}
diff --git a/src/main/java/com/minizin/travel/like/repository/LikeRepository.java b/src/main/java/com/minizin/travel/like/repository/LikeRepository.java
new file mode 100644
index 0000000..d8df97c
--- /dev/null
+++ b/src/main/java/com/minizin/travel/like/repository/LikeRepository.java
@@ -0,0 +1,23 @@
+package com.minizin.travel.like.repository;
+
+import com.minizin.travel.like.entity.Likes;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface LikeRepository extends JpaRepository {
+
+ // #52 2024.06.13 좋아요 생성 //
+ Boolean existsByPlanIdAndUserId(Long planId, Long userId);
+
+ // #53 2024.06.13 좋아요 조회 START //
+ List findAllByUserIdOrderByIdDesc(Long userId, Pageable page);
+
+ List findByIdLessThanAndUserIdOrderByIdDesc(Long cursorId, Long userId, Pageable page);
+
+ Boolean existsByUserId(Long userId);
+
+ // #53 2024.06.13 좋아요 조회 END //
+
+}
diff --git a/src/main/java/com/minizin/travel/like/service/LikeService.java b/src/main/java/com/minizin/travel/like/service/LikeService.java
new file mode 100644
index 0000000..0a96d48
--- /dev/null
+++ b/src/main/java/com/minizin/travel/like/service/LikeService.java
@@ -0,0 +1,130 @@
+/*
+package com.minizin.travel.like.service;
+
+import com.minizin.travel.like.dto.ResponseCreateLikePlanDto;
+import com.minizin.travel.like.dto.ResponseDeleteLikePlanDto;
+import com.minizin.travel.like.dto.ResponseSelectLikedPlansDto;
+import com.minizin.travel.like.dto.SelectLikedPlansDto;
+import com.minizin.travel.like.entity.Likes;
+import com.minizin.travel.like.repository.LikeRepository;
+import com.minizin.travel.plan.entity.Plan;
+import com.minizin.travel.plan.entity.PlanSchedule;
+import com.minizin.travel.plan.repository.PlanRepository;
+import com.minizin.travel.plan.repository.PlanScheduleRepository;
+import com.minizin.travel.plan.service.PlanService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class LikeService {
+
+ private final LikeRepository likeRepository;
+ private final PlanRepository planRepository;
+ private final PlanScheduleRepository planScheduleRepository;
+ private final PlanService planService;
+
+ final int DEFAULT_PAGE_SIZE = 6;
+
+ // #52 2024.06.13 좋아요 생성 START //
+ public ResponseCreateLikePlanDto createLikePlan(Long planId) {
+
+ // 테스트
+ Long userId = 1L;
+
+ if (!planRepository.existsById(planId)) {
+ throw new RuntimeException("유효하지 않은 plan id");
+ }
+
+ if (likeRepository.existsByPlanIdAndUserId(planId, userId)) {
+ throw new RuntimeException("이미 좋아요를 누른 plan");
+ }
+
+ Plan plan = planRepository.findById(planId).get();
+ plan.setNumberOfLikes(plan.getNumberOfLikes() + 1);
+
+ return ResponseCreateLikePlanDto.toDto(likeRepository.save(Likes.builder()
+ .userId(userId)
+ .planId(planId)
+ .createdAt(LocalDateTime.now())
+ .build()));
+ }
+ // #52 2024.06.13 좋아요 생성 END //
+
+ // #53 2024.06.13 좋아요 조회 START //
+ public ResponseSelectLikedPlansDto selectListLikedPlans(Long cursorId) {
+
+ Pageable page = PageRequest.of(0, DEFAULT_PAGE_SIZE);
+
+ // 테스트
+ Long userId = 1L;
+
+ if (!likeRepository.existsByUserId(userId)) {
+ throw new RuntimeException("좋아요한 plan이 없습니다.");
+ }
+
+ List likeList = findAllByCursorIdCheckExistCursor(userId, cursorId, page);
+ List listLikedPlansDtoList = new ArrayList<>();
+ Long likeId = 0L;
+ for (Likes like : likeList) {
+ likeId = like.getId();
+ Plan plan = planRepository.findById(like.getPlanId()).get();
+
+ SelectLikedPlansDto newSelectLikedPlansDto = SelectLikedPlansDto.toDto(plan);
+ newSelectLikedPlansDto.setId(likeId);
+ // 닉네임 추가 필요 newLikedPlansDto.setUserNickname
+
+ // 예산 계산
+ List planScheduleList = planScheduleRepository.findAllByPlanId(plan.getId());
+ newSelectLikedPlansDto.setPlanBudget(planService.calculateTotalPlanBudget(planScheduleList));
+
+ listLikedPlansDtoList.add(newSelectLikedPlansDto);
+ }
+
+ return ResponseSelectLikedPlansDto.builder()
+ .data(listLikedPlansDtoList)
+ .cursorId(likeId)
+ .build();
+ }
+
+ private List findAllByCursorIdCheckExistCursor(Long userId, Long cursorId, Pageable page) {
+
+ return cursorId == 0 ? likeRepository.findAllByUserIdOrderByIdDesc(userId, page)
+ : likeRepository.findByIdLessThanAndUserIdOrderByIdDesc(cursorId, userId, page);
+ }
+ // #53 2024.06.13 좋아요 조회 END //
+
+ // #54 2024.06.14 좋아요 삭제 START //
+ public ResponseDeleteLikePlanDto deleteLikedPlan(Long likeId) {
+
+ if (!likeRepository.existsById(likeId)) {
+
+ return ResponseDeleteLikePlanDto.builder()
+ .success(false)
+ .message("존재하지 않는 like_id 입니다.")
+ .likeId(likeId)
+ .build();
+ }
+
+ Long planId = likeRepository.findById(likeId).get().getPlanId();
+ Plan plan = planRepository.findById(planId).get();
+ plan.setNumberOfLikes(plan.getNumberOfLikes() - 1);
+
+ likeRepository.deleteById(likeId);
+
+ return ResponseDeleteLikePlanDto.builder()
+ .success(true)
+ .message("Like Deleted Successfully")
+ .likeId(likeId)
+ .build();
+ }
+ // #54 2024.06.14 좋아요 삭제 END //
+
+}
+*/
diff --git a/src/main/java/com/minizin/travel/plan/controller/OtherPlanController.java b/src/main/java/com/minizin/travel/plan/controller/OtherPlanController.java
new file mode 100644
index 0000000..b757bae
--- /dev/null
+++ b/src/main/java/com/minizin/travel/plan/controller/OtherPlanController.java
@@ -0,0 +1,64 @@
+package com.minizin.travel.plan.controller;
+
+import com.minizin.travel.plan.service.OtherPlanService;
+import lombok.RequiredArgsConstructor;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequiredArgsConstructor
+public class OtherPlanController {
+
+ private final OtherPlanService otherPlanService;
+
+
+ // #48 2024.06.10 다른 사람 여행 일정 조회 START //
+ @GetMapping("/plans/others/newest")
+ public ResponseEntity> selectOthersListPlan(@RequestParam("cursor_id") Long lastPlanId,
+ @RequestParam("region") String region,
+ @RequestParam("theme") String theme,
+ @RequestParam("search") String search) {
+
+ var result = otherPlanService.selectOthersListPlan(lastPlanId, region, theme, search);
+
+ return ResponseEntity.ok(result);
+ }
+ // #48 2024.06.10 다른 사람 여행 일정 조회 END //
+
+ // #58 2024.06.12 다른 사람 여행 일정 조회(북마크순) START //
+ @GetMapping("/plans/others/scraps")
+ public ResponseEntity> selectOthersListPlanScraps(@RequestParam("cursor_id") Long lastPlanId,
+ @RequestParam("region") String region,
+ @RequestParam("theme") String theme,
+ @RequestParam("search") String search) {
+
+ var result = otherPlanService.selectOthersListPlanScraps(lastPlanId, region, theme, search);
+
+ return ResponseEntity.ok(result);
+ }
+ // #58 2024.06.12 다른 사람 여행 일정 조회(북마크순) END //
+
+ // #106 2024.06.16 금주 인기 여행 일정 조회(북마크순) START //
+ @GetMapping("/plans/popular/week")
+ public ResponseEntity> selectPopularPlansWeek() {
+
+ var result = otherPlanService.selectPopularPlansWeek();
+
+ return ResponseEntity.ok(result);
+ }
+ // #106 2024.06.16 금주 인기 여행 일정 조회(북마크순) END //
+
+ // #129 다른 사람의 여행 일정 상세 보기 START //
+ @GetMapping("/plans/others/{plan_id}")
+ public ResponseEntity> selectOtherDetailPlan(@PathVariable("plan_id") Long planId) {
+
+ var result = otherPlanService.selectOtherDetailPlan(planId);
+
+ return ResponseEntity.ok(result);
+ }
+ // #129 다른 사람의 여행 일정 상세 보기 END //
+}
+
diff --git a/src/main/java/com/minizin/travel/plan/controller/PlanController.java b/src/main/java/com/minizin/travel/plan/controller/PlanController.java
index 7cd05c7..31da38e 100644
--- a/src/main/java/com/minizin/travel/plan/controller/PlanController.java
+++ b/src/main/java/com/minizin/travel/plan/controller/PlanController.java
@@ -1,10 +1,14 @@
package com.minizin.travel.plan.controller;
-import com.fasterxml.jackson.core.JsonProcessingException;
+import com.minizin.travel.plan.dto.EditPlanDto;
import com.minizin.travel.plan.dto.PlanDto;
import com.minizin.travel.plan.service.PlanService;
+import com.minizin.travel.user.domain.dto.PrincipalDetails;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
+import org.apache.coyote.BadRequestException;
import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
@RestController
@@ -16,10 +20,11 @@ public class PlanController {
// #28 2024.05.30 내 여행 일정 생성하기 START //
@PostMapping("/plans")
public ResponseEntity> createPlan(
- @RequestBody PlanDto request
- ) throws JsonProcessingException {
+ @RequestBody @Valid PlanDto request, // @Valid : #87 Request 예외/에러 처리
+ @AuthenticationPrincipal PrincipalDetails user)
+ throws BadRequestException {
- var result = planService.createPlan(request);
+ var result = planService.createPlan(request, user);
return ResponseEntity.ok(result);
@@ -28,12 +33,68 @@ public ResponseEntity> createPlan(
// #29 2024.06.02 내 여행 일정 조회 START //
@GetMapping("/plans")
- public ResponseEntity> selectListPlan(@RequestParam("cursor_id") Long cursorId) {
+ public ResponseEntity> selectListPlan(@RequestParam("cursor_id") Long lastPlanId,
+ @AuthenticationPrincipal PrincipalDetails user) { // #102 [GET] /plans : Refactoring - cursorId renaming
- var result = planService.selectListPlan(cursorId);
+ var result = planService.selectListPlan(lastPlanId, user);
return ResponseEntity.ok(result);
}
// #29 2024.06.02 내 여행 일정 조회 END //
+
+ // #32 2024.06.07 내 여행 일정 수정 START //
+ @PutMapping("/plans/{plan_id}")
+ public ResponseEntity> updatePlan(@PathVariable("plan_id") Long planId,
+ @RequestBody EditPlanDto request,
+ @AuthenticationPrincipal PrincipalDetails user) {
+
+ var result = planService.updatePlan(planId, request, user);
+
+ return ResponseEntity.ok(result);
+ }
+ // #32 2024.06.07 내 여행 일정 수정 END //
+
+ // #38 2024.06.08 내 여행 일정 상세 보기 START //
+ @GetMapping("/plans/details/{plan_id}")
+ public ResponseEntity> selectDetailPlan(@PathVariable("plan_id") Long planId,
+ @AuthenticationPrincipal PrincipalDetails user) {
+
+ var result = planService.selectDetailPlan(planId, user);
+
+ return ResponseEntity.ok(result);
+ }
+ // #38 2024.06.08 내 여행 일정 상세 보기 END //
+
+ // #47 2024.06.13 내 여행 일정 삭제 START //
+ @DeleteMapping("/plans/{plan_id}")
+ public ResponseEntity> deletePlan(@PathVariable("plan_id") Long planId,
+ @AuthenticationPrincipal PrincipalDetails user) {
+
+ var result = planService.deletePlan(planId, user);
+
+ return ResponseEntity.ok(result);
+ }
+ // #47 2024.06.13 내 여행 일정 삭제 END //
+
+ // #39 2024.06.10 다가오는 여행 일정 조회 START //
+ @GetMapping("/plans/upcoming")
+ public ResponseEntity> selectUpcomingPlan(@AuthenticationPrincipal PrincipalDetails user) {
+
+ var result = planService.selectUpcomingPlan(user);
+
+ return ResponseEntity.ok(result);
+ }
+ // #39 2024.06.10 다가오는 여행 일정 조회 END //
+
+ // #107 2024.06.20 일정 복사하기 START //
+ @GetMapping("/plans/copy/{plan_id}")
+ public ResponseEntity> copyAndCreatePlan(@PathVariable("plan_id") Long planId,
+ @AuthenticationPrincipal PrincipalDetails user) {
+
+ var result = planService.copyAndCreatePlan(planId, user);
+
+ return ResponseEntity.ok(result);
+ }
+ // #107 2024.06.20 일정 복사하기 END //
}
diff --git a/src/main/java/com/minizin/travel/plan/dto/DetailPlanBudgetDto.java b/src/main/java/com/minizin/travel/plan/dto/DetailPlanBudgetDto.java
new file mode 100644
index 0000000..85abbdd
--- /dev/null
+++ b/src/main/java/com/minizin/travel/plan/dto/DetailPlanBudgetDto.java
@@ -0,0 +1,31 @@
+package com.minizin.travel.plan.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.plan.entity.PlanBudget;
+import lombok.Builder;
+import lombok.Getter;
+
+@Builder
+@Getter
+@JsonPropertyOrder({"id", "budgetCategory", "cost", "budgetMemo"})
+@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class DetailPlanBudgetDto {
+
+ @JsonProperty("budget_id")
+ private Long id;
+
+ private String budgetCategory;
+
+ private Integer cost;
+
+ public static DetailPlanBudgetDto toDto(PlanBudget planBudget) {
+ return DetailPlanBudgetDto.builder()
+ .id(planBudget.getId())
+ .budgetCategory(planBudget.getBudgetCategory())
+ .cost(planBudget.getCost())
+ .build();
+ }
+}
diff --git a/src/main/java/com/minizin/travel/plan/dto/DetailPlanDto.java b/src/main/java/com/minizin/travel/plan/dto/DetailPlanDto.java
new file mode 100644
index 0000000..b95e925
--- /dev/null
+++ b/src/main/java/com/minizin/travel/plan/dto/DetailPlanDto.java
@@ -0,0 +1,91 @@
+package com.minizin.travel.plan.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.plan.entity.Plan;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDate;
+import java.util.List;
+
+@Builder
+@Getter
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonPropertyOrder({"id", "userId", "planName", "theme", "startDate", "endDate", "planBudget", "scope", "numberOfMembers",
+ "numberOfScraps", "regionList", "detailPlanScheduleDtoList"})
+@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class DetailPlanDto {
+
+ @JsonProperty("plan_id")
+ private Long id;
+
+ private Long userId;
+
+ private String planName;
+
+ @Setter
+ private String userNickname;
+
+ private String theme;
+
+ private LocalDate startDate;
+
+ private LocalDate endDate;
+
+ @Setter
+ private Integer planBudget;
+
+ private boolean scope;
+
+ private Integer numberOfMembers;
+
+ private Integer numberOfScraps;
+
+ @Setter
+ private List regionList;
+
+ @Setter
+ @JsonProperty("schedules")
+ private List detailPlanScheduleDtoList;
+
+ private Integer error;
+ private String message;
+
+ public static DetailPlanDto toDto(Plan plan) {
+ return DetailPlanDto.builder()
+ .id(plan.getId())
+ .userId(plan.getUserId())
+ .planName(plan.getPlanName())
+ .theme(plan.getTheme())
+ .startDate(plan.getStartDate())
+ .endDate(plan.getEndDate())
+ .scope(plan.isScope())
+ .numberOfMembers(plan.getNumberOfMembers())
+ .numberOfScraps(plan.getNumberOfScraps())
+ .build();
+ }
+
+ public static DetailPlanDto existsNot(Long planId) {
+
+ return DetailPlanDto.builder()
+ .error(-1)
+ .id(planId)
+ .message("요청하신 plan 은 존재하지 않습니다.")
+ .build();
+ }
+
+ public static DetailPlanDto notAuth(Long planId, String message) {
+
+ return DetailPlanDto.builder()
+ .error(-2)
+ .id(planId)
+ .message(message)
+ .build();
+ }
+
+}
diff --git a/src/main/java/com/minizin/travel/plan/dto/DetailPlanScheduleDto.java b/src/main/java/com/minizin/travel/plan/dto/DetailPlanScheduleDto.java
new file mode 100644
index 0000000..ae1da7c
--- /dev/null
+++ b/src/main/java/com/minizin/travel/plan/dto/DetailPlanScheduleDto.java
@@ -0,0 +1,64 @@
+package com.minizin.travel.plan.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.plan.entity.PlanSchedule;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+@Builder
+@Getter
+@JsonPropertyOrder({"id", "scheduleDays", "scheduleDate", "placeCategory", "placeName", "region", "placeMemo", "arrivalTime", "detailPlanBudgetDtoList", "x", "y", "placeAddr"})
+@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class DetailPlanScheduleDto {
+
+ @JsonProperty("schedule_id")
+ private Long id;
+
+ @Setter
+ private int scheduleDays;
+
+ private LocalDate scheduleDate;
+
+ private String placeName;
+
+ private String region;
+
+ private String placeMemo;
+
+ private String arrivalTime;
+
+ private Double x;
+ private Double y;
+
+ private String placeAddr;
+
+ private String placeCategory;
+
+ @Setter
+ @JsonProperty("budgets")
+ List detailPlanBudgetDtoList;
+
+ public static DetailPlanScheduleDto toDto(PlanSchedule planSchedule) {
+ return DetailPlanScheduleDto.builder()
+ .id(planSchedule.getId())
+ .scheduleDate(planSchedule.getScheduleDate())
+ .placeName(planSchedule.getPlaceName())
+ .region(planSchedule.getRegion())
+ .placeMemo(planSchedule.getPlaceMemo())
+ .arrivalTime(planSchedule.getArrivalTime().format(DateTimeFormatter.ofPattern("HH:mm")))
+ .x(planSchedule.getX())
+ .y(planSchedule.getY())
+ .placeAddr(planSchedule.getPlaceAddr())
+ .placeCategory(planSchedule.getPlaceCategory())
+ .build();
+ }
+}
diff --git a/src/main/java/com/minizin/travel/plan/dto/EditPlanDto.java b/src/main/java/com/minizin/travel/plan/dto/EditPlanDto.java
new file mode 100644
index 0000000..2153b4c
--- /dev/null
+++ b/src/main/java/com/minizin/travel/plan/dto/EditPlanDto.java
@@ -0,0 +1,43 @@
+package com.minizin.travel.plan.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDate;
+import java.util.List;
+
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+@JsonPropertyOrder({"id", "userId", "planName", "theme", "startDate", "endDate"
+ , "scope", "numberOfMembers", "numberOfScraps", "schedules"})
+public class EditPlanDto {
+
+ private Long userId;
+
+ private String planName;
+
+ private String theme;
+
+ private String startDate;
+
+ private String endDate;
+
+ private boolean scope;
+
+ private int numberOfMembers;
+
+ private int numberOfScraps;
+
+ @JsonProperty("schedules")
+ private List planScheduleDtos;
+
+}
diff --git a/src/main/java/com/minizin/travel/plan/dto/OthersListPlanDto.java b/src/main/java/com/minizin/travel/plan/dto/OthersListPlanDto.java
new file mode 100644
index 0000000..cb3bbf5
--- /dev/null
+++ b/src/main/java/com/minizin/travel/plan/dto/OthersListPlanDto.java
@@ -0,0 +1,67 @@
+package com.minizin.travel.plan.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.plan.entity.Plan;
+import lombok.*;
+
+import java.time.LocalDate;
+import java.util.List;
+
+@Builder
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonPropertyOrder({"id", "userId", "userNickname", "planName", "theme", "startDate", "endDate"
+ , "planBudget", "scope", "numberOfMembers", "numberOfScraps", "regionList", "othersListPlanScheduleDtoList"})
+@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class OthersListPlanDto {
+
+ @JsonProperty("plan_id")
+ private Long id;
+
+ private Long userId;
+
+ @Setter
+ private String userNickname;
+
+ private String planName;
+
+ private String theme;
+
+ private LocalDate startDate;
+
+ private LocalDate endDate;
+
+ @Setter
+ private int planBudget;
+
+ private boolean scope;
+
+ private int numberOfMembers;
+
+ private int numberOfScraps;
+
+ @Setter
+ List regionList;
+
+ @Setter
+ @JsonProperty("schedules")
+ List othersListPlanScheduleDtoList;
+
+ public static OthersListPlanDto toDto(Plan plan) {
+ return OthersListPlanDto.builder()
+ .id(plan.getId())
+ .userId(plan.getUserId())
+ .planName(plan.getPlanName())
+ .theme(plan.getTheme())
+ .startDate(plan.getStartDate())
+ .endDate(plan.getEndDate())
+ .scope(plan.isScope())
+ .numberOfMembers(plan.getNumberOfMembers())
+ .numberOfScraps(plan.getNumberOfScraps())
+ .build();
+ }
+}
diff --git a/src/main/java/com/minizin/travel/plan/dto/OthersListPlanScheduleDto.java b/src/main/java/com/minizin/travel/plan/dto/OthersListPlanScheduleDto.java
new file mode 100644
index 0000000..003b92f
--- /dev/null
+++ b/src/main/java/com/minizin/travel/plan/dto/OthersListPlanScheduleDto.java
@@ -0,0 +1,53 @@
+package com.minizin.travel.plan.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.plan.entity.PlanSchedule;
+import lombok.Builder;
+import lombok.Getter;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+
+@Builder
+@Getter
+@JsonPropertyOrder({"id", "scheduleDate", "placeName", "region", "arrivalTime", "x", "y"
+ , "placeAddr", "placeCategory"})
+@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class OthersListPlanScheduleDto {
+
+ @JsonProperty("schedule_id")
+ private Long id;
+
+ private LocalDate scheduleDate;
+
+ private String placeName;
+
+ private String region;
+
+ private String arrivalTime;
+
+ private Double x;
+ private Double y;
+
+ private String placeAddr;
+
+ private String placeCategory;
+
+ public static OthersListPlanScheduleDto toDto(PlanSchedule planSchedule) {
+ return OthersListPlanScheduleDto.builder()
+ .id(planSchedule.getId())
+ .scheduleDate(planSchedule.getScheduleDate())
+ .placeName(planSchedule.getPlaceName())
+ .region(planSchedule.getRegion())
+ .arrivalTime(planSchedule.getArrivalTime().format(DateTimeFormatter.ofPattern("HH:mm")))
+ .x(planSchedule.getX())
+ .y(planSchedule.getY())
+ .placeAddr(planSchedule.getPlaceAddr())
+ .placeCategory(planSchedule.getPlaceCategory())
+ .build();
+ }
+}
diff --git a/src/main/java/com/minizin/travel/plan/dto/PlanBudgetDto.java b/src/main/java/com/minizin/travel/plan/dto/PlanBudgetDto.java
index ea31144..f06705c 100644
--- a/src/main/java/com/minizin/travel/plan/dto/PlanBudgetDto.java
+++ b/src/main/java/com/minizin/travel/plan/dto/PlanBudgetDto.java
@@ -2,9 +2,10 @@
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.plan.entity.PlanBudget;
import lombok.*;
-@Data
+@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@@ -15,6 +16,10 @@ public class PlanBudgetDto {
private int cost;
- private String budgetMemo;
-
+ public static PlanBudgetDto toDto(PlanBudget budget) {
+ return PlanBudgetDto.builder()
+ .budgetCategory(budget.getBudgetCategory())
+ .cost(budget.getCost())
+ .build();
+ }
}
diff --git a/src/main/java/com/minizin/travel/plan/dto/PlanDto.java b/src/main/java/com/minizin/travel/plan/dto/PlanDto.java
index 0189dda..5519fd0 100644
--- a/src/main/java/com/minizin/travel/plan/dto/PlanDto.java
+++ b/src/main/java/com/minizin/travel/plan/dto/PlanDto.java
@@ -4,40 +4,40 @@
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import jakarta.validation.constraints.Size;
import lombok.*;
+import org.hibernate.validator.constraints.Range;
import java.time.LocalDate;
import java.util.List;
@Getter
@Builder
-@AllArgsConstructor
@NoArgsConstructor
+@AllArgsConstructor
@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
-@JsonPropertyOrder({"id", "userId", "planName", "theme", "startDate", "endDate", "scope", "numberOfMembers", "numberOfLikes", "numberOfScraps", "waypoints", "scheduleDtos"})
+@JsonPropertyOrder({"userId", "planName", "theme", "startDate", "endDate", "scope", "numberOfMembers", "schedules"})
public class PlanDto {
private Long userId;
+ // #87 Request 예외/에러 처리
+ @Size(min = 2, max = 60, message = "'여행 일정 이름'은 2 ~ 60자여야 합니다.")
private String planName;
private String theme;
- private LocalDate startDate;
+ private String startDate;
- private LocalDate endDate;
+ private String endDate;
private boolean scope;
+ // #87 Request 예외/에러 처리
+ @Range(min = 1, max = 300, message = "'여행 인원'은 1 ~ 20명이어야 합니다.")
private int numberOfMembers;
- //private int numberOfLikes;
-
- //private int numberOfScraps;
-
- //private List waypoints;
-
- @JsonProperty("schedule")
+ @JsonProperty("schedules")
private List planScheduleDtos;
}
\ No newline at end of file
diff --git a/src/main/java/com/minizin/travel/plan/dto/PlanScheduleDto.java b/src/main/java/com/minizin/travel/plan/dto/PlanScheduleDto.java
index 53cc86e..5634c54 100644
--- a/src/main/java/com/minizin/travel/plan/dto/PlanScheduleDto.java
+++ b/src/main/java/com/minizin/travel/plan/dto/PlanScheduleDto.java
@@ -3,31 +3,37 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.plan.entity.PlanSchedule;
+import jakarta.validation.constraints.Size;
import lombok.*;
import java.time.LocalDate;
import java.util.List;
-@Data
+@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
public class PlanScheduleDto {
- private LocalDate scheduleDate;
+ @Setter
+ private String scheduleDate; // 직렬화, 역직렬화 문제로 수정
private String placeCategory;
+ @Size(min = 1, max = 40, message = "'장소 이름'의 길이는 1 ~ 40자입니다.")
private String placeName;
private String region;
+ @Size(max = 100, message = "'메모'의 길이는 최대 100자입니다.")
private String placeMemo;
private String arrivalTime;
- @JsonProperty("budget")
+ @JsonProperty("budgets")
+ @Setter
private List planBudgetDtos;
private Double x;
@@ -35,5 +41,19 @@ public class PlanScheduleDto {
// #29 2024.06.02 내 여행 일정 조회
private String placeAddr;
+
+ public static PlanScheduleDto toDto(PlanSchedule planSchedule) {
+ return PlanScheduleDto.builder()
+ .scheduleDate(String.valueOf(planSchedule.getScheduleDate()))
+ .placeCategory(planSchedule.getPlaceCategory())
+ .placeName(planSchedule.getPlaceName())
+ .region(planSchedule.getRegion())
+ .placeMemo(planSchedule.getPlaceMemo())
+ .arrivalTime(String.valueOf(planSchedule.getArrivalTime()))
+ .x(planSchedule.getX())
+ .y(planSchedule.getY())
+ .placeAddr(planSchedule.getPlaceAddr())
+ .build();
+ }
}
diff --git a/src/main/java/com/minizin/travel/plan/dto/PopWeekPlanDto.java b/src/main/java/com/minizin/travel/plan/dto/PopWeekPlanDto.java
new file mode 100644
index 0000000..9868220
--- /dev/null
+++ b/src/main/java/com/minizin/travel/plan/dto/PopWeekPlanDto.java
@@ -0,0 +1,58 @@
+package com.minizin.travel.plan.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.plan.entity.Plan;
+import lombok.*;
+
+import java.time.LocalDate;
+
+@Builder
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonPropertyOrder({"id", "planName","userId", "userNickname", "theme", "startDate"
+ ,"endDate", "startDate", "endDate", "planBudget", "numberOfMembers", "numberOfScraps"})
+@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class PopWeekPlanDto {
+
+ @JsonProperty("plan_id")
+ private Long id;
+
+ private String planName;
+
+ private Long userId;
+
+ @Setter
+ private String userNickname;
+
+ private String theme;
+
+ private LocalDate startDate;
+
+ private LocalDate endDate;
+
+ @Setter
+ private int planBudget;
+
+ private int numberOfMembers;
+
+ private int numberOfScraps;
+
+ public static PopWeekPlanDto toDto(Plan plan) {
+
+ return PopWeekPlanDto.builder()
+ .id(plan.getId())
+ .planName(plan.getPlanName())
+ .userId(plan.getUserId())
+ .theme(plan.getTheme())
+ .startDate(plan.getStartDate())
+ .endDate(plan.getEndDate())
+ .numberOfMembers(plan.getNumberOfMembers())
+ .numberOfScraps(plan.getNumberOfScraps())
+ .build();
+ }
+
+}
diff --git a/src/main/java/com/minizin/travel/plan/dto/ResponseDeletePlanDto.java b/src/main/java/com/minizin/travel/plan/dto/ResponseDeletePlanDto.java
new file mode 100644
index 0000000..ea2e89f
--- /dev/null
+++ b/src/main/java/com/minizin/travel/plan/dto/ResponseDeletePlanDto.java
@@ -0,0 +1,19 @@
+package com.minizin.travel.plan.dto;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Builder;
+import lombok.Getter;
+
+@Builder
+@Getter
+@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class ResponseDeletePlanDto {
+
+ private boolean success;
+
+ private String message;
+
+ private Long planId;
+
+}
diff --git a/src/main/java/com/minizin/travel/plan/dto/ResponseEditPlanDto.java b/src/main/java/com/minizin/travel/plan/dto/ResponseEditPlanDto.java
new file mode 100644
index 0000000..7feb266
--- /dev/null
+++ b/src/main/java/com/minizin/travel/plan/dto/ResponseEditPlanDto.java
@@ -0,0 +1,29 @@
+package com.minizin.travel.plan.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class ResponseEditPlanDto {
+
+ boolean success;
+ String message;
+
+ Long planId;
+
+ Integer numberOfScraps;
+
+ String createdAt;
+
+ String updatedAt;
+}
diff --git a/src/main/java/com/minizin/travel/plan/dto/ResponseListPlanDto.java b/src/main/java/com/minizin/travel/plan/dto/ResponseListPlanDto.java
deleted file mode 100644
index da914e8..0000000
--- a/src/main/java/com/minizin/travel/plan/dto/ResponseListPlanDto.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.minizin.travel.plan.dto;
-
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-
-import java.util.List;
-
-// #29 2024.06.02 내 여행 일정 조회 START //
-@Getter
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class ResponseListPlanDto {
-
- List data;
-
- Long nextCursor;
-}
-// #29 2024.06.02 내 여행 일정 조회 END //
\ No newline at end of file
diff --git a/src/main/java/com/minizin/travel/plan/dto/ResponseOthersPlanDto.java b/src/main/java/com/minizin/travel/plan/dto/ResponseOthersPlanDto.java
new file mode 100644
index 0000000..db57161
--- /dev/null
+++ b/src/main/java/com/minizin/travel/plan/dto/ResponseOthersPlanDto.java
@@ -0,0 +1,26 @@
+package com.minizin.travel.plan.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+// #48 2024.06.10 다른 사람 여행 일정 조회 START //
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class ResponseOthersPlanDto {
+
+ List data;
+
+ Long nextCursor;
+}
+// #48 2024.06.10 다른 사람 여행 일정 조회 END //
\ No newline at end of file
diff --git a/src/main/java/com/minizin/travel/plan/dto/ResponsePlanDto.java b/src/main/java/com/minizin/travel/plan/dto/ResponsePlanDto.java
index 36cdd0c..d64853a 100644
--- a/src/main/java/com/minizin/travel/plan/dto/ResponsePlanDto.java
+++ b/src/main/java/com/minizin/travel/plan/dto/ResponsePlanDto.java
@@ -1,29 +1,85 @@
package com.minizin.travel.plan.dto;
+import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
+import com.minizin.travel.plan.entity.Plan;
+import lombok.*;
-@Data
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
public class ResponsePlanDto {
- boolean success;
- String message;
+ private boolean success;
+ private String message;
+
+ private Long planId;
+
+ private Integer numberOfScraps; // #87 Request 예외/에러 처리
+
+ private String createAt;
+
+ private String updatedAt;
+
+ private Data data;
+
+ @Builder
+ @Getter
+ @JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+ public static class Data {
+
+ private String startDate;
+
+ private String endDate;
+
+ private String scheduleDate;
+ }
+
+ public static ResponsePlanDto success(Plan plan) {
+
+ return ResponsePlanDto.builder()
+ .success(true)
+ .message("일정을 생성하였습니다.")
+ .planId(plan.getId())
+ .numberOfScraps(plan.getNumberOfScraps())
+ .createAt(plan.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
+ .updatedAt(plan.getModifiedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
+ .build();
+ }
+
+ public static ResponsePlanDto fail(ResponsePlanDto.Data data) {
- Long planId;
+ return ResponsePlanDto.builder()
+ .success(false)
+ .message("날짜가 유효하지 않습니다.")
+ .data(data).build();
+ }
- int numberOfLikes;
+ public static ResponsePlanDto copySuccess(Plan plan) {
- int numberOfScraps;
+ return ResponsePlanDto.builder()
+ .success(true)
+ .message("일정을 복사하였습니다.")
+ .planId(plan.getId())
+ .numberOfScraps(plan.getNumberOfScraps())
+ .createAt(plan.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
+ .updatedAt(plan.getModifiedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
+ .build();
+ }
- String createAt;
+ public static ResponsePlanDto existsNot(Long planId) {
- String updatedAt;
+ return ResponsePlanDto.builder()
+ .success(false)
+ .message("요청하신 plan 은 존재하지 않습니다.")
+ .planId(planId)
+ .build();
+ }
}
diff --git a/src/main/java/com/minizin/travel/plan/dto/ResponseSelectListPlanDto.java b/src/main/java/com/minizin/travel/plan/dto/ResponseSelectListPlanDto.java
new file mode 100644
index 0000000..54f24cb
--- /dev/null
+++ b/src/main/java/com/minizin/travel/plan/dto/ResponseSelectListPlanDto.java
@@ -0,0 +1,36 @@
+package com.minizin.travel.plan.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+// #29 2024.06.02 내 여행 일정 조회 START //
+
+@Getter
+@Builder
+@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class ResponseSelectListPlanDto {
+
+ // success
+ private List data;
+
+ private Long nextCursor;
+
+ // fail
+ private Boolean success;
+
+ private String message;
+
+ private Long userId;
+
+}
+// #29 2024.06.02 내 여행 일정 조회 END //
\ No newline at end of file
diff --git a/src/main/java/com/minizin/travel/plan/dto/ListPlanDto.java b/src/main/java/com/minizin/travel/plan/dto/SelectListPlanDto.java
similarity index 75%
rename from src/main/java/com/minizin/travel/plan/dto/ListPlanDto.java
rename to src/main/java/com/minizin/travel/plan/dto/SelectListPlanDto.java
index 4b5913a..dc581e1 100644
--- a/src/main/java/com/minizin/travel/plan/dto/ListPlanDto.java
+++ b/src/main/java/com/minizin/travel/plan/dto/SelectListPlanDto.java
@@ -15,8 +15,9 @@
@AllArgsConstructor
@NoArgsConstructor
@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
-@JsonPropertyOrder({"id", "userId", "planName", "theme", "startDate", "endDate", "planBudget", "scope", "numberOfMembers", "numberOfLikes", "numberOfScraps", "waypoints", "responseScheduleListDtos"})
-public class ListPlanDto {
+@JsonPropertyOrder({"id", "userId", "planName", "theme", "startDate", "endDate"
+ , "planBudget", "scope", "numberOfMembers", "numberOfScraps", "regionList", "schedules"})
+public class SelectListPlanDto {
@JsonProperty("plan_id")
private Long id;
@@ -38,19 +39,17 @@ public class ListPlanDto {
private int numberOfMembers;
- private int numberOfLikes;
-
private int numberOfScraps;
@Setter
- private List waypoints;
+ private List regionList; // 방문 장소 경로 ex) [ 서울 -> 부산 -> 서울 ]
- @JsonProperty("schedule")
+ @JsonProperty("schedules")
@Setter
- private List listPlanScheduleDtoList;
+ private List listPlanScheduleDtoList;
- public static ListPlanDto toDto(Plan plan) {
- return ListPlanDto.builder()
+ public static SelectListPlanDto toDto(Plan plan) {
+ return SelectListPlanDto.builder()
.id(plan.getId())
.userId(plan.getUserId())
.planName(plan.getPlanName())
@@ -59,7 +58,6 @@ public static ListPlanDto toDto(Plan plan) {
.endDate(plan.getEndDate())
.scope(plan.isScope())
.numberOfMembers(plan.getNumberOfMembers())
- .numberOfLikes(plan.getNumberOfLikes())
.numberOfScraps(plan.getNumberOfScraps())
.build();
}
diff --git a/src/main/java/com/minizin/travel/plan/dto/ListPlanScheduleDto.java b/src/main/java/com/minizin/travel/plan/dto/SelectListPlanScheduleDto.java
similarity index 79%
rename from src/main/java/com/minizin/travel/plan/dto/ListPlanScheduleDto.java
rename to src/main/java/com/minizin/travel/plan/dto/SelectListPlanScheduleDto.java
index bbd8001..1719571 100644
--- a/src/main/java/com/minizin/travel/plan/dto/ListPlanScheduleDto.java
+++ b/src/main/java/com/minizin/travel/plan/dto/SelectListPlanScheduleDto.java
@@ -12,6 +12,7 @@
import java.time.LocalDate;
import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
// 2024.06.05 내 여행 일정 조회 //
@Builder
@@ -20,7 +21,7 @@
@AllArgsConstructor
@JsonPropertyOrder({"id", "scheduleDate", "placeName", "arrivalTime", "x", "y"})
@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
-public class ListPlanScheduleDto {
+public class SelectListPlanScheduleDto {
@JsonProperty("schedule_id")
private Long id;
@@ -29,7 +30,7 @@ public class ListPlanScheduleDto {
private String placeName;
- private LocalTime arrivalTime;
+ private String arrivalTime;
private Double x;
private Double y;
@@ -38,12 +39,12 @@ public class ListPlanScheduleDto {
private String placeCategory;
- public static ListPlanScheduleDto toDto(PlanSchedule planSchedule) {
- return ListPlanScheduleDto.builder()
+ public static SelectListPlanScheduleDto toDto(PlanSchedule planSchedule) {
+ return SelectListPlanScheduleDto.builder()
.id(planSchedule.getId())
.scheduleDate(planSchedule.getScheduleDate())
.placeName(planSchedule.getPlaceName())
- .arrivalTime(planSchedule.getArrivalTime())
+ .arrivalTime(planSchedule.getArrivalTime().format(DateTimeFormatter.ofPattern("HH:mm")))
.x(planSchedule.getX())
.y(planSchedule.getY())
.placeAddr(planSchedule.getPlaceAddr())
diff --git a/src/main/java/com/minizin/travel/plan/dto/UpcomingPlanDto.java b/src/main/java/com/minizin/travel/plan/dto/UpcomingPlanDto.java
new file mode 100644
index 0000000..6bb273c
--- /dev/null
+++ b/src/main/java/com/minizin/travel/plan/dto/UpcomingPlanDto.java
@@ -0,0 +1,49 @@
+package com.minizin.travel.plan.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.plan.entity.Plan;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDate;
+
+@Builder
+@Getter
+@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+@JsonPropertyOrder({"id", "userId", "planName", "theme", "startDate", "endDate", "planBudget", "numberOfMembers"})
+public class UpcomingPlanDto {
+
+ @JsonProperty("plan_id")
+ private Long id;
+
+ private Long userId;
+
+ private String planName;
+
+ private String theme;
+
+ private LocalDate startDate;
+
+ private LocalDate endDate;
+
+ @Setter
+ private int planBudget;
+
+ private int numberOfMembers;
+
+ public static UpcomingPlanDto toDto(Plan plan) {
+ return UpcomingPlanDto.builder()
+ .id(plan.getId())
+ .userId(plan.getUserId())
+ .planName(plan.getPlanName())
+ .theme(plan.getTheme())
+ .startDate(plan.getStartDate())
+ .endDate(plan.getEndDate())
+ .numberOfMembers(plan.getNumberOfMembers())
+ .build();
+ }
+}
diff --git a/src/main/java/com/minizin/travel/plan/entity/Plan.java b/src/main/java/com/minizin/travel/plan/entity/Plan.java
index 5bf74a8..8cbd273 100644
--- a/src/main/java/com/minizin/travel/plan/entity/Plan.java
+++ b/src/main/java/com/minizin/travel/plan/entity/Plan.java
@@ -40,9 +40,7 @@ public class Plan {
@Column(name = "number_of_members")
private int numberOfMembers;
- @Column(name = "number_of_likes")
- private int numberOfLikes;
-
+ @Setter
@Column(name = "number_of_scraps")
private int numberOfScraps;
diff --git a/src/main/java/com/minizin/travel/plan/entity/PlanBudget.java b/src/main/java/com/minizin/travel/plan/entity/PlanBudget.java
index 54fdd8c..28ef59c 100644
--- a/src/main/java/com/minizin/travel/plan/entity/PlanBudget.java
+++ b/src/main/java/com/minizin/travel/plan/entity/PlanBudget.java
@@ -27,9 +27,6 @@ public class PlanBudget {
private int cost;
- @Column(name = "budget_memo")
- private String budgetMemo;
-
@CreatedDate
@Column(name = "created_at")
private LocalDateTime createdAt;
diff --git a/src/main/java/com/minizin/travel/plan/repository/PlanBudgetRepository.java b/src/main/java/com/minizin/travel/plan/repository/PlanBudgetRepository.java
index 6d2ea9f..41b885c 100644
--- a/src/main/java/com/minizin/travel/plan/repository/PlanBudgetRepository.java
+++ b/src/main/java/com/minizin/travel/plan/repository/PlanBudgetRepository.java
@@ -10,4 +10,7 @@
public interface PlanBudgetRepository extends JpaRepository {
List findAllByScheduleId(Long scheduleId);
+
+ // #47 2024.06.13 내 여행 일정 삭제
+ void deleteByScheduleId(Long scheduleId);
}
diff --git a/src/main/java/com/minizin/travel/plan/repository/PlanRepository.java b/src/main/java/com/minizin/travel/plan/repository/PlanRepository.java
index 5646bc0..ea087a1 100644
--- a/src/main/java/com/minizin/travel/plan/repository/PlanRepository.java
+++ b/src/main/java/com/minizin/travel/plan/repository/PlanRepository.java
@@ -1,10 +1,15 @@
package com.minizin.travel.plan.repository;
import com.minizin.travel.plan.entity.Plan;
+import com.minizin.travel.plan.entity.PlanSchedule;
import org.springframework.data.domain.Pageable;
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.LocalDate;
+import java.time.LocalDateTime;
import java.util.List;
@Repository
@@ -14,4 +19,64 @@ public interface PlanRepository extends JpaRepository {
List findAllByUserIdOrderByIdDesc(Long userId, Pageable pageable);
+ boolean existsByIdLessThanAndUserId(Long id, Long userId);
+
+ boolean existsByIdAndUserId(Long id, Long userId);
+
+ // #39 2024.06.10 다가오는 여행 일정 조회 //
+ List findTop6ByUserIdAndStartDateGreaterThanEqualOrderByStartDateAscIdDesc(Long userId, LocalDate today);
+
+ // #48 2024.06.10 다른 사람 여행 일정 조회 START //
+ @Query("select DISTINCT p from Plan p, PlanSchedule s\n" +
+ "where p.id = s.planId and p.id < :lastPlanId\n" +
+ "and (:region IS NULL OR s.placeAddr like %:region%)\n" +
+ "and (:theme IS NULL OR p.theme = :theme )\n" +
+ "and (:search IS NULL OR (p.planName like %:search%\n" +
+ "OR s.placeAddr like %:search%\n" +
+ "OR s.placeName like %:search%))\n" +
+ "and p.scope is true order by p.id desc")
+ List findLessThanSearchAndThemeAndRegionOrderByIdDesc(
+ @Param("lastPlanId") Long lastPlanId, @Param("region") String region, @Param("theme") String theme, @Param("search") String search, Pageable pageable);
+
+ @Query("select DISTINCT p from Plan p, PlanSchedule s\n" +
+ "where p.id = s.planId\n" +
+ "and (:region IS NULL OR s.placeAddr like %:region%)\n" +
+ "and (:theme IS NULL OR p.theme = :theme )\n" +
+ "and (:search IS NULL OR (p.planName like %:search%\n" +
+ "OR s.placeAddr like %:search%\n" +
+ "OR s.placeName like %:search%))\n" +
+ "and p.scope is true order by p.id desc")
+ List findSearchAndThemeAndRegionOrderByIdDesc(
+ @Param("region") String region, @Param("theme") String theme, @Param("search") String search, Pageable pageable);
+ // #48 2024.06.10 다른 사람 여행 일정 조회 END //
+
+ // #58 2024.06.12 다른 사람 여행 일정 조회(북마크순) START //
+ @Query("select DISTINCT p from Plan p, PlanSchedule s\n" +
+ "where p.id = s.planId and p.id < :lastPlanId\n" +
+ "and (:region IS NULL OR s.placeAddr like %:region%)\n" +
+ "and (:theme IS NULL OR p.theme = :theme )\n" +
+ "and (:search IS NULL OR (p.planName like %:search%\n" +
+ "OR s.placeAddr like %:search%\n" +
+ "OR s.placeName like %:search%))\n" +
+ "and p.scope is true order by p.numberOfScraps desc, p.id desc")
+ List findLessThanSearchAndThemeAndRegionOrderByNumberOfScrapsDescIdDesc(
+ @Param("lastPlanId") Long lastPlanId, @Param("region") String region, @Param("theme") String theme, @Param("search") String search, Pageable pageable);
+
+ @Query("select DISTINCT p from Plan p, PlanSchedule s\n" +
+ "where p.id = s.planId\n" +
+ "and (:region IS NULL OR s.placeAddr like %:region%)\n" +
+ "and (:theme IS NULL OR p.theme = :theme )\n" +
+ "and (:search IS NULL OR (p.planName like %:search%\n" +
+ "OR s.placeAddr like %:search%\n" +
+ "OR s.placeName like %:search%))\n" +
+ "and p.scope is true order by p.numberOfScraps desc, p.id desc")
+ List findSearchAndThemeAndRegionOrderByNumberOfScrapsDescIdDesc(
+ @Param("region") String region, @Param("theme") String theme, @Param("search") String search, Pageable pageable);
+ // #58 2024.06.12 다른 사람 여행 일정 조회(북마크순) END //
+
+ // #129 금주 인기 여행
+ //List findTop20ByStartDateBetweenOrderByNumberOfScrapsDescIdDesc(LocalDate startDate, LocalDate endDate);
+
+ List findTop20ByCreatedAtBetweenOrderByNumberOfScrapsDescIdDesc(LocalDateTime startDate, LocalDateTime endDate);
+
}
diff --git a/src/main/java/com/minizin/travel/plan/repository/PlanScheduleRepository.java b/src/main/java/com/minizin/travel/plan/repository/PlanScheduleRepository.java
index 5aa6249..3f4c6c0 100644
--- a/src/main/java/com/minizin/travel/plan/repository/PlanScheduleRepository.java
+++ b/src/main/java/com/minizin/travel/plan/repository/PlanScheduleRepository.java
@@ -10,4 +10,7 @@
public interface PlanScheduleRepository extends JpaRepository {
List findAllByPlanId(Long planId);
+
+ // #47 2024.06.13 내 여행 일정 삭제
+ void deleteByPlanId(Long planId);
}
diff --git a/src/main/java/com/minizin/travel/plan/service/OtherPlanService.java b/src/main/java/com/minizin/travel/plan/service/OtherPlanService.java
new file mode 100644
index 0000000..13101bb
--- /dev/null
+++ b/src/main/java/com/minizin/travel/plan/service/OtherPlanService.java
@@ -0,0 +1,195 @@
+package com.minizin.travel.plan.service;
+
+import com.minizin.travel.plan.dto.*;
+import com.minizin.travel.plan.entity.Plan;
+import com.minizin.travel.plan.entity.PlanSchedule;
+import com.minizin.travel.plan.repository.PlanRepository;
+import com.minizin.travel.plan.repository.PlanScheduleRepository;
+import com.minizin.travel.user.domain.repository.UserRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.thymeleaf.util.StringUtils;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class OtherPlanService {
+
+ private final PlanService planService;
+ private final PlanRepository planRepository;
+ private final PlanScheduleRepository planScheduleRepository;
+
+ final int DEFAULT_PAGE_SIZE = 6; // #29
+
+ private final UserRepository userRepository;
+
+ // #48 2024.06.10 다른 사람 여행 일정 조회 START //
+ public ResponseOthersPlanDto selectOthersListPlan(
+ Long lastPlanId, String region, String theme, String search) {
+
+ Pageable page = PageRequest.of(0, DEFAULT_PAGE_SIZE);
+
+ List planList = findOtherPlanByLastPlanIdCheckExistCursor(region, theme, search, lastPlanId, page);
+ List othersListPlanDtoList = new ArrayList<>();
+ Long planId = 0L;
+
+ for (Plan plan : planList) {
+ planId = plan.getId();
+ othersListPlanDtoList.add(selectOthersPlanAndSchedule(plan));
+ }
+
+ if (findOtherPlanByLastPlanIdCheckExistCursor(region, theme, search, planId, page).isEmpty()) {
+
+ planId = null;
+ }
+
+ return ResponseOthersPlanDto.builder()
+ .data(othersListPlanDtoList)
+ .nextCursor(planId)
+ .build();
+ }
+
+ // #58 2024.06.12 다른 사람 여행 일정 조회(북마크순) START //
+ public ResponseOthersPlanDto selectOthersListPlanScraps(
+ Long lastPlanId, String region, String theme, String search) {
+
+ Pageable page = PageRequest.of(0, DEFAULT_PAGE_SIZE);
+
+ List planList = findOtherPlanScrapsByLastPlanIdCheckExistCursor(region, theme, search, lastPlanId, page);
+ List othersListPlanDtoList = new ArrayList<>();
+ Long planId = 0L;
+
+ for (Plan plan : planList) {
+ planId = plan.getId();
+ othersListPlanDtoList.add(selectOthersPlanAndSchedule(plan));
+ }
+
+ if (findOtherPlanScrapsByLastPlanIdCheckExistCursor(region, theme, search, planId, page).isEmpty()) {
+ planId = null;
+ }
+
+ return ResponseOthersPlanDto.builder()
+ .data(othersListPlanDtoList)
+ .nextCursor(planId)
+ .build();
+ }
+ // #58 2024.06.12 다른 사람 여행 일정 조회(북마크순) END //
+
+/*
+ private List selectOthersListPlan(List planList) {
+
+ List othersListPlanDtoList = new ArrayList<>();
+ Long planId = 0L;
+
+ for (Plan plan : planList) {
+ planId = plan.getId();
+ othersListPlanDtoList.add(selectOthersPlanAndSchedule(plan));
+ }
+
+ return othersListPlanDtoList;
+ }
+ */
+
+ private OthersListPlanDto selectOthersPlanAndSchedule(Plan plan) {
+
+ List planScheduleList = planScheduleRepository.findAllByPlanId(plan.getId());
+ List othersListPlanScheduleDtoList = new ArrayList<>();
+ List regionList = new ArrayList<>();
+
+ for (PlanSchedule planSchedule : planScheduleList) {
+ othersListPlanScheduleDtoList.add(OthersListPlanScheduleDto.toDto(planSchedule));
+ regionList.add(planSchedule.getRegion());
+ }
+
+ OthersListPlanDto othersListPlanDto = OthersListPlanDto.toDto(plan);
+ othersListPlanDto.setUserNickname(userRepository.findById(plan.getUserId()).get().getNickname());
+ othersListPlanDto.setRegionList(planService.duplicateRegionList(regionList));
+ othersListPlanDto.setPlanBudget(planService.calculateTotalPlanBudget(planScheduleList));
+ othersListPlanDto.setOthersListPlanScheduleDtoList(othersListPlanScheduleDtoList);
+
+ return othersListPlanDto;
+ }
+
+ private List findOtherPlanByLastPlanIdCheckExistCursor(String region, String theme, String search, Long lastPlanId, Pageable page) {
+
+ if (StringUtils.isEmpty(region)) {
+ region = null;
+ }
+ if (StringUtils.isEmpty(theme)) {
+ theme = null;
+ }
+ if (StringUtils.isEmpty(search)) {
+ search = null;
+ }
+
+ return lastPlanId == 0 ? planRepository.findSearchAndThemeAndRegionOrderByIdDesc(region, theme, search, page)
+ : planRepository.findLessThanSearchAndThemeAndRegionOrderByIdDesc(lastPlanId, region, theme, search, page);
+ }
+ // #48 2024.06.10 다른 사람 여행 일정 조회 END //
+
+ // #58 2024.06.12 다른 사람 여행 일정 조회(북마크순) START //
+ private List findOtherPlanScrapsByLastPlanIdCheckExistCursor(String region, String theme, String search, Long lastPlanId, Pageable page) {
+
+ if (StringUtils.isEmpty(region)) {
+ region = null;
+ }
+ if (StringUtils.isEmpty(theme)) {
+ theme = null;
+ }
+ if (StringUtils.isEmpty(search)) {
+ search = null;
+ }
+ return lastPlanId == 0? planRepository.findSearchAndThemeAndRegionOrderByNumberOfScrapsDescIdDesc(region, theme, search, page)
+ : planRepository.findLessThanSearchAndThemeAndRegionOrderByNumberOfScrapsDescIdDesc(lastPlanId, region, theme, search, page);
+ }
+ // #58 2024.06.12 다른 사람 여행 일정 조회(북마크순) END //
+
+ // #106 2024.06.16 금주 인기 여행 일정 조회(북마크순) START //
+ public List selectPopularPlansWeek() {
+
+ List popWeekPlanDtoList = new ArrayList<>();
+
+ LocalDateTime endDate = LocalDateTime.now();
+ LocalDateTime startDate = endDate.minusDays(6);
+ List planList = planRepository.findTop20ByCreatedAtBetweenOrderByNumberOfScrapsDescIdDesc(startDate, endDate);
+
+ for (Plan plan : planList) {
+ PopWeekPlanDto newPopWeekPlanDto = PopWeekPlanDto.toDto(plan);
+ List planScheduleList = planScheduleRepository.findAllByPlanId(plan.getId());
+ newPopWeekPlanDto.setPlanBudget(planService.calculateTotalPlanBudget(planScheduleList));
+ newPopWeekPlanDto.setUserNickname(userRepository.findById(plan.getUserId()).get().getNickname());
+ popWeekPlanDtoList.add(newPopWeekPlanDto);
+ }
+
+ return popWeekPlanDtoList;
+ }
+ // #106 2024.06.16 금주 인기 여행 일정 조회(북마크순) END //
+
+ // #129 다른 사람의 여행 일정 상세 보기 START //
+ public DetailPlanDto selectOtherDetailPlan(Long planId) {
+
+ if (!planRepository.existsById(planId)) {
+ return DetailPlanDto.existsNot(planId);
+ }
+
+ Plan plan = planRepository.findById(planId).get();
+
+ if (!plan.isScope()) {
+ return DetailPlanDto.notAuth(planId, "비공개 plan 입니다.");
+ }
+
+ DetailPlanDto detailPlanDto = planService.findListOfPlanScheduleDtoAndPlanBudgetDto(plan);
+ detailPlanDto.setUserNickname(userRepository.findById(plan.getUserId()).get().getNickname());
+
+ return detailPlanDto;
+ }
+ // #129 다른 사람의 여행 일정 상세 보기 END //
+}
diff --git a/src/main/java/com/minizin/travel/plan/service/PlanService.java b/src/main/java/com/minizin/travel/plan/service/PlanService.java
index b9be39f..b012fde 100644
--- a/src/main/java/com/minizin/travel/plan/service/PlanService.java
+++ b/src/main/java/com/minizin/travel/plan/service/PlanService.java
@@ -7,16 +7,21 @@
import com.minizin.travel.plan.repository.PlanBudgetRepository;
import com.minizin.travel.plan.repository.PlanRepository;
import com.minizin.travel.plan.repository.PlanScheduleRepository;
+import com.minizin.travel.user.domain.dto.PrincipalDetails;
+import com.minizin.travel.user.domain.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.apache.coyote.BadRequestException;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
@@ -28,22 +33,82 @@ public class PlanService {
private final PlanRepository planRepository;
private final PlanScheduleRepository planScheduleRepository;
private final PlanBudgetRepository planBudgetRepository;
+ private final UserRepository userRepository;
final int INITIAL_VALUE = 0;
final int DEFAULT_PAGE_SIZE = 6; // #29
// #28 2024.05.30 내 여행 일정 생성하기 START //
- public ResponsePlanDto createPlan(PlanDto planDto) {
+ public ResponsePlanDto createPlan(PlanDto planDto, PrincipalDetails user)
+ throws BadRequestException {
+
+ // user 정보 확인
+ Long userId = userRepository.findByUsername(user.getUsername()).get().getId();
+
+ // #87 Request 예외/에러 처리 START
+ // 여행 일자 최소 1일 ~ 최대 60일 START //
+ LocalDate startDate = LocalDate.parse(planDto.getStartDate());
+ LocalDate endDate = LocalDate.parse(planDto.getEndDate());
+ int planDays = (int) ChronoUnit.DAYS.between(startDate, endDate);
+ if (planDays < 0 || planDays > 60) {
+ System.out.println("여행의 날짜는 최소 1일 ~ 최대 60일이어야 합니다.");
+ throw new BadRequestException();
+ }
+
+ // 일정당 장소 최소 1개
+ String curDate = "";
+ int scheduleCnt = 0;
+ for (PlanScheduleDto planScheduleDto : planDto.getPlanScheduleDtos()) {
+ if (curDate.equals(planScheduleDto.getScheduleDate())) {
+ scheduleCnt++;
+ if (scheduleCnt > 40) {
+ System.out.println("일정의 개수는 하루당 40개를 넘을 수 없습니다.");
+ throw new BadRequestException();
+ }
+ } else {
+ curDate = planScheduleDto.getScheduleDate();
+ scheduleCnt = 1;
+ }
+ }
+
+ // 예산 ~ 100,000,000원
+ int totalPlanBudget = 0;
+ for (PlanScheduleDto planScheduleDto : planDto.getPlanScheduleDtos()) {
+
+ if (planScheduleDto.getPlanBudgetDtos() != null) { // #87 Request 예외/에러 처리
+ for (PlanBudgetDto planBudgetDto : planScheduleDto.getPlanBudgetDtos()) {
+ totalPlanBudget += planBudgetDto.getCost();
+ }
+ }
+ }
+ if (totalPlanBudget > 100000000) {
+ throw new BadRequestException();
+ }
+
+ // schedule 날짜가 유효하지 않은 경우
+ for (PlanScheduleDto planScheduleDto : planDto.getPlanScheduleDtos()) {
+ LocalDate scheduleDate = LocalDate.parse(planScheduleDto.getScheduleDate());
+ if (scheduleDate.isBefore(startDate)
+ || scheduleDate.isAfter(endDate)) {
+
+ return ResponsePlanDto.fail(
+ ResponsePlanDto.Data.builder()
+ .startDate(planDto.getStartDate())
+ .endDate(planDto.getEndDate())
+ .scheduleDate(planScheduleDto.getScheduleDate())
+ .build());
+ }
+ }
+ // #87 Request 예외/에러 처리 END //
Plan newPlan = planRepository.save(Plan.builder()
- .userId(planDto.getUserId())
+ .userId(userId)
.planName(planDto.getPlanName())
.theme(planDto.getTheme())
- .startDate(planDto.getStartDate())
- .endDate(planDto.getEndDate())
+ .startDate(startDate)
+ .endDate(endDate)
.scope(planDto.isScope())
.numberOfMembers(planDto.getNumberOfMembers())
- .numberOfLikes(INITIAL_VALUE)
.numberOfScraps(INITIAL_VALUE)
.createdAt(LocalDateTime.now())
.modifiedAt(LocalDateTime.now())
@@ -52,37 +117,30 @@ public ResponsePlanDto createPlan(PlanDto planDto) {
Long planId = newPlan.getId();
for (PlanScheduleDto planScheduleDto : planDto.getPlanScheduleDtos()) {
- Long scheduleId = createPlanSchedule(planScheduleDto, planId).getId();
+ Long scheduleId = createPlanSchedule(planScheduleDto, planId).getId();
- for (PlanBudgetDto planBudgetDto : planScheduleDto.getPlanBudgetDtos()) {
- createPlanBudget(planBudgetDto, scheduleId);
+ if (planScheduleDto.getPlanBudgetDtos() != null) { // #87 Request 예외/에러 처리
+ for (PlanBudgetDto planBudgetDto : planScheduleDto.getPlanBudgetDtos()) {
+ createPlanBudget(planBudgetDto, scheduleId);
+ }
}
}
- ResponsePlanDto newResponsePlanDto = ResponsePlanDto.builder()
- .success(true)
- .message("일정을 생성하였습니다.")
- .planId(planId)
- .numberOfLikes(newPlan.getNumberOfLikes())
- .numberOfScraps(newPlan.getNumberOfScraps())
- .createAt(newPlan.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
- .updatedAt(newPlan.getModifiedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
- .build();
+ return ResponsePlanDto.success(newPlan); // #87 Request 예외/에러 처리 : Response success 코드 정리
- return newResponsePlanDto;
}
public PlanSchedule createPlanSchedule(PlanScheduleDto planScheduleDto, Long planId) {
return planScheduleRepository.save(PlanSchedule.builder()
.planId(planId)
- .scheduleDate(planScheduleDto.getScheduleDate())
+ .scheduleDate(LocalDate.parse(planScheduleDto.getScheduleDate()))
.placeCategory(planScheduleDto.getPlaceCategory())
.placeName(planScheduleDto.getPlaceName())
.placeAddr(planScheduleDto.getPlaceAddr()) // #29 2024.06.02 내 여행 일정 조회
.region(planScheduleDto.getRegion())
.placeMemo(planScheduleDto.getPlaceMemo())
- .arrivalTime(LocalTime.parse(planScheduleDto.getArrivalTime(), DateTimeFormatter.ofPattern("HH:mm:ss")))
+ .arrivalTime(LocalTime.parse(planScheduleDto.getArrivalTime(), DateTimeFormatter.ofPattern("HH:mm")))
.x(planScheduleDto.getX())
.y(planScheduleDto.getY())
.createdAt(LocalDateTime.now())
@@ -96,7 +154,6 @@ public PlanBudget createPlanBudget(PlanBudgetDto planBudgetDto, Long scheduleId)
.scheduleId(scheduleId)
.budgetCategory(planBudgetDto.getBudgetCategory())
.cost(planBudgetDto.getCost())
- .budgetMemo(planBudgetDto.getBudgetMemo())
.createdAt(LocalDateTime.now())
.modifiedAt(LocalDateTime.now())
.build());
@@ -104,71 +161,75 @@ public PlanBudget createPlanBudget(PlanBudgetDto planBudgetDto, Long scheduleId)
// #28 2024.05.30 내 여행 일정 생성하기 END //
// #29 2024.06.02 내 여행 일정 조회 START //
- public ResponseListPlanDto selectListPlan(Long cursorId) {
+ public ResponseSelectListPlanDto selectListPlan(Long lastPlanId, PrincipalDetails user) {
Pageable page = PageRequest.of(0, DEFAULT_PAGE_SIZE);
- // 테스트
- Long userId = 1L;
+ Long userId = userRepository.findByUsername(user.getUsername()).get().getId();
- List planList = findAllByCursorIdCheckExistCursor(userId, cursorId, page);
- List listPlanDtoList = new ArrayList<>();
+ List planList = findAllByLastPlanIdCheckExistCursor(userId, lastPlanId, page);
+ List listPlanDtoList = new ArrayList<>();
Long planId = 0L;
for (Plan plan : planList) {
planId = plan.getId();
List planScheduleList = planScheduleRepository.findAllByPlanId(planId);
- List listPlanScheduleDtoList = new ArrayList<>();
- List waypoints = new ArrayList<>();
+ List listPlanScheduleDtoList = new ArrayList<>();
+ List regionList = new ArrayList<>();
int totalBudget = calculateTotalPlanBudget(planScheduleList); // #44 여행 일정 예산 추가
for (PlanSchedule planSchedule : planScheduleList) {
- ListPlanScheduleDto newResponseScheduleDto = ListPlanScheduleDto.toDto(planSchedule);
- listPlanScheduleDtoList.add(newResponseScheduleDto);
- waypoints.add(planSchedule.getRegion());
+ listPlanScheduleDtoList.add(SelectListPlanScheduleDto.toDto(planSchedule));
+ regionList.add(planSchedule.getRegion());
}
- ListPlanDto newPlanDto = ListPlanDto.toDto(plan);
- newPlanDto.setPlanBudget(totalBudget); // #44 여행 일정 예산 추가
- newPlanDto.setListPlanScheduleDtoList(listPlanScheduleDtoList);
- newPlanDto.setWaypoints(duplicateWaypoints(waypoints));
- listPlanDtoList.add(newPlanDto);
+
+ SelectListPlanDto selectListPlanDto = SelectListPlanDto.toDto(plan);
+ selectListPlanDto.setPlanBudget(totalBudget); // #44 여행 일정 예산 추가
+ selectListPlanDto.setListPlanScheduleDtoList(listPlanScheduleDtoList);
+ selectListPlanDto.setRegionList(duplicateRegionList(regionList));
+ listPlanDtoList.add(selectListPlanDto);
}
- return ResponseListPlanDto.builder()
+ if (!planRepository.existsByIdLessThanAndUserId(planId, userId)) {
+ planId = null;
+ }
+
+ return ResponseSelectListPlanDto.builder()
.data(listPlanDtoList)
.nextCursor(planId)
.build();
}
- private List duplicateWaypoints(List waypoints) {
+ public List duplicateRegionList(List regionList) {
- if (waypoints.size() <= 1) {
- return waypoints;
+ if (regionList.size() <= 1) {
+ return regionList;
}
- List newWaypoints = new ArrayList<>();
- newWaypoints.add(waypoints.get(0));
+ List newRegionList = new ArrayList<>();
+ newRegionList.add(regionList.get(0));
- for (int i = 1; i < waypoints.size(); i++) {
- if (waypoints.get(i - 1).equals(waypoints.get(i))) {
+ // [ 서울 -> 서울 -> 부산 ] 인 경우 [ 서울 -> 부산 ] 으로 중복 제거
+ for (int i = 1; i < regionList.size(); i++) {
+ if (regionList.get(i - 1).equals(regionList.get(i))) {
continue;
}
- newWaypoints.add(waypoints.get(i));
+ newRegionList.add(regionList.get(i));
}
- return newWaypoints;
+ return newRegionList;
}
- private List findAllByCursorIdCheckExistCursor(Long userId, Long cursorId, Pageable page) {
+ private List findAllByLastPlanIdCheckExistCursor(Long userId, Long lastPlanId, Pageable page) {
- return cursorId == 0 ? planRepository.findAllByUserIdOrderByIdDesc(userId, page)
- : planRepository.findByIdLessThanAndUserIdOrderByIdDesc(cursorId, userId, page);
+ return lastPlanId == 0 ? planRepository.findAllByUserIdOrderByIdDesc(userId, page)
+ : planRepository.findByIdLessThanAndUserIdOrderByIdDesc(lastPlanId, userId, page);
}
// #29 2024.06.02 내 여행 일정 조회 END //
// #44 2024.06.12 여행 일정 예산 계산하기 START //
- private int calculateTotalPlanBudget(List planScheduleList) {
+ public int calculateTotalPlanBudget(List planScheduleList) {
int totalPlanBudget = 0;
@@ -183,4 +244,224 @@ private int calculateTotalPlanBudget(List planScheduleList) {
return totalPlanBudget;
}
// #44 2024.06.12 여행 일정 예산 계산하기 END //
+
+ // #32 2024.06.07 내 여행 일정 수정 START //
+ @Transactional
+ public ResponseEditPlanDto updatePlan(Long planId, EditPlanDto editPlanDto, PrincipalDetails user) {
+
+ if (!planRepository.existsById(planId)) {
+ return ResponseEditPlanDto.builder()
+ .success(false)
+ .message("요청하신 plan 은 존재하지 않습니다.")
+ .planId(planId)
+ .build();
+ }
+
+ /* 추후 PUT요청에 id값이 포함되면 변경 예정 */
+
+ // 기존 PlanSchedule 및 PlanBudget 삭제
+ List planScheduleList = planScheduleRepository.findAllByPlanId(planId);
+ for (PlanSchedule planSchedule : planScheduleList) {
+ planBudgetRepository.deleteByScheduleId(planSchedule.getId());
+ }
+ planScheduleRepository.deleteByPlanId(planId);
+
+ // plan id 로 plan DB 찾아오기
+ // plan DB 에 저장
+ Plan plan = planRepository.findById(planId).get();
+
+ Long userId = userRepository.findByUsername(user.getUsername()).get().getId();
+ if (!plan.getUserId().equals(userId)) {
+ return ResponseEditPlanDto.builder()
+ .success(false)
+ .message("로그인한 사용자의 id가 아닙니다.")
+ .planId(planId)
+ .build();
+ }
+
+ Plan newPlan = planRepository.save(Plan.builder()
+ .id(planId)
+ .userId(plan.getUserId())
+ .planName(editPlanDto.getPlanName())
+ .theme(editPlanDto.getTheme())
+ .startDate(LocalDate.parse(editPlanDto.getStartDate()))
+ .endDate(LocalDate.parse(editPlanDto.getEndDate()))
+ .scope(editPlanDto.isScope())
+ .numberOfMembers(editPlanDto.getNumberOfMembers())
+ .numberOfScraps(plan.getNumberOfScraps())
+ .createdAt(plan.getCreatedAt())
+ .modifiedAt(LocalDateTime.now())
+ .build());
+
+ for (PlanScheduleDto planScheduleDto : editPlanDto.getPlanScheduleDtos()) {
+ Long scheduleId = createPlanSchedule(planScheduleDto, planId).getId();
+
+ for (PlanBudgetDto planBudgetDto : planScheduleDto.getPlanBudgetDtos()) {
+ createPlanBudget(planBudgetDto, scheduleId);
+ }
+ }
+
+ return ResponseEditPlanDto.builder()
+ .success(true)
+ .message("일정을 수정하였습니다.")
+ .planId(planId)
+ .createdAt(newPlan.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
+ .updatedAt(newPlan.getModifiedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
+ .numberOfScraps(newPlan.getNumberOfScraps())
+ .build();
+ }
+ // #32 2024.06.07 내 여행 일정 수정 END //
+
+ // #38 2024.06.08 내 여행 일정 상세 보기 START //
+ public DetailPlanDto selectDetailPlan(Long planId, PrincipalDetails user) {
+
+ if (!planRepository.existsById(planId)) {
+ return DetailPlanDto.existsNot(planId);
+ }
+
+ Plan plan = planRepository.findById(planId).get();
+ Long userId = userRepository.findByUsername(user.getUsername()).get().getId();
+ if (!plan.getUserId().equals(userId)) {
+ return DetailPlanDto.notAuth(planId, "로그인한 사용자의 plan 이 아닙니다.");
+ }
+
+ return findListOfPlanScheduleDtoAndPlanBudgetDto(plan);
+ }
+
+ public DetailPlanDto findListOfPlanScheduleDtoAndPlanBudgetDto(Plan plan) {
+
+ List planScheduleList = planScheduleRepository.findAllByPlanId(plan.getId()); // DB를 가져온 list
+ List detailPlanScheduleDtoList = new ArrayList<>(); // 객체를 가져온 list
+ List regionList = new ArrayList<>();
+ int days = 0;
+
+ for (PlanSchedule planSchedule : planScheduleList) {
+ List planBudgetList = planBudgetRepository.findAllByScheduleId(planSchedule.getId());
+ List detailPlanBudgetDtoList = new ArrayList<>();
+
+ for (PlanBudget planBudget : planBudgetList) {
+ detailPlanBudgetDtoList.add(DetailPlanBudgetDto.toDto(planBudget));
+ }
+
+ DetailPlanScheduleDto detailPlanScheduleDto = DetailPlanScheduleDto.toDto(planSchedule);
+ regionList.add(planSchedule.getRegion());
+ detailPlanScheduleDto.setScheduleDays((int) ChronoUnit.DAYS.between(plan.getStartDate(), detailPlanScheduleDto.getScheduleDate()) + 1);
+ detailPlanScheduleDto.setDetailPlanBudgetDtoList(detailPlanBudgetDtoList);
+ detailPlanScheduleDtoList.add(detailPlanScheduleDto);
+ }
+
+ DetailPlanDto detailPlanDto = DetailPlanDto.toDto(plan);
+ detailPlanDto.setPlanBudget(calculateTotalPlanBudget(planScheduleList));
+ detailPlanDto.setRegionList(duplicateRegionList(regionList));
+ detailPlanDto.setDetailPlanScheduleDtoList(detailPlanScheduleDtoList);
+
+ return detailPlanDto;
+ }
+ // #38 2024.06.08 내 여행 일정 상세 보기 END //
+
+ // #47 2024.06.13 내 여행 일정 삭제 START //
+ @Transactional
+ public ResponseDeletePlanDto deletePlan(Long planId, PrincipalDetails user) {
+
+ if (!planRepository.existsById(planId)) {
+ return ResponseDeletePlanDto.builder()
+ .success(false)
+ .message("여행 일정 id가 유효하지 않습니다.")
+ .planId(planId)
+ .build();
+ }
+
+ Plan plan = planRepository.findById(planId).get();
+ Long userId = userRepository.findByUsername(user.getUsername()).get().getId();
+ if (!plan.getUserId().equals(userId)) {
+ return ResponseDeletePlanDto.builder()
+ .success(false)
+ .message("로그인한 사용자의 plan 이 아닙니다.")
+ .planId(planId)
+ .build();
+ }
+
+ List planScheduleList = planScheduleRepository.findAllByPlanId(planId);
+ for (PlanSchedule planSchedule : planScheduleList) {
+ planBudgetRepository.deleteByScheduleId(planSchedule.getId());
+ }
+ planScheduleRepository.deleteByPlanId(planId);
+
+ planRepository.deleteById(planId);
+
+ return ResponseDeletePlanDto.builder()
+ .success(true)
+ .message("일정을 삭제하였습니다.")
+ .planId(planId)
+ .build();
+ }
+ // #47 2024.06.13 내 여행 일정 삭제 END //
+
+ // #39 2024.06.10 다가오는 여행 일정 조회 START //
+ public List selectUpcomingPlan(PrincipalDetails user) {
+
+ Long userId = userRepository.findByUsername(user.getUsername()).get().getId();
+
+ List upcomingPlanDtoList = new ArrayList<>();
+ List planList = planRepository.findTop6ByUserIdAndStartDateGreaterThanEqualOrderByStartDateAscIdDesc(userId, LocalDate.now());
+
+ for (Plan plan : planList) {
+ List planScheduleList = planScheduleRepository.findAllByPlanId(plan.getId());
+
+ UpcomingPlanDto upcomingPlanDto = UpcomingPlanDto.toDto(plan);
+ upcomingPlanDto.setPlanBudget(calculateTotalPlanBudget(planScheduleList));
+ upcomingPlanDtoList.add(upcomingPlanDto);
+ }
+
+ return upcomingPlanDtoList;
+ }
+ // #39 2024.06.10 다가오는 여행 일정 조회 END //
+
+ // #107 2024.06.20 일정 복사하기 START //
+ public ResponsePlanDto copyAndCreatePlan(Long planId, PrincipalDetails user) {
+
+ if (!planRepository.existsById(planId)) {
+ return ResponsePlanDto.existsNot(planId);
+ }
+
+ Long userId = userRepository.findByUsername(user.getUsername()).get().getId();
+
+ Plan plan = planRepository.findById(planId).get();
+
+ int planDays = (int) ChronoUnit.DAYS.between(plan.getStartDate(), plan.getEndDate());
+ LocalDate endDate = LocalDate.now().plusDays(planDays);
+ Plan newPlan = planRepository.save(Plan.builder()
+ .userId(userId)
+ .planName(plan.getPlanName())
+ .theme(plan.getTheme())
+ .startDate(LocalDate.now())
+ .endDate(endDate)
+ .scope(plan.isScope())
+ .numberOfMembers(plan.getNumberOfMembers())
+ .numberOfScraps(INITIAL_VALUE)
+ .createdAt(LocalDateTime.now())
+ .modifiedAt(LocalDateTime.now())
+ .build());
+
+ // 기존 스케줄 찾기
+ List planScheduleList = planScheduleRepository.findAllByPlanId(planId);
+ planDays = (int) ChronoUnit.DAYS.between(plan.getStartDate(), LocalDate.now());
+
+ for (PlanSchedule planSchedule : planScheduleList) {
+ PlanScheduleDto planScheduleDto = PlanScheduleDto.toDto(planSchedule);
+ planScheduleDto.setScheduleDate(String.valueOf(LocalDate.parse(planScheduleDto.getScheduleDate()).plusDays(planDays)));
+ Long scheduleId = createPlanSchedule(planScheduleDto, newPlan.getId()).getId();
+
+ // 기존 예산 찾기
+ List planBudgetList = planBudgetRepository.findAllByScheduleId(planSchedule.getId());
+ // 새 예산 넣기
+ for (PlanBudget planBudget : planBudgetList) {
+ createPlanBudget(PlanBudgetDto.toDto(planBudget), scheduleId);
+ }
+ }
+
+ return ResponsePlanDto.copySuccess(newPlan); // #87 Request 예외/에러 처리 : Response success 코드 정리
+
+ }
+ // #107 2024.06.20 일정 복사하기 END //
}
diff --git a/src/main/java/com/minizin/travel/scrap/controller/ScrapController.java b/src/main/java/com/minizin/travel/scrap/controller/ScrapController.java
new file mode 100644
index 0000000..bf666d5
--- /dev/null
+++ b/src/main/java/com/minizin/travel/scrap/controller/ScrapController.java
@@ -0,0 +1,57 @@
+package com.minizin.travel.scrap.controller;
+
+import com.minizin.travel.scrap.service.ScrapService;
+import com.minizin.travel.user.domain.dto.PrincipalDetails;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+
+@RestController
+@RequiredArgsConstructor
+public class ScrapController {
+
+ private final ScrapService scrapService;
+
+ // #49 스크랩 생성 START //
+ @PostMapping("/scraps/{plan_id}")
+ public ResponseEntity> createScrap(@PathVariable("plan_id") Long planId,
+ @AuthenticationPrincipal PrincipalDetails user) {
+
+ var result = scrapService.createScrap(planId, user);
+
+ return ResponseEntity.ok(result);
+ }
+ // #49 스크랩 생성 END //
+
+ // #50 스크랩 조회 START //
+ @GetMapping("/scraps")
+ public ResponseEntity> selectListScrapedPlans(@RequestParam("cursor_id") Long cursorId,
+ @AuthenticationPrincipal PrincipalDetails user) {
+
+ var result = scrapService.selectListScrapedPlans(cursorId, user);
+
+ return ResponseEntity.ok(result);
+ }
+ // #50 스크랩 조회 END //
+
+ // #51 스크랩 삭제 START //
+ @DeleteMapping("/scraps/{plan_id}")
+ public ResponseEntity> deleteScrapedPlan(@PathVariable("plan_id") Long planId,
+ @AuthenticationPrincipal PrincipalDetails user) {
+
+ var result = scrapService.deleteScrapedPlan(planId, user);
+
+ return ResponseEntity.ok(result);
+ }
+ // #51 스크랩 삭제 END //
+
+ @GetMapping("/scraps/check/{plan_id}")
+ public ResponseEntity> checkScrapedPlan(@PathVariable("plan_id") Long planId,
+ @AuthenticationPrincipal PrincipalDetails user) {
+
+ var result = scrapService.checkScrapedPlan(planId, user);
+
+ return ResponseEntity.ok(result);
+ }
+}
diff --git a/src/main/java/com/minizin/travel/scrap/dto/ResponseCheckScrapedPlanDto.java b/src/main/java/com/minizin/travel/scrap/dto/ResponseCheckScrapedPlanDto.java
new file mode 100644
index 0000000..1931199
--- /dev/null
+++ b/src/main/java/com/minizin/travel/scrap/dto/ResponseCheckScrapedPlanDto.java
@@ -0,0 +1,19 @@
+package com.minizin.travel.scrap.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import lombok.Builder;
+import lombok.Getter;
+
+@Builder
+@Getter
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class ResponseCheckScrapedPlanDto {
+
+ private boolean success;
+
+ private String message;
+
+ @JsonPropertyOrder("scrap_id")
+ private Long scrapId;
+}
diff --git a/src/main/java/com/minizin/travel/scrap/dto/ResponseCreateScrapPlanDto.java b/src/main/java/com/minizin/travel/scrap/dto/ResponseCreateScrapPlanDto.java
new file mode 100644
index 0000000..620f638
--- /dev/null
+++ b/src/main/java/com/minizin/travel/scrap/dto/ResponseCreateScrapPlanDto.java
@@ -0,0 +1,47 @@
+package com.minizin.travel.scrap.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.minizin.travel.scrap.entity.Scrap;
+import lombok.Builder;
+import lombok.Getter;
+
+import java.time.format.DateTimeFormatter;
+
+@Builder
+@Getter
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class ResponseCreateScrapPlanDto {
+
+ @JsonProperty("scrap_id")
+ private Long id;
+
+ private Long userId;
+
+ private Long planId;
+
+ private String createdAt;
+
+ private boolean success;
+
+ private String message;
+
+ public static ResponseCreateScrapPlanDto toDto(Scrap scrap) {
+ return ResponseCreateScrapPlanDto.builder()
+ .success(true)
+ .id(scrap.getId())
+ .userId(scrap.getUserId())
+ .planId(scrap.getPlanId())
+ .createdAt(scrap.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
+ .build();
+ }
+
+ public static ResponseCreateScrapPlanDto fail(Long planId, String message) {
+
+ return ResponseCreateScrapPlanDto.builder()
+ .success(false)
+ .message(message)
+ .id(planId)
+ .build();
+ }
+}
diff --git a/src/main/java/com/minizin/travel/scrap/dto/ResponseDeleteScrapedPlanDto.java b/src/main/java/com/minizin/travel/scrap/dto/ResponseDeleteScrapedPlanDto.java
new file mode 100644
index 0000000..7ba4d76
--- /dev/null
+++ b/src/main/java/com/minizin/travel/scrap/dto/ResponseDeleteScrapedPlanDto.java
@@ -0,0 +1,19 @@
+package com.minizin.travel.scrap.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import lombok.Builder;
+import lombok.Getter;
+
+@Builder
+@Getter
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class ResponseDeleteScrapedPlanDto {
+
+ private boolean success;
+
+ private String message;
+
+ @JsonPropertyOrder("scrap_id")
+ private Long scrapId;
+}
diff --git a/src/main/java/com/minizin/travel/scrap/dto/ResponseSelectScrapedPlansDto.java b/src/main/java/com/minizin/travel/scrap/dto/ResponseSelectScrapedPlansDto.java
new file mode 100644
index 0000000..9a59b88
--- /dev/null
+++ b/src/main/java/com/minizin/travel/scrap/dto/ResponseSelectScrapedPlansDto.java
@@ -0,0 +1,33 @@
+package com.minizin.travel.scrap.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Builder;
+import lombok.Getter;
+
+import java.util.List;
+
+@Builder
+@Getter
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+public class ResponseSelectScrapedPlansDto {
+
+ private List data;
+
+ private Long cursorId;
+
+ private String message;
+
+ @JsonProperty("scrap_id")
+ private Long id;
+
+ public static ResponseSelectScrapedPlansDto fail(String message) {
+
+ return ResponseSelectScrapedPlansDto.builder()
+ .message(message)
+ .build();
+ }
+}
diff --git a/src/main/java/com/minizin/travel/scrap/dto/SelectScrapedPlansDto.java b/src/main/java/com/minizin/travel/scrap/dto/SelectScrapedPlansDto.java
new file mode 100644
index 0000000..f33c535
--- /dev/null
+++ b/src/main/java/com/minizin/travel/scrap/dto/SelectScrapedPlansDto.java
@@ -0,0 +1,59 @@
+package com.minizin.travel.scrap.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.plan.entity.Plan;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDate;
+
+@Builder
+@Getter
+@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
+@JsonPropertyOrder({"id", "planId", "userNickname", "planName", "theme", "startDate", "endDate",
+ "planBudget", "scope", "numberOfMembers", "numberOfScraps"})
+public class SelectScrapedPlansDto {
+
+ @Setter
+ @JsonProperty("scrap_id")
+ private Long id;
+
+ private Long planId;
+
+ @Setter
+ private String userNickname;
+
+ private String planName;
+
+ private String theme;
+
+ private LocalDate startDate;
+
+ private LocalDate endDate;
+
+ @Setter
+ private int planBudget;
+
+ private boolean scope;
+
+ private int numberOfMembers;
+
+ private int numberOfScraps;
+
+ public static SelectScrapedPlansDto toDto(Plan plan) {
+ return SelectScrapedPlansDto.builder()
+ .planId(plan.getId())
+ .planName(plan.getPlanName())
+ .theme(plan.getTheme())
+ .startDate(plan.getStartDate())
+ .endDate(plan.getEndDate())
+ .scope(plan.isScope())
+ .numberOfMembers(plan.getNumberOfMembers())
+ .numberOfScraps(plan.getNumberOfScraps())
+ .build();
+ }
+}
diff --git a/src/main/java/com/minizin/travel/scrap/entity/Scrap.java b/src/main/java/com/minizin/travel/scrap/entity/Scrap.java
new file mode 100644
index 0000000..1f57154
--- /dev/null
+++ b/src/main/java/com/minizin/travel/scrap/entity/Scrap.java
@@ -0,0 +1,30 @@
+package com.minizin.travel.scrap.entity;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.CreatedDate;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Builder
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class Scrap {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "scrap_id")
+ private Long id;
+
+ private Long userId;
+
+ private Long planId;
+
+ @CreatedDate
+ private LocalDateTime createdAt;
+}
diff --git a/src/main/java/com/minizin/travel/scrap/repository/ScrapRepository.java b/src/main/java/com/minizin/travel/scrap/repository/ScrapRepository.java
new file mode 100644
index 0000000..1969227
--- /dev/null
+++ b/src/main/java/com/minizin/travel/scrap/repository/ScrapRepository.java
@@ -0,0 +1,25 @@
+package com.minizin.travel.scrap.repository;
+
+import com.minizin.travel.scrap.entity.Scrap;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface ScrapRepository extends JpaRepository {
+
+ // #49 스크랩 생성 : 이미 스크랩한 일정인지 확인 //
+ boolean existsByPlanIdAndUserId(Long planId, Long userId);
+
+ // #50 2024.06.12 스크랩 조회 : 해당 회원이 스크랩한 일정이 있는지 확인 //
+ boolean existsByUserId(Long userId);
+
+ // #50 2024.06.12 스크랩 조회 : 해당 회원이 스크랩한 일정 모두 조회 //
+ List findAllByUserIdOrderByIdDesc(Long userId, Pageable page);
+
+ List findByIdLessThanAndUserIdOrderByIdDesc(Long cursorId, Long userId, Pageable page);
+
+ // #51 스크랩 삭제 전 삭제할 스크랩이 있는지 조회
+ Optional findByPlanIdAndUserId(Long planId, Long userId);
+}
diff --git a/src/main/java/com/minizin/travel/scrap/service/ScrapService.java b/src/main/java/com/minizin/travel/scrap/service/ScrapService.java
new file mode 100644
index 0000000..16088ce
--- /dev/null
+++ b/src/main/java/com/minizin/travel/scrap/service/ScrapService.java
@@ -0,0 +1,165 @@
+package com.minizin.travel.scrap.service;
+
+import com.minizin.travel.plan.entity.Plan;
+import com.minizin.travel.plan.entity.PlanSchedule;
+import com.minizin.travel.plan.repository.PlanRepository;
+import com.minizin.travel.plan.repository.PlanScheduleRepository;
+import com.minizin.travel.plan.service.PlanService;
+import com.minizin.travel.scrap.dto.*;
+import com.minizin.travel.scrap.entity.Scrap;
+import com.minizin.travel.scrap.repository.ScrapRepository;
+import com.minizin.travel.user.domain.dto.PrincipalDetails;
+import com.minizin.travel.user.domain.entity.UserEntity;
+import com.minizin.travel.user.domain.enums.UserErrorCode;
+import com.minizin.travel.user.domain.exception.CustomUserException;
+import com.minizin.travel.user.domain.repository.UserRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class ScrapService {
+
+ private final ScrapRepository scrapRepository;
+ private final PlanRepository planRepository;
+ private final PlanScheduleRepository planScheduleRepository;
+ private final UserRepository userRepository;
+ private final PlanService planService;
+
+ final int DEFAULT_PAGE_SIZE = 6;
+
+ // #49 스크랩 생성 START //
+ public ResponseCreateScrapPlanDto createScrap(Long planId,
+ PrincipalDetails user) {
+
+ Long userId = userRepository.findByUsername(user.getUsername()).get().getId();
+
+ // 해당 plan 이 DB에 저장되었는지 확인
+ if (!planRepository.existsById(planId)) {
+ return ResponseCreateScrapPlanDto.fail(planId, "요청하신 plan 은 존재하지 않습니다.");
+ }
+
+ // 해당 plan 을 이미 저장하지 않았는지 확인
+ if (scrapRepository.existsByPlanIdAndUserId(planId, userId)) {
+ return ResponseCreateScrapPlanDto.fail(planId, "이미 북마크한 plan 입니다.");
+ }
+
+ Plan plan = planRepository.findById(planId).get();
+
+ /*
+ if (plan.getUserId().equals(userId)) {
+ return ResponseCreateScrapPlanDto.fail(planId, "본인의 plan은 북마크할 수 없습니다.");
+ }
+ */
+
+ plan.setNumberOfScraps(plan.getNumberOfScraps() + 1);
+
+ return ResponseCreateScrapPlanDto.toDto(scrapRepository.save(Scrap.builder()
+ .userId(userId)
+ .planId(planId)
+ .createdAt(LocalDateTime.now())
+ .build()));
+ }
+ // #49 스크랩 생성 END //
+
+ // #50 스크랩 조회 START //
+ public ResponseSelectScrapedPlansDto selectListScrapedPlans(Long cursorId, PrincipalDetails user) {
+
+ Pageable page = PageRequest.of(0, DEFAULT_PAGE_SIZE);
+
+ Long userId = userRepository.findByUsername(user.getUsername()).get().getId();
+
+ if (!scrapRepository.existsByUserId(userId)) {
+ return ResponseSelectScrapedPlansDto.fail("북마크한 plan 이 없습니다.");
+ }
+
+ List scrapList = findAllByCursorIdCheckExistCursor(userId, cursorId, page);
+ List scrapedPlansDtoList = new ArrayList<>();
+ Long scrapId = 0L;
+ for (Scrap scrap : scrapList) {
+ scrapId = scrap.getId();
+ Plan plan = planRepository.findById(scrap.getPlanId()).get();
+
+ SelectScrapedPlansDto newSelectScrapedPlanDto = SelectScrapedPlansDto.toDto(plan);
+ newSelectScrapedPlanDto.setId(scrapId);
+ newSelectScrapedPlanDto.setUserNickname(userRepository.findById(plan.getUserId()).get().getNickname());
+
+ // 예산 계산
+ List planScheduleList = planScheduleRepository.findAllByPlanId(plan.getId());
+ newSelectScrapedPlanDto.setPlanBudget(planService.calculateTotalPlanBudget(planScheduleList));
+
+ scrapedPlansDtoList.add(newSelectScrapedPlanDto);
+ }
+
+ if (!findAllByCursorIdCheckExistCursor(userId, scrapId, page).isEmpty()) {
+ scrapId = null;
+ }
+
+ return ResponseSelectScrapedPlansDto.builder()
+ .data(scrapedPlansDtoList)
+ .cursorId(scrapId)
+ .build();
+ }
+
+ private List findAllByCursorIdCheckExistCursor(Long userId, Long cursorId, Pageable page) {
+
+ return cursorId == 0 ? scrapRepository.findAllByUserIdOrderByIdDesc(userId, page)
+ : scrapRepository.findByIdLessThanAndUserIdOrderByIdDesc(cursorId, userId, page);
+ }
+ // #50 스크랩 조회 END //
+
+ // #51 스크랩 삭제 START //
+ @Transactional
+ public ResponseDeleteScrapedPlanDto deleteScrapedPlan(Long planId, PrincipalDetails user) {
+
+ Long userId = userRepository.findByUsername(user.getUsername()).get().getId();
+
+ if (!scrapRepository.existsByPlanIdAndUserId(planId, userId)) {
+
+ return ResponseDeleteScrapedPlanDto.builder()
+ .success(false)
+ .message("존재하지 않는 scrap 입니다.")
+ .build();
+ }
+
+ Scrap scrap = scrapRepository.findByPlanIdAndUserId(planId, userId).get();
+
+ Plan plan = planRepository.findById(scrap.getPlanId()).get();
+ plan.setNumberOfScraps(plan.getNumberOfScraps() - 1);
+
+ scrapRepository.deleteById(scrap.getId());
+
+ return ResponseDeleteScrapedPlanDto.builder()
+ .success(true)
+ .message("Scrap Deleted Successfully")
+ .scrapId(scrap.getId())
+ .build();
+ }
+ // #51 스크랩 삭제 END //
+
+ public ResponseCheckScrapedPlanDto checkScrapedPlan(Long planId, PrincipalDetails user) {
+
+ UserEntity userEntity = userRepository.findByUsername(user.getUsername())
+ .orElseThrow(() -> new CustomUserException(UserErrorCode.USER_NOT_FOUND));
+
+ if (!scrapRepository.existsByPlanIdAndUserId(planId, userEntity.getId())) {
+
+ return ResponseCheckScrapedPlanDto.builder()
+ .success(false)
+ .message("존재하지 않는 scrap 입니다.")
+ .build();
+ }
+
+ return ResponseCheckScrapedPlanDto.builder()
+ .success(true)
+ .message("존재하는 scrap 입니다.")
+ .build();
+ }
+}
diff --git a/src/main/java/com/minizin/travel/tour/controller/TourController.java b/src/main/java/com/minizin/travel/tour/controller/TourController.java
new file mode 100644
index 0000000..b0ba330
--- /dev/null
+++ b/src/main/java/com/minizin/travel/tour/controller/TourController.java
@@ -0,0 +1,82 @@
+package com.minizin.travel.tour.controller;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.minizin.travel.tour.domain.dto.TourAPIDto;
+import com.minizin.travel.tour.domain.entity.TourAPI;
+import com.minizin.travel.tour.service.TourService;
+import io.swagger.v3.oas.annotations.Operation;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Class: TourController Project: travel Package: com.minizin.travel.tour.controller
+ *
+ * Description: TourController
+ *
+ * @author dong-hoshin
+ * @date 6/8/24 03:35 Copyright (c) 2024 miniJin
+ * @see GitHub Repository
+ */
+//@Tag(name = "Tour Controller")
+@Slf4j
+@RestController
+@RequestMapping("/tour")
+@RequiredArgsConstructor
+public class TourController {
+ private final TourService tourService;
+ Logger logger = LoggerFactory.getLogger(getClass());
+ private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
+
+ @Operation(summary = "Get detailCommon", description = "Retrieve a specific detailCommon ")
+ @GetMapping("/detailCommon")
+ public CompletableFuture> getAPITourDataDetailCommon(@ModelAttribute TourAPIDto.TourRequest requestParam) {
+ return tourService.getTourAPIFromSiteDetailCommon(requestParam)
+ .thenApply(response -> {
+ logger.info(response); // 성공 메시지를 로그에 출력
+ return ResponseEntity.ok(response);
+ });
+ }
+
+ @Operation(summary = "Get areaBasedList", description = "Retrieve a specific areaBasedList ")
+ @GetMapping("/areaBasedList")
+ public CompletableFuture>> getAPITourDataAreaBasedList(@ModelAttribute TourAPIDto.TourRequest requestParam) {
+ return createResponseEntity(tourService.getTourAPIFromSiteAreaBasedList(requestParam));
+ }
+ @Operation(summary = "Get areaBasedListSingle", description = "Retrieve a specific Single areaBasedList with batch")
+ @GetMapping("/areaBasedListSingle")
+ public CompletableFuture>> getAPITourDataAreaBasedListSingle(@ModelAttribute TourAPIDto.TourRequest requestParam) {
+ return createResponseEntity(tourService.getTourAPIFromSiteAreaBasedListSingle(requestParam));
+ }
+
+
+ @Operation(summary = "Get areaCode", description = "Retrieve a specific areaBasedList by its ID")
+ @GetMapping("/areacode")
+ public CompletableFuture>> getAPITourDataAreaCode(@ModelAttribute TourAPIDto.TourRequest requestParam) {
+ return createResponseEntity(tourService.getTourAPIFromSiteAreaCode(requestParam));
+ }
+
+ @Operation(summary = "Get searchkeyword", description = "Retrieve a specific searchkeyword by keyword")
+ @GetMapping("/searchkeyword")
+ public CompletableFuture>> getTourAPIFromSiteSearchKeyword(@ModelAttribute TourAPIDto.TourRequest requestParam) {
+ return createResponseEntity(tourService.getTourAPIFromSiteSearchKeyword(requestParam));
+ }
+
+ private CompletableFuture>> createResponseEntity(CompletableFuture> future) {
+ return future.thenApply(ResponseEntity::ok)
+ .exceptionally(throwable -> ResponseEntity.status(500).build());
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/minizin/travel/tour/controller/TourInfoController.java b/src/main/java/com/minizin/travel/tour/controller/TourInfoController.java
new file mode 100644
index 0000000..eb2a89e
--- /dev/null
+++ b/src/main/java/com/minizin/travel/tour/controller/TourInfoController.java
@@ -0,0 +1,132 @@
+package com.minizin.travel.tour.controller;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.minizin.travel.tour.domain.dto.TourAPIDto;
+import com.minizin.travel.tour.service.TourInfoService;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.function.Supplier;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.Protocol;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Class: TourInfoController Project: travel Package: com.minizin.travel.tour.controller
+ *
+ * Description: TourInfoController
+ *
+ * @author dong-hoshin
+ * @date 6/16/24 13:49 Copyright (c) 2024 MiniJin
+ * @see GitHub Repository
+ */
+@Slf4j
+@RestController
+@RequestMapping("/tour/info")
+@RequiredArgsConstructor
+public class TourInfoController {
+ private final TourInfoService tourInfoService;
+ private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
+ @GetMapping("/areaCode1")
+ public ResponseEntity getTourDataByAreaCode(@ModelAttribute TourAPIDto.TourRequest requestUrl) throws IOException {
+ log.info("Received request: {}", requestUrl);
+ return processTourRequest(requestUrl, () -> {
+ try {
+ return tourInfoService.getTourDataByAreaCode(requestUrl);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ @GetMapping("/areaBasedList1")
+ public ResponseEntity getTourDataByBasedList(@ModelAttribute TourAPIDto.TourRequest requestUrl) throws IOException {
+ log.info("Received request: {}", requestUrl);
+ try {
+ return processTourRequest(requestUrl, () -> {
+ try {
+ return tourInfoService.getTourDataByAreaBasedList(requestUrl);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ } catch (RuntimeException e) {
+ return handleException(e);
+ }
+ }
+
+ @GetMapping("/searchKeyword1")
+ public ResponseEntity getTourDataBySearchKeyword(@ModelAttribute TourAPIDto.TourRequest requestUrl) throws IOException {
+ log.info("Received request: {}", requestUrl);
+ try {
+ return processTourRequest(requestUrl, () -> {
+ try {
+ return tourInfoService.getTourDataSearchKeyword(requestUrl);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ } catch (RuntimeException e) {
+ return handleException(e);
+ }
+ }
+
+ @GetMapping("/detailCommon1")
+ public ResponseEntity getTourDataByDetailCommon(@ModelAttribute TourAPIDto.TourRequest requestUrl) throws IOException {
+ log.info("Received request: {}", requestUrl);
+ try {
+ return processTourRequest(requestUrl, () -> {
+ try {
+ return tourInfoService.getTourDataByDetailCommon(requestUrl);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ } catch (RuntimeException e) {
+ return handleException(e);
+ }
+ }
+
+ private ResponseEntity processTourRequest(TourAPIDto.TourRequest requestUrl, Supplier tourDataSupplier) {
+ try {
+ if (requestUrl.getServiceKey() != null ) {
+ TourAPIDto responseDto = tourDataSupplier.get();
+ String jsonResponse = gson.toJson(responseDto);
+ log.info(jsonResponse);
+ return ResponseEntity.ok()
+ .contentType(org.springframework.http.MediaType.APPLICATION_JSON)
+ .body(jsonResponse);
+ } else {
+ String errorResponse = "{\"successful\":false,\"redirect\":false}";
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST)
+ .contentType(org.springframework.http.MediaType.APPLICATION_JSON)
+ .body(errorResponse);
+ }
+ } catch (RuntimeException e) {
+ throw e;
+ }
+ }
+
+ private ResponseEntity handleException(RuntimeException e) {
+ String errorResponse;
+ if (e.getCause() instanceof IOException) {
+ errorResponse = "{\"successful\":false,\"error\":\"IOException occurred\"}";
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .contentType(org.springframework.http.MediaType.APPLICATION_JSON)
+ .body(errorResponse);
+ } else {
+ errorResponse = "{\"successful\":false,\"error\":\"Unexpected error occurred\"}";
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .contentType(org.springframework.http.MediaType.APPLICATION_JSON)
+ .body(errorResponse);
+ }
+ }
+}
diff --git a/src/main/java/com/minizin/travel/tour/domain/dto/TourAPIDto.java b/src/main/java/com/minizin/travel/tour/domain/dto/TourAPIDto.java
new file mode 100644
index 0000000..f6f926e
--- /dev/null
+++ b/src/main/java/com/minizin/travel/tour/domain/dto/TourAPIDto.java
@@ -0,0 +1,238 @@
+package com.minizin.travel.tour.domain.dto;
+
+import com.minizin.travel.tour.domain.entity.TourAPI;
+import jakarta.persistence.Column;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * Class: TourAPIRequestDTO Project: travel Package: com.minizin.travel.tour.domain.dto
+ *
+ * Description: TourAPIRequestDTO
+ *
+ * @author dong-hoshin
+ * @date 6/3/24 19:56 Copyright (c) 2024 MiniJin
+ * @see GitHub Repository
+ */
+@Data
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class TourAPIDto {
+ private TourRequest request;
+ @Data
+ @Getter
+ @Setter
+ public static class TourRequest {
+ private String MobileOS;
+ private String MobileApp;
+ private String _type;
+ private String ServiceKey;
+
+ private String areaCode;
+ private String arrange;
+ private String cat1;
+ private String cat2;
+ private String cat3;
+ private String contentTypeId;
+ private String contentId;
+ private String eventStartDate;
+ private String keyword;
+ private String listYN;
+ private String mapX;
+ private String mapY;
+ private String modifiedtime;
+ private String numOfRows;
+ private String pageNo;
+ private String radius;
+ private String sigunguCode;
+ private String defaultYN;
+ private String firstImageYN;
+ private String areacodeYN;
+ private String catcodeYN;
+ private String addrinfoYN;
+ private String mapinfoYN;
+ private String overviewYN;
+ }
+
+ private TourResponse response;
+
+ @Getter
+ @Setter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Builder
+ public static class TourResponse {
+ private Header header;
+ private Body body;
+
+
+ @Getter
+ @Setter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Builder
+ public static class Header {
+ private String resultCode;
+ private String resultMsg;
+ }
+
+ @Getter
+ @Setter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Builder
+ public static class Body {
+ private Items items;
+ private int numOfRows;
+ private int pageNo;
+ private int totalCount;
+
+ @Getter
+ @Setter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Builder
+ public static class Items {
+ private List- item;
+
+ @Getter
+ @Setter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Builder
+ public static class Item {
+ private String addr1;
+ private String addr2;
+ private String areacode;
+ private String benikia;
+ private String booktour;
+ private String cat1;
+ private String cat2;
+ private String cat3;
+ private String code;
+ private String contentid;
+ private String contenttypeid;
+ private String cpyrhtDivCd;
+ private String createdtime;
+ private String dist;
+ private String eventstartdate;
+ private String firstimage;
+ private String firstimage2;
+ private String goodstay;
+ private String hanok;
+ private String mapx;
+ private String mapy;
+ private String mlevel;
+ private String modifiedtime;
+ private String name;
+ private Integer numOfRows;
+ private Integer pageNo;
+ private Integer rnum;
+ private String sigungucode;
+ private String tel;
+ private String title;
+ private Integer totalCnt;
+ private Integer totalCount;
+ private String zipcode;
+ @Column(columnDefinition = "TEXT")
+ private String homepage;
+ private String telname;
+ @Column(columnDefinition = "TEXT")
+ private String overview;
+ private String readcount;
+
+ public TourAPI toEntity() {
+ return TourAPI.builder()
+ .addr1(Optional.ofNullable(addr1).orElse(""))
+ .addr2(Optional.ofNullable(addr2).orElse(""))
+ .areaCode(Optional.ofNullable(areacode).orElse(""))
+ .benikia(Optional.ofNullable(benikia).orElse(""))
+ .booktour(Optional.ofNullable(booktour).orElse(""))
+ .cat1(Optional.ofNullable(cat1).orElse(""))
+ .cat2(Optional.ofNullable(cat2).orElse(""))
+ .cat3(Optional.ofNullable(cat3).orElse(""))
+ .code(Optional.ofNullable(code).orElse(""))
+ .contentId(Optional.ofNullable(contentid).orElse(""))
+ .contentTypeId(Optional.ofNullable(contenttypeid).orElse(""))
+ .cpyrhtDivCd(Optional.ofNullable(cpyrhtDivCd).orElse(""))
+ .createdTime(Optional.ofNullable(createdtime).orElse(""))
+ .dist(Optional.ofNullable(dist).orElse(""))
+ .eventStartDate(Optional.ofNullable(eventstartdate).orElse(""))
+ .firstImage(Optional.ofNullable(firstimage).orElse(""))
+ .firstImage2(Optional.ofNullable(firstimage2).orElse(""))
+ .goodStay(Optional.ofNullable(goodstay).orElse(""))
+ .hanok(Optional.ofNullable(hanok).orElse(""))
+ .mapX(Optional.ofNullable(mapx).orElse(""))
+ .mapY(Optional.ofNullable(mapy).orElse(""))
+ .mlevel(Optional.ofNullable(mlevel).orElse(""))
+ .modifiedTime(Optional.ofNullable(modifiedtime).orElse(""))
+ .name(Optional.ofNullable(name).orElse(""))
+ .numOfRows(Optional.ofNullable(numOfRows).orElse(0))
+ .pageNo(Optional.ofNullable(pageNo).orElse(0))
+ .rnum(Optional.ofNullable(rnum).orElse(0))
+ .sigunguCode(Optional.ofNullable(sigungucode).orElse(""))
+ .tel(Optional.ofNullable(tel).orElse(""))
+ .title(Optional.ofNullable(title).orElse(""))
+ .totalCnt(Optional.ofNullable(totalCnt).orElse(0))
+ .totalCount(Optional.ofNullable(totalCount).orElse(0))
+ .zipcode(Optional.ofNullable(zipcode).orElse(""))
+ .homepage(Optional.ofNullable(homepage).orElse(""))
+ .telName(Optional.ofNullable(telname).orElse(""))
+ .overview(Optional.ofNullable(overview).orElse(""))
+ .build();
+ }
+ public TourAPI toEntitySigungu(String codeArea,boolean codeOrSigungu) {
+ // areaCode 가 비어있을 때 모든 특별시, 도를 검색 (true)
+ // areaCode가 있을 때는 해당 areaCode의 sigungu를 검색 (false)
+ // !codeOrSigungu -> areaCode가 있을 때 sigungucode에 code를 넣고 code에 areaCode를 넣는다.
+ if (!codeOrSigungu) {
+ sigungucode = code;
+ code = codeArea;
+ }
+ return TourAPI.builder()
+ .code(Optional.ofNullable(code).orElse(""))
+ .sigunguCode(Optional.ofNullable(sigungucode).orElse(""))
+ .rnum(Optional.ofNullable(rnum).orElse(0))
+ .totalCnt(Optional.ofNullable(totalCnt).orElse(0))
+ .totalCount(Optional.ofNullable(totalCount).orElse(0))
+ .name(Optional.ofNullable(name).orElse(""))
+ .build();
+ }
+ }
+ }
+ }
+ }
+
+ public List toEntityList() {
+ List tourAPIList = new ArrayList<>();
+ if (response != null && response.getBody() != null && response.getBody().getItems() != null) {
+ for (TourResponse.Body.Items.Item item : response.getBody().getItems().getItem()) {
+ TourAPI tourAPI = item.toEntity();
+ tourAPIList.add(tourAPI);
+ }
+ }
+ return tourAPIList;
+ }
+
+ public List toEntityListArea(String codeArea,boolean codeOrSigungu) {
+ List tourAPIList = new ArrayList<>();
+ if (response != null && response.getBody() != null && response.getBody().getItems() != null) {
+ for (TourResponse.Body.Items.Item item : response.getBody().getItems().getItem()) {
+ TourAPI tourAPI = item.toEntitySigungu(codeArea, codeOrSigungu);
+
+ tourAPIList.add(tourAPI);
+ }
+ }
+ return tourAPIList;
+ }
+}
diff --git a/src/main/java/com/minizin/travel/tour/domain/dto/TourAPIRequestDTO.java b/src/main/java/com/minizin/travel/tour/domain/dto/TourAPIRequestDTO.java
deleted file mode 100644
index 30ff444..0000000
--- a/src/main/java/com/minizin/travel/tour/domain/dto/TourAPIRequestDTO.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.minizin.travel.tour.domain.dto;
-
-/**
- * Class: TourAPIRequestDTO Project: travel Package: com.minizin.travel.tour.domain.dto
- *
- * Description: TourAPIRequestDTO
- *
- * @author dong-hoshin
- * @date 6/3/24 19:56 Copyright (c) 2024 MiniJin
- * @see GitHub Repository
- */
-public class TourAPIRequestDTO {
- private Long requestId;
- private String type;
- private String areaCode;
- private String arrange;
- private String cat1;
- private String cat2;
- private String cat3;
- private String contentTypeId;
- private String eventStartDate;
- private String keyword;
- private String listYN;
- private String mapX;
- private String mapY;
- private String mobileApp;
- private String mobileOS;
- private String modifiedTime;
- private Integer numOfRows;
- private Integer pageNo;
- private String radius;
- private String serviceKey;
- private String sigunguCode;
-
-
-}
diff --git a/src/main/java/com/minizin/travel/tour/domain/dto/TourAPIResponseDTO.java b/src/main/java/com/minizin/travel/tour/domain/dto/TourAPIResponseDTO.java
deleted file mode 100644
index 3f7361a..0000000
--- a/src/main/java/com/minizin/travel/tour/domain/dto/TourAPIResponseDTO.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.minizin.travel.tour.domain.dto;
-
-/**
- * Class: TourAPIResponseDTO Project: travel Package: com.minizin.travel.tour.domain.dto
- *
- * Description: TourAPIResponseDTO
- *
- * @author dong-hoshin
- * @date 6/3/24 19:58 Copyright (c) 2024 MiniJin
- * @see GitHub Repository
- */
-public class TourAPIResponseDTO {
- private Long responseId;
- private String addr1;
- private String addr2;
- private String areaCode;
- private String benikia;
- private String booktour;
- private String cat1;
- private String cat2;
- private String cat3;
- private String code;
- private String contentId;
- private String contentTypeId;
- private String cpyrhtDivCd;
- private String createdTime;
- private String dist;
- private String eventStartDate;
- private String firstImage;
- private String firstImage2;
- private String goodStay;
- private String hanok;
- private String mapx;
- private String mapy;
- private String mlevel;
- private String modifiedTime;
- private String name;
- private Integer numOfRows;
- private Integer pageNo;
- private String resultCode;
- private String resultMsg;
- private String rnum;
- private String sigunguCode;
- private String tel;
- private String title;
- private Integer totalCnt;
- private Integer totalCount;
- private String zipcode;
-
-}
diff --git a/src/main/java/com/minizin/travel/tour/domain/entity/TourAPI.java b/src/main/java/com/minizin/travel/tour/domain/entity/TourAPI.java
new file mode 100644
index 0000000..0b3faa5
--- /dev/null
+++ b/src/main/java/com/minizin/travel/tour/domain/entity/TourAPI.java
@@ -0,0 +1,278 @@
+package com.minizin.travel.tour.domain.entity;
+
+import com.minizin.travel.tour.domain.dto.TourAPIDto;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * Class: TourAPIRequest Project: travel Package: com.minizin.travel.tour.domain.entity
+ *
+ * Description: TourAPIRequest
+ *
+ * @author dong-hoshin
+ * @date 6/3/24 19:23 Copyright (c) 2024 MiniJin
+ * @see GitHub Repository
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@Entity
+@Table(name = "tour_api")
+public class TourAPI {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "tour_id")
+ private Long tourId;
+
+ @Column(name = "content_id")
+ private String contentId;
+
+ @Column(name = "type")
+ private String _type;
+
+ @Column(name = "area_code")
+ private String areaCode;
+
+ @Column(name = "arrange")
+ private String arrange;
+
+ @Column(name = "cat1")
+ private String cat1;
+
+ @Column(name = "cat2")
+ private String cat2;
+
+ @Column(name = "cat3")
+ private String cat3;
+
+ @Column(name = "content_type_id")
+ private String contentTypeId;
+
+ @Column(name = "event_start_date")
+ private String eventStartDate;
+
+ @Column(name = "keyword")
+ private String keyword;
+
+ @Column(name = "list_yn")
+ private String listYn;
+
+ @Column(name = "map_x")
+ private String mapX;
+
+ @Column(name = "map_y")
+ private String mapY;
+
+ @Column(name = "mobile_app")
+ private String mobileApp;
+
+ @Column(name = "mobile_os")
+ private String mobileOs;
+
+ @Column(name = "modified_time")
+ private String modifiedTime;
+
+ @Column(name = "num_of_rows")
+ private Integer numOfRows;
+
+ @Column(name = "page_no")
+ private Integer pageNo;
+
+ @Column(name = "radius")
+ private String radius;
+
+ @Column(name = "service_key")
+ private String serviceKey;
+
+ @Column(name = "sigungu_code")
+ private String sigunguCode;
+
+ @Column(name = "default_yn")
+ private String defaultYn;
+
+ @Column(name = "first_image_yn")
+ private String firstImageYn;
+
+ @Column(name = "area_code_yn")
+ private String areaCodeYn;
+
+ @Column(name = "cat_code_yn")
+ private String catCodeYn;
+
+ @Column(name = "addr_info_yn")
+ private String addrInfoYn;
+
+ @Column(name = "map_info_yn")
+ private String mapInfoYn;
+
+ @Column(name = "overview_yn")
+ private String overviewYn;
+
+ @Column(name = "addr1")
+ private String addr1;
+
+ @Column(name = "addr2")
+ private String addr2;
+
+ @Column(name = "benikia")
+ private String benikia;
+
+ @Column(name = "booktour")
+ private String booktour;
+
+ @Column(name = "code")
+ private String code;
+
+ @Column(name = "cpyrht_div_cd")
+ private String cpyrhtDivCd;
+
+ @Column(name = "created_time")
+ private String createdTime;
+
+ @Column(name = "dist")
+ private String dist;
+
+ @Column(name = "first_image")
+ private String firstImage;
+
+ @Column(name = "first_image2")
+ private String firstImage2;
+
+ @Column(name = "good_stay")
+ private String goodStay;
+
+ @Column(name = "hanok")
+ private String hanok;
+
+ @Column(name = "mlevel")
+ private String mlevel;
+
+ @Column(name = "name")
+ private String name;
+
+ @Column(name = "rnum")
+ private Integer rnum;
+
+ @Column(name = "tel")
+ private String tel;
+
+ @Column(name = "title")
+ private String title;
+
+ @Column(name = "total_cnt")
+ private int totalCnt;
+
+ @Column(name = "total_count")
+ private int totalCount;
+
+ @Column(name = "zipcode")
+ private String zipcode;
+
+ @Column(name = "homepage", columnDefinition = "TEXT")
+ private String homepage;
+
+ @Column(name = "tel_name")
+ private String telName;
+
+ @Column(name = "overview", columnDefinition = "TEXT")
+ private String overview;
+
+ public TourAPIDto.TourResponse.Body.Items.Item toDto() {
+ return TourAPIDto.TourResponse.Body.Items.Item.builder()
+ .addr1(Optional.ofNullable(this.getAddr1()).orElse(""))
+ .addr2(Optional.ofNullable(this.getAddr2()).orElse(""))
+ .areacode(Optional.ofNullable(this.getAreaCode()).orElse(""))
+ .benikia(Optional.ofNullable(this.getBenikia()).orElse(""))
+ .booktour(Optional.ofNullable(this.getBooktour()).orElse(""))
+ .cat1(Optional.ofNullable(this.getCat1()).orElse(""))
+ .cat2(Optional.ofNullable(this.getCat2()).orElse(""))
+ .cat3(Optional.ofNullable(this.getCat3()).orElse(""))
+ .code(Optional.ofNullable(this.getCode()).orElse(""))
+ .contentid(Optional.ofNullable(this.getContentId()).orElse(""))
+ .contenttypeid(Optional.ofNullable(this.getContentTypeId()).orElse(""))
+ .cpyrhtDivCd(Optional.ofNullable(this.getCpyrhtDivCd()).orElse(""))
+ .createdtime(Optional.ofNullable(this.getCreatedTime()).orElse(""))
+ .dist(Optional.ofNullable(this.getDist()).orElse(""))
+ .eventstartdate(Optional.ofNullable(this.getEventStartDate()).orElse(""))
+ .firstimage(Optional.ofNullable(this.getFirstImage()).orElse(""))
+ .firstimage2(Optional.ofNullable(this.getFirstImage2()).orElse(""))
+ .goodstay(Optional.ofNullable(this.getGoodStay()).orElse(""))
+ .hanok(Optional.ofNullable(this.getHanok()).orElse(""))
+ .mapx(Optional.ofNullable(this.getMapX()).orElse(""))
+ .mapy(Optional.ofNullable(this.getMapY()).orElse(""))
+ .mlevel(Optional.ofNullable(this.getMlevel()).orElse(""))
+ .modifiedtime(Optional.ofNullable(this.getModifiedTime()).orElse(""))
+ .name(Optional.ofNullable(this.getName()).orElse(""))
+ .numOfRows(Optional.ofNullable(this.getNumOfRows()).orElse(0))
+ .pageNo(Optional.ofNullable(this.getPageNo()).orElse(0))
+ .rnum(Optional.ofNullable(this.getRnum()).orElse(0))
+ .sigungucode(Optional.ofNullable(this.getSigunguCode()).orElse(""))
+ .tel(Optional.ofNullable(this.getTel()).orElse(""))
+ .title(Optional.ofNullable(this.getTitle()).orElse(""))
+ .totalCnt(Optional.ofNullable(this.getTotalCnt()).orElse(0))
+ .totalCount(Optional.ofNullable(this.getTotalCount()).orElse(0))
+ .zipcode(Optional.ofNullable(this.getZipcode()).orElse(""))
+ .homepage(Optional.ofNullable(this.getHomepage()).orElse(""))
+ .telname(Optional.ofNullable(this.getTelName()).orElse(""))
+ .overview(Optional.ofNullable(this.getOverview()).orElse(""))
+ .build();
+ }
+
+ private static final ExecutorService threadPool = Executors.newCachedThreadPool();
+ public CompletableFuture updateFromDtoAsync(TourAPIDto.TourResponse.Body.Items.Item dto) {
+ return CompletableFuture.runAsync(() -> {
+ if (dto.getAddr1() != null) this.addr1 = dto.getAddr1();
+ if (dto.getAddr2() != null) this.addr2 = dto.getAddr2();
+ if (dto.getAreacode() != null) this.areaCode = dto.getAreacode();
+ if (dto.getBenikia() != null) this.benikia = dto.getBenikia();
+ if (dto.getBooktour() != null) this.booktour = dto.getBooktour();
+ if (dto.getCat1() != null) this.cat1 = dto.getCat1();
+ if (dto.getCat2() != null) this.cat2 = dto.getCat2();
+ if (dto.getCat3() != null) this.cat3 = dto.getCat3();
+ if (dto.getCode() != null) this.code = dto.getCode();
+ if (dto.getContentid() != null) this.contentId = dto.getContentid();
+ if (dto.getContenttypeid() != null) this.contentTypeId = dto.getContenttypeid();
+ if (dto.getCpyrhtDivCd() != null) this.cpyrhtDivCd = dto.getCpyrhtDivCd();
+ if (dto.getCreatedtime() != null) this.createdTime = dto.getCreatedtime();
+ if (dto.getDist() != null) this.dist = dto.getDist();
+ if (dto.getEventstartdate() != null) this.eventStartDate = dto.getEventstartdate();
+ if (dto.getFirstimage() != null) this.firstImage = dto.getFirstimage();
+ if (dto.getFirstimage2() != null) this.firstImage2 = dto.getFirstimage2();
+ if (dto.getGoodstay() != null) this.goodStay = dto.getGoodstay();
+ if (dto.getHanok() != null) this.hanok = dto.getHanok();
+ if (dto.getMapx() != null) this.mapX = dto.getMapx();
+ if (dto.getMapy() != null) this.mapY = dto.getMapy();
+ if (dto.getMlevel() != null) this.mlevel = dto.getMlevel();
+ if (dto.getModifiedtime() != null) this.modifiedTime = dto.getModifiedtime();
+ if (dto.getName() != null) this.name = dto.getName();
+ if (dto.getNumOfRows() != null) this.numOfRows = dto.getNumOfRows();
+ if (dto.getPageNo() != null) this.pageNo = dto.getPageNo();
+ if (dto.getRnum() != null) this.rnum = dto.getRnum();
+ if (dto.getSigungucode() != null) this.sigunguCode = dto.getSigungucode();
+ if (dto.getTel() != null) this.tel = dto.getTel();
+ if (dto.getTitle() != null) this.title = dto.getTitle();
+ if (dto.getTotalCnt() != null) this.totalCnt = dto.getTotalCnt();
+ if (dto.getTotalCount() != null) this.totalCount = dto.getTotalCount();
+ if (dto.getZipcode() != null) this.zipcode = dto.getZipcode();
+ if (dto.getHomepage() != null) this.homepage = dto.getHomepage();
+ if (dto.getTelname() != null) this.telName = dto.getTelname();
+ if (dto.getOverview() != null) this.overview = dto.getOverview();
+ }, threadPool);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/minizin/travel/tour/domain/entity/TourAPIRequest.java b/src/main/java/com/minizin/travel/tour/domain/entity/TourAPIRequest.java
deleted file mode 100644
index 09acea5..0000000
--- a/src/main/java/com/minizin/travel/tour/domain/entity/TourAPIRequest.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package com.minizin.travel.tour.domain.entity;
-
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.Table;
-
-/**
- * Class: TourAPIRequest Project: travel Package: com.minizin.travel.tour.domain.entity
- *
- * Description: TourAPIRequest
- *
- * @author dong-hoshin
- * @date 6/3/24 19:23 Copyright (c) 2024 MiniJin
- * @see GitHub Repository
- */
-@Entity
-@Table(name = "tour_api_request")
-public class TourAPIRequest {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- @Column(name = "request_id", nullable = false)
- private Long requestId;
-
- @Column(name = "_type")
- private String type;
-
- @Column(name = "area_code")
- private String areaCode;
-
- @Column(name = "arrange")
- private String arrange;
-
- @Column(name = "cat1")
- private String cat1;
-
- @Column(name = "cat2")
- private String cat2;
-
- @Column(name = "cat3")
- private String cat3;
-
- @Column(name = "content_type_id")
- private String contentTypeId;
-
- @Column(name = "event_start_date")
- private String eventStartDate;
-
- @Column(name = "keyword")
- private String keyword;
-
- @Column(name = "list_yn")
- private String listYN;
-
- @Column(name = "map_x")
- private String mapX;
-
- @Column(name = "map_y")
- private String mapY;
-
- @Column(name = "mobile_app")
- private String mobileApp;
-
- @Column(name = "mobile_os")
- private String mobileOS;
-
- @Column(name = "modified_time")
- private String modifiedTime;
-
- @Column(name = "num_of_rows")
- private Integer numOfRows;
-
- @Column(name = "page_no")
- private Integer pageNo;
-
- @Column(name = "radius")
- private String radius;
-
- @Column(name = "service_key")
- private String serviceKey;
-
- @Column(name = "sigungu_code")
- private String sigunguCode;
-
-}
diff --git a/src/main/java/com/minizin/travel/tour/domain/entity/TourAPIResponse.java b/src/main/java/com/minizin/travel/tour/domain/entity/TourAPIResponse.java
deleted file mode 100644
index 2811025..0000000
--- a/src/main/java/com/minizin/travel/tour/domain/entity/TourAPIResponse.java
+++ /dev/null
@@ -1,133 +0,0 @@
-package com.minizin.travel.tour.domain.entity;
-
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.Table;
-
-/**
- * Class: TourAPIResponse Project: travel Package: com.minizin.travel.tour.domain.entity
- *
- * Description: TourAPIResponse
- *
- * @author dong-hoshin
- * @date 6/3/24 19:46 Copyright (c) 2024 MiniJin
- * @see GitHub Repository
- */
-
-@Entity
-@Table(name = "tour_api_response")
-public class TourAPIResponse {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- @Column(name = "response_id", nullable = false)
- private Long responseId;
-
- @Column(name = "addr1")
- private String addr1;
-
- @Column(name = "addr2")
- private String addr2;
-
- @Column(name = "area_code")
- private String areaCode;
-
- @Column(name = "benikia")
- private String benikia;
-
- @Column(name = "booktour")
- private String booktour;
-
- @Column(name = "cat1")
- private String cat1;
-
- @Column(name = "cat2")
- private String cat2;
-
- @Column(name = "cat3")
- private String cat3;
-
- @Column(name = "code")
- private String code;
-
- @Column(name = "content_id")
- private String contentId;
-
- @Column(name = "content_type_id")
- private String contentTypeId;
-
- @Column(name = "cpyrht_div_cd")
- private String cpyrhtDivCd;
-
- @Column(name = "created_time")
- private String createdTime;
-
- @Column(name = "dist")
- private String dist;
-
- @Column(name = "event_start_date")
- private String eventStartDate;
-
- @Column(name = "first_image")
- private String firstImage;
-
- @Column(name = "first_image2")
- private String firstImage2;
-
- @Column(name = "good_stay")
- private String goodStay;
-
- @Column(name = "hanok")
- private String hanok;
-
- @Column(name = "mapx")
- private String mapx;
-
- @Column(name = "mapy")
- private String mapy;
-
- @Column(name = "mlevel")
- private String mlevel;
-
- @Column(name = "modified_time")
- private String modifiedTime;
-
- @Column(name = "name")
- private String name;
-
- @Column(name = "num_of_rows")
- private Integer numOfRows;
-
- @Column(name = "page_no")
- private Integer pageNo;
-
- @Column(name = "result_code")
- private String resultCode;
-
- @Column(name = "result_msg")
- private String resultMsg;
-
- @Column(name = "rnum")
- private String rnum;
-
- @Column(name = "sigungu_code")
- private String sigunguCode;
-
- @Column(name = "tel")
- private String tel;
-
- @Column(name = "title")
- private String title;
-
- @Column(name = "total_cnt")
- private Integer totalCnt;
-
- @Column(name = "total_count")
- private Integer totalCount;
-
- @Column(name = "zipcode")
- private String zipcode;
-
-}
diff --git a/src/main/java/com/minizin/travel/tour/domain/repository/TourAPIRepository.java b/src/main/java/com/minizin/travel/tour/domain/repository/TourAPIRepository.java
new file mode 100644
index 0000000..dace5d2
--- /dev/null
+++ b/src/main/java/com/minizin/travel/tour/domain/repository/TourAPIRepository.java
@@ -0,0 +1,55 @@
+package com.minizin.travel.tour.domain.repository;
+
+import com.minizin.travel.tour.domain.dto.TourAPIDto;
+import com.minizin.travel.tour.domain.entity.TourAPI;
+import io.lettuce.core.dynamic.annotation.Param;
+import jakarta.persistence.Tuple;
+import java.util.List;
+import java.util.Optional;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+/**
+ * Class: TourAPIRequestRepository Project: travel Package:
+ * com.minizin.travel.tour.domain.repository
+ *
+ * Description: TourAPIRequestRepository
+ *
+ * @author dong-hoshin
+ * @date 6/3/24 20:00 Copyright (c) 2024 MiniJin
+ * @see GitHub Repository
+ */
+@Repository
+public interface TourAPIRepository extends JpaRepository {
+
+ // TourAPI 에서 중복 값 없이(DISTINCT) Null 값 & 빈 값('') 제외하고 가져오기.
+ @Query(value = "SELECT DISTINCT t FROM TourAPI t WHERE t.code IS NOT NULL AND t.code != '' GROUP BY t.code,t.sigunguCode")
+ List findDistinctAreaCode();
+
+ @Query("SELECT DISTINCT t FROM TourAPI t WHERE t.areaCode = :areaCode GROUP BY t.contentId")
+ List findDistinctAreaBasedList(@Param("areaCode") String areaCode);
+
+ /*@Query("SELECT DISTINCT t FROM TourAPI t WHERE " +
+ "(t.areaCode = :areaCode) OR " +
+ "(t.contentTypeId = :contentTypeId) OR " +
+ "(t.sigunguCode = :sigunguCode) " )
+ List findDistinctSearchKeyword(@Param("areaCode") String areaCode,
+ @Param("contentTypeId") String contentTypeId,
+ @Param("sigunguCode") String sigunguCode,
+ Pageable pageable);*/
+
+ @Query("SELECT DISTINCT t FROM TourAPI t WHERE t.addr1 LIKE CONCAT('%', :keyword, '%') OR t.title LIKE CONCAT('%', :keyword, '%') GROUP BY t.contentId ORDER BY t.areaCode,t.sigunguCode,t.contentTypeId" )
+ List findDistinctSearchKeyword(@Param("keyword") String keyword);
+
+ @Query("SELECT DISTINCT t FROM TourAPI t WHERE t.contentId IS NOT NULL AND t.contentId != '' AND t.overview = '' GROUP BY t.contentId")
+ Page findAll(Pageable pageable);
+
+ @Query("SELECT DISTINCT t FROM TourAPI t WHERE t.contentId = :contentId GROUP BY t.contentId")
+ List findByContentId(@Param("contentId") String contentId);
+ @Query("SELECT DISTINCT t FROM TourAPI t WHERE t.contentId IS NOT NULL AND t.contentId != '' GROUP BY t.contentId")
+ List findAllList();
+
+}
diff --git a/src/main/java/com/minizin/travel/tour/domain/repository/TourAPIRequestRepository.java b/src/main/java/com/minizin/travel/tour/domain/repository/TourAPIRequestRepository.java
deleted file mode 100644
index c2ef3a2..0000000
--- a/src/main/java/com/minizin/travel/tour/domain/repository/TourAPIRequestRepository.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.minizin.travel.tour.domain.repository;
-
-import com.minizin.travel.tour.domain.entity.TourAPIRequest;
-import java.util.Optional;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.stereotype.Repository;
-
-/**
- * Class: TourAPIRequestRepository Project: travel Package:
- * com.minizin.travel.tour.domain.repository
- *
- * Description: TourAPIRequestRepository
- *
- * @author dong-hoshin
- * @date 6/3/24 20:00 Copyright (c) 2024 MiniJin
- * @see GitHub Repository
- */
-@Repository
-public interface TourAPIRequestRepository extends JpaRepository {
- Optional findByRequestId(Long RequestId);
-
-}
diff --git a/src/main/java/com/minizin/travel/tour/domain/repository/TourAPIResponseRepository.java b/src/main/java/com/minizin/travel/tour/domain/repository/TourAPIResponseRepository.java
deleted file mode 100644
index 83b1551..0000000
--- a/src/main/java/com/minizin/travel/tour/domain/repository/TourAPIResponseRepository.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.minizin.travel.tour.domain.repository;
-
-import com.minizin.travel.tour.domain.entity.TourAPIResponse;
-import java.util.Optional;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.stereotype.Repository;
-
-/**
- * Class: TourAPIResponseRepository Project: travel Package:
- * com.minizin.travel.tour.domain.repository
- *
- * Description: TourAPIResponseRepository
- *
- * @author dong-hoshin
- * @date 6/3/24 20:03 Copyright (c) 2024 MiniJin
- * @see GitHub Repository
- */
-@Repository
-public interface TourAPIResponseRepository extends JpaRepository {
- Optional findByResponseId(Long ResponseId);
-}
diff --git a/src/main/java/com/minizin/travel/tour/service/TourInfoService.java b/src/main/java/com/minizin/travel/tour/service/TourInfoService.java
new file mode 100644
index 0000000..34958fd
--- /dev/null
+++ b/src/main/java/com/minizin/travel/tour/service/TourInfoService.java
@@ -0,0 +1,224 @@
+package com.minizin.travel.tour.service;
+
+import com.minizin.travel.tour.domain.dto.TourAPIDto;
+import com.minizin.travel.tour.domain.entity.TourAPI;
+import com.minizin.travel.tour.domain.repository.TourAPIRepository;
+import java.io.IOException;
+import java.text.Collator;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+/**
+ * Class: TourInfoService Project: travel Package: com.minizin.travel.tour.service
+ *
+ * Description: TourInfoService
+ *
+ * @author dong-hoshin
+ * @date 6/16/24 13:49 Copyright (c) 2024 MiniJin
+ * @see GitHub Repository
+ */
+@Service
+@RequiredArgsConstructor
+public class TourInfoService {
+
+ private final TourAPIRepository tourAPIRepository;
+
+ public TourAPIDto getTourDataByAreaCode(TourAPIDto.TourRequest requestUrl) throws IOException {
+ int pageNo = 0;
+ String areaCode = Optional.ofNullable(requestUrl.getAreaCode()).orElse("");
+
+ // 데이터베이스에서 중복 제거된 데이터 가져오기
+ List rawEntities = tourAPIRepository.findDistinctAreaCode();
+ int numOfRows = 100;
+
+ List rawItems = rawEntities.stream()
+ .filter(tourAPI -> {
+ // areaCode 있을 때 (sigunguCode가 존재 && areaCode와 동일한 Code) Data만 가져오기.
+ if (!areaCode.isEmpty()) {
+ return !tourAPI.getSigunguCode().isEmpty() && tourAPI.getCode().equals(areaCode);
+ //areaCode가 없을 때 (sigunguCode가 없는) data가져오기.
+ } else {
+ return tourAPI.getSigunguCode().isEmpty();
+ }
+ })
+ .map(TourAPI::toDto)
+ .collect(Collectors.toList());
+
+ // 응답 객체 생성
+ TourAPIDto responseDto = buildResponseWithTourAPIDto(rawItems, pageNo,numOfRows);
+
+ return responseDto;
+ }
+
+ public TourAPIDto getTourDataByDetailCommon(TourAPIDto.TourRequest requestUrl) throws IOException {
+ String pageNo = Optional.ofNullable(requestUrl.getPageNo()).orElse("0");
+ String numOfRows = Optional.ofNullable(requestUrl.getNumOfRows()).orElse("10");
+ String contentId = Optional.ofNullable(requestUrl.getContentId()).orElse("");
+
+ int page = Integer.parseInt(pageNo);
+ int size = Integer.parseInt(numOfRows);
+
+ List rawEntities;
+ // 데이터베이스에서 중복 제거된 데이터 가져오기
+ // contentId가 있을 때는 contentId 기반으로 가져오고
+ // 없을 때는 모든 자료를 가져오기.
+ if (contentId != "") {
+ rawEntities = tourAPIRepository.findByContentId(contentId);
+ } else {
+ rawEntities = tourAPIRepository.findAllList();
+ }
+
+ List rawItems = rawEntities.stream()
+ .map(TourAPI::toDto)
+ .collect(Collectors.toList());
+
+ List sortedItems = sortItemsByTitle(rawItems);
+
+ TourAPIDto responseDto = buildResponseWithTourAPIDto(sortedItems, page, size);
+
+ return responseDto;
+ }
+
+ public TourAPIDto getTourDataByAreaBasedList(TourAPIDto.TourRequest requestUrl) throws IOException {
+ String pageNo = Optional.ofNullable(requestUrl.getPageNo()).orElse("0");
+ String numOfRows = Optional.ofNullable(requestUrl.getNumOfRows()).orElse("10");
+ String areaCode = Optional.ofNullable(requestUrl.getAreaCode()).orElse("");
+ String contentTypeId = Optional.ofNullable(requestUrl.getContentTypeId()).orElse("");
+ String sigunguCode = Optional.ofNullable(requestUrl.getSigunguCode()).orElse("");
+
+ int page = Integer.parseInt(pageNo);
+ int size = Integer.parseInt(numOfRows);
+ List rawEntities;
+
+ // 데이터베이스에서 중복 제거된 데이터 가져오기
+ // areaCode가 있을 때는 areaCode기반으로 가져오고
+ // 없을 때는 모든 자료를 가져오기.
+ if (areaCode != "") {
+ rawEntities = tourAPIRepository.findDistinctAreaBasedList(areaCode);
+ } else {
+ rawEntities = tourAPIRepository.findAllList();
+ }
+ List rawItems = rawEntities.stream()
+ .filter(tourAPI ->
+ // 변수값이 비어있을 때는 오른쪽 filter조건을 건너뜀
+ // 변수값이 비어있지 않은 조건들을 모두 만족 시킨 데이터만 필터링함.
+ (areaCode.isEmpty() || tourAPI.getAreaCode().equals(areaCode)) &&
+ (contentTypeId.isEmpty() || tourAPI.getContentTypeId().equals(contentTypeId)) &&
+ (sigunguCode.isEmpty() || tourAPI.getSigunguCode().equals(sigunguCode))
+ )
+ .map(TourAPI::toDto)
+ .collect(Collectors.toList());
+
+ List sortedItems = sortItemsByTitle(rawItems);
+
+ TourAPIDto responseDto = buildResponseWithTourAPIDto(sortedItems, page, size);
+
+ return responseDto;
+ }
+
+ public TourAPIDto getTourDataSearchKeyword(TourAPIDto.TourRequest requestUrl) throws IOException {
+ String pageNo = Optional.ofNullable(requestUrl.getPageNo()).orElse("0");
+ String numOfRows = Optional.ofNullable(requestUrl.getNumOfRows()).orElse("10");
+ String keyword = Optional.ofNullable(requestUrl.getKeyword()).orElse("");
+ String areaCode = Optional.ofNullable(requestUrl.getAreaCode()).orElse("");
+ String contentTypeId = Optional.ofNullable(requestUrl.getContentTypeId()).orElse("");
+ String sigunguCode = Optional.ofNullable(requestUrl.getSigunguCode()).orElse("");
+
+ int page = Integer.parseInt(pageNo);
+ int size = Integer.parseInt(numOfRows);
+
+ // 데이터베이스에서 중복 제거된 데이터 가져오기
+ List rawEntities = tourAPIRepository.findAllList();
+
+ // 변수값이 비어있을 때는 오른쪽 filter조건을 건너뜀
+ // 변수값이 비어있지 않은 조건들을 모두 만족 시킨 데이터만 필터링함.
+ List rawItems = rawEntities.stream()
+ .filter(tourAPI ->
+ (areaCode.isEmpty() || tourAPI.getAreaCode().equals(areaCode)) &&
+ (contentTypeId.isEmpty() || tourAPI.getContentTypeId().equals(contentTypeId)) &&
+ (sigunguCode.isEmpty() || tourAPI.getSigunguCode().equals(sigunguCode)) &&
+ (keyword.isEmpty() || (tourAPI.getAddr1().contains(keyword) || tourAPI.getTitle().contains(keyword)))
+ )
+ .map(TourAPI::toDto)
+ .collect(Collectors.toList());
+
+ List sortedItems = sortItemsByTitle(rawItems);
+
+ TourAPIDto responseDto = buildResponseWithTourAPIDto(sortedItems, page, size);
+
+ return responseDto;
+ }
+
+ private List sortItemsByTitle(List items) {
+ Collator koreanCollator = Collator.getInstance(Locale.KOREAN);
+
+ Comparator customComparator = Comparator
+ //한글, 영문자, 숫자, 기타 순으로 정렬.
+ .comparing((TourAPIDto.TourResponse.Body.Items.Item item) -> {
+ String title = item.getTitle();
+ if (title.matches("^[가-힣ㄱ-ㅎ].*")) return 0;
+ if (title.matches("^[a-zA-Z].*")) return 1;
+ if (title.matches("^[0-9].*")) return 2;
+ return 3;
+ })
+ // 각 그룹 내 정렬 기준에 맞춰 정렬
+ .thenComparing((item1, item2) -> {
+ String title1 = item1.getTitle();
+ String title2 = item2.getTitle();
+ if (title1.matches("^[가-힣ㄱ-ㅎ].*") && title2.matches("^[가-힣ㄱ-ㅎ].*")) {
+ return koreanCollator.compare(title1, title2);
+ }
+ if (title1.matches("^[a-zA-Z].*") && title2.matches("^[a-zA-Z].*")) {
+ return title1.compareToIgnoreCase(title2);
+ }
+ return title1.compareTo(title2);
+ });
+
+ return items.stream()
+ .sorted(customComparator)
+ .collect(Collectors.toList());
+ }
+
+
+ private TourAPIDto buildResponseWithTourAPIDto(List itemList, int pageNo, int numOfRows) {
+ // 객체 page별로 반환
+ int start = Math.min(pageNo * numOfRows, itemList.size());
+ int end = Math.min((pageNo + 1) * numOfRows, itemList.size());
+ List pagedItems = itemList.subList(start, end);
+
+ // Items 객체로 변환
+ TourAPIDto.TourResponse.Body.Items items = TourAPIDto.TourResponse.Body.Items.builder()
+ .item(pagedItems)
+ .build();
+
+ // Body 객체 생성
+ TourAPIDto.TourResponse.Body body = TourAPIDto.TourResponse.Body.builder()
+ .items(items)
+ .numOfRows(numOfRows)
+ .pageNo(pageNo)
+ .totalCount(itemList.size())
+ .build();
+
+ // Header 객체 생성
+ TourAPIDto.TourResponse.Header header = TourAPIDto.TourResponse.Header.builder()
+ .resultCode("0000")
+ .resultMsg("OK")
+ .build();
+
+ // 최종 응답 객체 생성
+ return TourAPIDto.builder()
+ .response(TourAPIDto.TourResponse.builder()
+ .header(header)
+ .body(body)
+ .build())
+ .build();
+ }
+
+}
diff --git a/src/main/java/com/minizin/travel/tour/service/TourService.java b/src/main/java/com/minizin/travel/tour/service/TourService.java
new file mode 100644
index 0000000..d609a58
--- /dev/null
+++ b/src/main/java/com/minizin/travel/tour/service/TourService.java
@@ -0,0 +1,350 @@
+package com.minizin.travel.tour.service;
+
+
+import com.minizin.travel.tour.domain.dto.TourAPIDto;
+import com.minizin.travel.tour.domain.entity.TourAPI;
+import com.minizin.travel.tour.domain.repository.TourAPIRepository;
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import okhttp3.HttpUrl;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Service;
+import okhttp3.OkHttpClient;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Class: TourService Project: travel Package: com.minizin.travel.tour.service
+ *
+ * Description: TourService
+ *
+ * @author dong-hoshin
+ * @date 6/5/24 01:04 Copyright (c) 2024 MiniJin
+ * @see GitHub Repository
+ */
+@Service
+@RequiredArgsConstructor
+public class TourService {
+
+ @Value("${api-tour.serviceKey_De}")
+ public String serviceKey;
+ private final OkHttpClient client = new OkHttpClient.Builder()
+ .connectTimeout(6000, TimeUnit.SECONDS)
+ .readTimeout(6000, TimeUnit.SECONDS)
+ .writeTimeout(6000, TimeUnit.SECONDS)
+ .build();
+ private final Gson gson = new Gson();
+ private final String baseUrl = "https://apis.data.go.kr/B551011/KorService1/";
+ private final TourAPIRepository tourAPIRepository;
+ private final ExecutorService executorService = Executors.newFixedThreadPool(10);
+ Logger logger = LoggerFactory.getLogger(getClass());
+ private static final int MAX_RETRIES = 3;
+
+ public CompletableFuture getTourAPIFromSiteDetailCommon(TourAPIDto.TourRequest requestParam) {
+ String getCategoryUrl = baseUrl + "detailCommon1";
+ // 기존 TourAPI entity(DB)에 detail 정보 추가해서 DB업데이트 하기.
+ long startTime = System.currentTimeMillis();
+ int pageNo = Integer.parseInt(Optional.ofNullable(requestParam.getPageNo()).orElse("0"));
+ int pageSize = Integer.parseInt(Optional.ofNullable(requestParam.getNumOfRows()).orElse("1000"));
+
+ PageRequest pageRequest = PageRequest.of(pageNo, pageSize);
+ // overview 정보가 추가 안된 데이터만 중복값 없이 가져오기
+ // (pageable 한 이유는 간혹 DB 업데이트가 안되는 data들을 page를 통해 건너뛰기 위함)
+ Page tourAPIPage = tourAPIRepository.findAll(pageRequest);
+ List tourAPIs = tourAPIPage.getContent();
+
+ //가져온 TourAPI 목록의 모든 데이터들을 비동기 방식으로 공공데이터 api에 정보 요청
+ List> futures = tourAPIs.stream()
+ .map(tourAPI -> {
+ String contentId = tourAPI.getContentId();
+ String contentTypeId = tourAPI.getContentTypeId();
+
+ Map params = Map.ofEntries(
+ Map.entry("ServiceKey", serviceKey),
+ Map.entry("MobileOS", "ETC"),
+ Map.entry("MobileApp", "AppTest"),
+ Map.entry("_type", "json"),
+ Map.entry("contentId", contentId),
+ Map.entry("contentTypeId", contentTypeId),
+ Map.entry("defaultYN", "Y"),
+ Map.entry("firstImageYN", "Y"),
+ Map.entry("areacodeYN", "Y"),
+ Map.entry("catcodeYN", "Y"),
+ Map.entry("addrinfoYN", "Y"),
+ Map.entry("mapinfoYN", "Y"),
+ Map.entry("overviewYN", "Y"),
+ Map.entry("pageNo", "0")
+ );
+ // 각 Entity Data별로 공공데이터 Request url에 맞춰 request param을 build 하기.
+ String url = buildUrlWithParams(getCategoryUrl, params);
+ // 각 Entity Data별로 공공데이터에 요청한 detail정보 update하기.
+ return fetchAndUpdateTourAPIData(url, tourAPI);
+ })
+ .collect(Collectors.toList());
+
+ return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
+ .thenApply(v -> {
+ long endTime = System.currentTimeMillis();
+ long duration = endTime - startTime;
+ logger.info("getTourAPIFromSiteDetailCommon executed in " + duration + " ms");
+ if (!tourAPIs.isEmpty()) {
+ // 비동기 방식으로 가져온 모든 업데이트된 정보들을 한번에 저장.
+ tourAPIRepository.saveAll(tourAPIs);
+ }
+ return "getTourAPIFromSiteDetailCommon executed successfully";
+ });
+ }
+ public CompletableFuture> getTourAPIFromSiteSearchKeyword(TourAPIDto.TourRequest requestParam) {
+ //baseUrl에 공공데이터의 request url에 맞춰 카테고리url 넣기
+ // + requestParam에 들어간 keyword, pageNo,numOfRows에 맞춰 공공데이터에서 값 가져오기
+ String getCategoryUrl = baseUrl + "searchKeyword1";
+ String keyword = Optional.ofNullable(requestParam.getKeyword()).orElse("강원");
+ String pageNo = Optional.ofNullable(requestParam.getPageNo()).orElse("0");
+ String numOfRows = Optional.ofNullable(requestParam.getNumOfRows()).orElse("1000");
+
+ Map params = Map.of(
+ "ServiceKey", serviceKey,
+ "MobileOS", "ETC",
+ "MobileApp", "AppTest",
+ "_type", "json",
+ "keyword", keyword,
+ "pageNo",pageNo,
+ "numOfRows", numOfRows
+ );
+
+ String url = buildUrlWithParams(getCategoryUrl, params);
+
+ return getListCompletableFuture(url);
+ }
+
+ public CompletableFuture> getTourAPIFromSiteAreaBasedList(TourAPIDto.TourRequest requestParam) {
+ String getCategoryUrl = baseUrl + "areaBasedList1";
+ long startTime = System.currentTimeMillis();
+ String[] areaCodes = {"1","2","3","4","5","6","7","8","31","32","33","34","35","36","37","38","39"};
+ String[] contentTypeIds = {"12","14","15","25","28","32","38","39"};
+ String pageNo = Optional.ofNullable(requestParam.getPageNo()).orElse("0");
+ String numOfRows = Optional.ofNullable(requestParam.getNumOfRows()).orElse("1000");
+ /* int[] sigunguCodes = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
+ 27, 28, 29, 30, 31, 99}; 같은 시군구 코드에 다른지역 표기가 있어 제외*/
+
+ // stream(contentTypeIds) or Arrays.stream(areaCodes) 로 활용
+ List>> futures = Arrays.stream(areaCodes)
+ .flatMap(areaCode -> Arrays.stream(contentTypeIds)
+ .map(contentTypeId -> {
+ Map params = Map.of(
+ "ServiceKey", serviceKey,
+ "MobileOS", "ETC",
+ "MobileApp", "AppTest",
+ "_type", "json",
+ "areaCode", areaCode,
+ "numOfRows", numOfRows,
+ "pageNo", pageNo,
+ "contentTypeId", contentTypeId
+ );
+
+ String url = buildUrlWithParams(getCategoryUrl, params);
+ return getListCompletableFuture(url);
+ })
+ ).collect(Collectors.toList());
+
+ return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
+ .thenApply(v -> {
+ long endTime = System.currentTimeMillis();
+ long duration = endTime - startTime;
+ logger.info("getTourAPIFromSiteAreaBasedList executed in " + duration + " ms");
+ return futures.stream()
+ .map(CompletableFuture::join)
+ .flatMap(List::stream)
+ .collect(Collectors.toList());
+ });
+ }
+
+ public CompletableFuture> getTourAPIFromSiteAreaBasedListSingle(TourAPIDto.TourRequest requestParam) {
+ String getCategoryUrl = baseUrl + "areaBasedList1";
+ String pageNo = Optional.ofNullable(requestParam.getPageNo()).orElse("0");
+ String numOfRows = Optional.ofNullable(requestParam.getNumOfRows()).orElse("10");
+ String areaCode = Optional.ofNullable(requestParam.getAreaCode()).orElse("");
+ String contentTypeId = Optional.ofNullable(requestParam.getContentTypeId()).orElse("");
+ String sigunguCode = Optional.ofNullable(requestParam.getSigunguCode()).orElse("");
+
+ Map params = Map.of(
+ "ServiceKey", serviceKey,
+ "MobileOS", "ETC",
+ "MobileApp", "AppTest",
+ "_type", "json",
+ "areaCode",areaCode,
+ "contentTypeId",contentTypeId,
+ "sigunguCode",sigunguCode,
+ "pageNo", pageNo,
+ "numOfRows", numOfRows
+ );
+
+ String url = buildUrlWithParams(getCategoryUrl, params);
+
+ return getListCompletableFuture(url);
+ }
+ public CompletableFuture> getTourAPIFromSiteAreaCode(TourAPIDto.TourRequest requestParam) {
+ String getCategoryUrl = baseUrl + "areaCode1";
+ String areaCode = Optional.ofNullable(requestParam.getAreaCode()).orElse("");
+ // areaCode 가 비어있으면 code(true) areaCode가 있으면 Sigungu(false)
+ boolean codeOrSigungu = areaCode.isEmpty();
+ // areaCode 가 비어있을 때 모든 특별시, 도를 검색 (true)
+ // areaCode가 있을 때는 해당 areaCode의 sigungu를 검색 (false)
+
+ Map params = Map.of(
+ "ServiceKey", serviceKey,
+ "MobileOS", "ETC",
+ "MobileApp", "AppTest",
+ "_type", "json",
+ "numOfRows","100",
+ "areaCode",areaCode
+ );
+
+ String url = buildUrlWithParams(getCategoryUrl, params);
+
+ return getListCompletableFutureForArea(url, areaCode, codeOrSigungu);
+ }
+
+ @NotNull
+ private CompletableFuture> getListCompletableFuture(String url) {
+ // 공공데이터 request url에 맞춰 build된 url에 json형식으로 header 추가
+ Request request = new Request.Builder()
+ .url(url)
+ .get()
+ .addHeader("Content-type", "application/json")
+ .build();
+
+ //build된 url을 기반으로 공공데이터 비동기 방식으로 response 가져오기 -> Entity 저장
+ return CompletableFuture.supplyAsync(() -> {
+ try (Response response = client.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ throw new IOException("Unexpected code " + response);
+ }
+
+ String responseJson = response.body().string();
+
+// logger.info("Response JSON: " + responseJson); // Log the JSON response
+
+ // items이 비어있을 때 Error Msg없이 emptyList 출력 - 이 코드가 없으면 error 발생
+ if (responseJson.contains("\"items\": \"\"")) {
+ return Collections.emptyList();
+ }
+
+ TourAPIDto tourAPIDto = gson.fromJson(responseJson, TourAPIDto.class);
+
+ List tourAPIList = tourAPIDto.toEntityList();
+
+ if (!tourAPIList.isEmpty()) {
+ tourAPIRepository.saveAll(tourAPIList);
+ return tourAPIList;
+ }
+ } catch (IOException | JsonSyntaxException e) {
+ if (!e.getMessage().contains("\"items\": \"\",")) {
+ logger.error("JSON parsing error: " + e.getMessage());
+ }
+ }
+ return Collections.emptyList();
+ }, executorService);
+ }
+
+ private String buildUrlWithParams(String baseUrl, Map params) {
+ HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
+ params.forEach((key, value) -> urlBuilder.addQueryParameter(key, value));
+ return urlBuilder.build().toString();
+ }
+
+ private CompletableFuture fetchAndUpdateTourAPIData(String url, TourAPI existingTourAPI) {
+ Request request = new Request.Builder()
+ .url(url)
+ .get()
+ .addHeader("Content-type", "application/json")
+ .build();
+
+ return CompletableFuture.runAsync(() -> {
+ try (Response response = client.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ throw new IOException("Unexpected code " + response);
+ }
+ String responseJson = response.body().string();
+ logger.debug("Response JSON: " + responseJson);
+
+ // JSON 파싱 및 데이터 형식 검증
+ TourAPIDto tourAPIDto = gson.fromJson(responseJson, TourAPIDto.class);
+
+ TourAPIDto.TourResponse.Body.Items.Item item = tourAPIDto.getResponse()
+ .getBody()
+ .getItems()
+ .getItem()
+ .get(0);
+ // 공공데이터에서 가져온 data를 기존 Entity Data(TourAPI)에 업데이트
+ existingTourAPI.updateFromDtoAsync(item);
+ } catch (IOException | JsonSyntaxException e) {
+ logger.error("Error fetching data for URL: " + url, e);
+ throw new CompletionException(e);
+ }
+ }, executorService);
+ }
+
+ @NotNull
+ private CompletableFuture> getListCompletableFutureForArea(String url,String areaCode,boolean codeOrSigungu) {
+ Request request = new Request.Builder()
+ .url(url)
+ .get()
+ .addHeader("Content-type", "application/json")
+ .build();
+
+ return CompletableFuture.supplyAsync(() -> {
+ try (Response response = client.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ throw new IOException("Unexpected code " + response);
+ }
+
+ String responseJson = response.body().string();
+
+// logger.info("Response JSON: " + responseJson); // Log the JSON response
+
+ // items이 비어있을 때 Error Msg없이 emptyList 출력 - 이 코드가 없으면 error 발생
+ if (responseJson.contains("\"items\": \"\"")) {
+ return Collections.emptyList();
+ }
+
+ TourAPIDto tourAPIDto = gson.fromJson(responseJson, TourAPIDto.class);
+
+ List tourAPIList = tourAPIDto.toEntityListArea(areaCode,codeOrSigungu);
+
+ if (!tourAPIList.isEmpty()) {
+ tourAPIRepository.saveAll(tourAPIList);
+ return tourAPIList;
+ }
+
+ } catch (IOException | JsonSyntaxException e) {
+ if (!e.getMessage().contains("\"items\": \"\",")) {
+ logger.error("JSON parsing error: " + e.getMessage());
+ }
+ }
+ return Collections.emptyList();
+ }, executorService);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/minizin/travel/user/auth/LoginFilter.java b/src/main/java/com/minizin/travel/user/auth/LoginFilter.java
new file mode 100644
index 0000000..3305fd9
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/auth/LoginFilter.java
@@ -0,0 +1,75 @@
+package com.minizin.travel.user.auth;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.minizin.travel.user.domain.dto.ErrorResponse;
+import com.minizin.travel.user.domain.dto.PrincipalDetails;
+import com.minizin.travel.user.domain.enums.Role;
+import com.minizin.travel.user.jwt.TokenProvider;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+
+import java.io.IOException;
+
+public class LoginFilter extends UsernamePasswordAuthenticationFilter {
+
+ private final AuthenticationManager authenticationManager;
+ private final TokenProvider tokenProvider;
+
+ public LoginFilter(AuthenticationManager authenticationManager, TokenProvider tokenProvider) {
+ // 로그인 path 설정
+ this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/auth/login", "POST"));
+ this.authenticationManager = authenticationManager;
+ this.tokenProvider = tokenProvider;
+ }
+
+ @Override
+ public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
+
+ String username = obtainUsername(request);
+ String password = obtainPassword(request);
+
+ UsernamePasswordAuthenticationToken authToken =
+ new UsernamePasswordAuthenticationToken(username, password);
+
+ return authenticationManager.authenticate(authToken);
+ }
+
+ //로그인 성공시 실행하는 메소드 (여기서 JWT 발급)
+ @Override
+ protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
+
+ PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
+
+ String username = principalDetails.getUsername();
+ Role role = principalDetails.getUserEntity().getRole();
+
+ String token = tokenProvider.createJwt(username, role.toString(), 24 * 60 * 60 * 1000L);
+
+ response.addHeader("Authorization", "Bearer " + token);
+ }
+
+ //로그인 실패시 실행하는 메소드
+ @Override
+ protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
+ response.setContentType("application/json");
+ response.setCharacterEncoding("utf-8");
+
+ ErrorResponse errorResponse = new ErrorResponse(
+ HttpStatus.UNAUTHORIZED, "인증에 실패했습니다."
+ );
+
+ ObjectMapper objectMapper = new ObjectMapper();
+ String result = objectMapper.writeValueAsString(errorResponse);
+
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ response.getWriter().write(result);
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/controller/AuthController.java b/src/main/java/com/minizin/travel/user/controller/AuthController.java
new file mode 100644
index 0000000..ca63278
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/controller/AuthController.java
@@ -0,0 +1,43 @@
+package com.minizin.travel.user.controller;
+
+import com.minizin.travel.user.domain.dto.JoinDto;
+import com.minizin.travel.user.service.AuthService;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequiredArgsConstructor
+public class AuthController {
+
+ private final AuthService authService;
+
+ @GetMapping("/auth/jwt")
+ public ResponseEntity> getJwt(HttpServletRequest request, HttpServletResponse response) {
+ Cookie[] cookies = request.getCookies();
+ if (cookies != null) {
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals("Authorization")) {
+ String token = cookie.getValue();
+ response.setHeader("Authorization", "Bearer " + token);
+ return ResponseEntity.ok("JWT 헤더로 발급");
+ }
+ }
+ }
+ return ResponseEntity.status(401).body("Unauthorized");
+ }
+
+ @PostMapping("/auth/join")
+ public JoinDto.Response join(
+ @RequestBody @Valid JoinDto.Request request
+ ) {
+ return authService.join(request);
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/controller/MailController.java b/src/main/java/com/minizin/travel/user/controller/MailController.java
new file mode 100644
index 0000000..8227912
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/controller/MailController.java
@@ -0,0 +1,35 @@
+package com.minizin.travel.user.controller;
+
+import com.minizin.travel.user.domain.dto.SendAuthCodeDto;
+import com.minizin.travel.user.domain.dto.VerifyAuthCodeDto;
+import com.minizin.travel.user.service.MailService;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequiredArgsConstructor
+public class MailController {
+
+ private final MailService mailService;
+
+ @PostMapping("/mails/auth-code")
+ public String sendAuthCode(
+ @RequestBody @Valid SendAuthCodeDto sendAuthCodeDto
+ ) {
+ mailService.sendAuthCode(sendAuthCodeDto);
+
+ return "인증코드가 발송되었습니다.";
+ }
+
+ @PostMapping("/mails/auth-code/verification")
+ public String verifyAuthCode(
+ @RequestBody @Valid VerifyAuthCodeDto verifyAuthCodeDto
+ ) {
+ boolean verified = mailService.verifyAuthCode(verifyAuthCodeDto);
+
+ return verified ? "인증 성공" : "인증 실패";
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/controller/UserController.java b/src/main/java/com/minizin/travel/user/controller/UserController.java
new file mode 100644
index 0000000..b875909
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/controller/UserController.java
@@ -0,0 +1,68 @@
+package com.minizin.travel.user.controller;
+
+import com.minizin.travel.user.domain.dto.*;
+import com.minizin.travel.user.service.UserService;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequiredArgsConstructor
+public class UserController {
+
+ private final UserService userService;
+
+ @GetMapping("/users")
+ public UserDto.Response getUserInfo(
+ @AuthenticationPrincipal PrincipalDetails principalDetails
+ ) {
+ return userService.getUserInfo(principalDetails);
+ }
+
+ @PostMapping("/users/find-id")
+ public FindIdDto.Response findId(
+ @RequestBody @Valid FindIdDto.Request request
+ ) {
+ return userService.findId(request);
+ }
+
+ @PostMapping("/users/find-password")
+ public String findPassword(
+ @RequestBody @Valid FindPasswordDto.Request request
+ ) {
+ userService.findPassword(request);
+ return "임시 비밀번호가 메일로 전송되었습니다.";
+ }
+
+ @PatchMapping("/users/password")
+ public UpdatePasswordDto.Response updatePassword(
+ @RequestBody @Valid UpdatePasswordDto.Request request,
+ @AuthenticationPrincipal PrincipalDetails principalDetails
+ ) {
+ return userService.updatePassword(request, principalDetails);
+ }
+
+ @PatchMapping("/users/email")
+ public UpdateEmailDto.Response updateEmail(
+ @RequestBody @Valid UpdateEmailDto.Request request,
+ @AuthenticationPrincipal PrincipalDetails principalDetails
+ ) {
+ return userService.updateEmail(request, principalDetails);
+ }
+
+ @PatchMapping("/users/nickname")
+ public UpdateNicknameDto.Response updateNickname(
+ @RequestBody @Valid UpdateNicknameDto.Request request,
+ @AuthenticationPrincipal PrincipalDetails principalDetails
+ ) {
+ return userService.updateNickname(request, principalDetails);
+ }
+
+ @DeleteMapping("/users")
+ public DeleteUserDto.Response deleteUser(
+ @AuthenticationPrincipal PrincipalDetails principalDetails
+ ) {
+ return userService.deleteUser(principalDetails);
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/dto/DeleteUserDto.java b/src/main/java/com/minizin/travel/user/domain/dto/DeleteUserDto.java
new file mode 100644
index 0000000..75e2a97
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/dto/DeleteUserDto.java
@@ -0,0 +1,25 @@
+package com.minizin.travel.user.domain.dto;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.user.domain.entity.UserEntity;
+import lombok.Builder;
+import lombok.Getter;
+
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class DeleteUserDto {
+
+ @Getter
+ @Builder
+ public static class Response {
+ private Boolean success;
+ private Long userId;
+
+ public static Response fromUserEntity(UserEntity userEntity) {
+ return Response.builder()
+ .success(true)
+ .userId(userEntity.getId())
+ .build();
+ }
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/dto/ErrorResponse.java b/src/main/java/com/minizin/travel/user/domain/dto/ErrorResponse.java
new file mode 100644
index 0000000..6e78175
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/dto/ErrorResponse.java
@@ -0,0 +1,12 @@
+package com.minizin.travel.user.domain.dto;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@RequiredArgsConstructor
+public class ErrorResponse {
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/dto/FindIdDto.java b/src/main/java/com/minizin/travel/user/domain/dto/FindIdDto.java
new file mode 100644
index 0000000..e10acff
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/dto/FindIdDto.java
@@ -0,0 +1,26 @@
+package com.minizin.travel.user.domain.dto;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+public class FindIdDto {
+ @Getter
+ @Setter
+ public static class Request {
+ @NotBlank
+ @Email
+ // 64 (local) + 1 (at) + 255 (domain) = 320
+ @Size(max = 320, message = "Email must be less than 320 characters")
+ String email;
+ }
+
+ @Getter
+ @Builder
+ public static class Response {
+ String username;
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/dto/FindPasswordDto.java b/src/main/java/com/minizin/travel/user/domain/dto/FindPasswordDto.java
new file mode 100644
index 0000000..98637b1
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/dto/FindPasswordDto.java
@@ -0,0 +1,25 @@
+package com.minizin.travel.user.domain.dto;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Getter;
+import lombok.Setter;
+
+public class FindPasswordDto {
+ @Getter
+ @Setter
+ public static class Request {
+
+ @NotBlank
+ @Size(min = 6, max = 20, message = "Username must be between 6 and 20 characters")
+ private String username;
+
+ @NotBlank
+ @Email
+ // 64 (local) + 1 (at) + 255 (domain) = 320
+ @Size(max = 320, message = "Email must be less than 320 characters")
+ String email;
+ }
+
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/dto/JoinDto.java b/src/main/java/com/minizin/travel/user/domain/dto/JoinDto.java
new file mode 100644
index 0000000..ed8e079
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/dto/JoinDto.java
@@ -0,0 +1,73 @@
+package com.minizin.travel.user.domain.dto;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.user.domain.entity.UserEntity;
+import com.minizin.travel.user.domain.enums.LoginType;
+import com.minizin.travel.user.domain.enums.Role;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class JoinDto {
+ @Getter
+ @Setter
+ public static class Request {
+ @NotBlank
+ @Size(min = 6, max = 20, message = "Username must be between 6 and 20 characters")
+ private String username;
+
+ @NotBlank
+ @Size(min = 8, max = 20, message = "Password must be between 8 and 20 characters")
+ @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!-~]).*$",
+ message = "Password must contain at least one uppercase letter," +
+ " one lowercase letter, one number, and one special character")
+ private String password;
+
+ private String nickname;
+
+ @NotBlank
+ @Email
+ // 64 (local) + 1 (at) + 255 (domain) = 320
+ @Size(max = 320, message = "Email must be less than 320 characters")
+ private String email;
+
+ }
+
+ @Getter
+ @Builder
+ public static class Response {
+ private Boolean success;
+ private Long userId;
+ private String username;
+ private String password;
+ private String nickname;
+ private String email;
+ private Role role;
+ private LoginType loginType;
+ private LocalDateTime createdAt;
+ private LocalDateTime updatedAt;
+
+ public static Response fromUserEntity(UserEntity userEntity) {
+ return Response.builder()
+ .success(true)
+ .userId(userEntity.getId())
+ .username(userEntity.getUsername())
+ .password(userEntity.getPassword())
+ .nickname(userEntity.getNickname())
+ .email(userEntity.getEmail())
+ .role(userEntity.getRole())
+ .loginType(userEntity.getLoginType())
+ .createdAt(userEntity.getCreatedAt())
+ .updatedAt(userEntity.getUpdatedAt())
+ .build();
+ }
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/dto/KakaoResponse.java b/src/main/java/com/minizin/travel/user/domain/dto/KakaoResponse.java
new file mode 100644
index 0000000..3af2517
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/dto/KakaoResponse.java
@@ -0,0 +1,36 @@
+package com.minizin.travel.user.domain.dto;
+
+import java.util.Map;
+
+public class KakaoResponse {
+
+ private final Map attributes;
+ private final Map kakaoAccount;
+ private final Map profile;
+
+ public KakaoResponse(Map attributes) {
+ this.attributes = attributes;
+ this.kakaoAccount = (Map) attributes.get("kakao_account");
+ this.profile = (Map) kakaoAccount.get("profile");
+ }
+
+ // 제공자 (Ex. naver, google, ...)
+ public String getProvider() {
+ return "kakao";
+ }
+
+ // 제공자에서 발급해주는 아이디(번호)
+ public String getProviderId() {
+ return this.attributes.get("id").toString();
+ }
+
+ // 이메일
+ public String getEmail() {
+ return this.kakaoAccount.get("email").toString();
+ }
+
+ // 프로필로 설정된 이름
+ public String getNickname() {
+ return this.profile.get("nickname").toString();
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/dto/PrincipalDetails.java b/src/main/java/com/minizin/travel/user/domain/dto/PrincipalDetails.java
new file mode 100644
index 0000000..08a5727
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/dto/PrincipalDetails.java
@@ -0,0 +1,67 @@
+package com.minizin.travel.user.domain.dto;
+
+import com.minizin.travel.user.domain.entity.UserEntity;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+
+@Getter
+@RequiredArgsConstructor
+public class PrincipalDetails implements UserDetails, OAuth2User {
+
+ private final UserEntity userEntity;
+
+ @Override
+ public Map getAttributes() {
+ return null;
+ }
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ Collection collection = new ArrayList<>();
+ collection.add((GrantedAuthority) () -> userEntity.getRole().toString());
+ return collection;
+ }
+
+ @Override
+ public String getName() {
+ return userEntity.getUsername();
+ }
+
+ @Override
+ public String getPassword() {
+ return userEntity.getPassword();
+ }
+
+ @Override
+ public String getUsername() {
+ return userEntity.getUsername();
+ }
+
+ @Override
+ public boolean isAccountNonExpired() {
+ return true;
+ }
+
+ @Override
+ public boolean isAccountNonLocked() {
+ return true;
+ }
+
+ @Override
+ public boolean isCredentialsNonExpired() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/dto/SendAuthCodeDto.java b/src/main/java/com/minizin/travel/user/domain/dto/SendAuthCodeDto.java
new file mode 100644
index 0000000..bdf18d7
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/dto/SendAuthCodeDto.java
@@ -0,0 +1,17 @@
+package com.minizin.travel.user.domain.dto;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class SendAuthCodeDto {
+ @NotBlank
+ @Email
+ // 64 (local) + 1 (at) + 255 (domain) = 320
+ @Size(max = 320, message = "Email must be less than 320 characters")
+ private String email;
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/dto/UpdateEmailDto.java b/src/main/java/com/minizin/travel/user/domain/dto/UpdateEmailDto.java
new file mode 100644
index 0000000..27aa7d3
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/dto/UpdateEmailDto.java
@@ -0,0 +1,42 @@
+package com.minizin.travel.user.domain.dto;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.user.domain.entity.UserEntity;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class UpdateEmailDto {
+ @Getter
+ @Setter
+ public static class Request {
+
+ @NotBlank
+ @Email
+ // 64 (local) + 1 (at) + 255 (domain) = 320
+ @Size(max = 320, message = "Email must be less than 320 characters")
+ private String email;
+
+ }
+
+ @Getter
+ @Builder
+ public static class Response {
+ private Boolean success;
+ private Long userId;
+ private String changedEmail;
+
+ public static Response fromUserEntity(UserEntity userEntity) {
+ return Response.builder()
+ .success(true)
+ .userId(userEntity.getId())
+ .changedEmail(userEntity.getEmail())
+ .build();
+ }
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/dto/UpdateNicknameDto.java b/src/main/java/com/minizin/travel/user/domain/dto/UpdateNicknameDto.java
new file mode 100644
index 0000000..e907410
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/dto/UpdateNicknameDto.java
@@ -0,0 +1,35 @@
+package com.minizin.travel.user.domain.dto;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.user.domain.entity.UserEntity;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class UpdateNicknameDto {
+ @Getter
+ @Setter
+ public static class Request {
+
+ private String nickname;
+
+ }
+
+ @Getter
+ @Builder
+ public static class Response {
+ private Boolean success;
+ private Long userId;
+ private String changedNickname;
+
+ public static Response fromUserEntity(UserEntity userEntity) {
+ return Response.builder()
+ .success(true)
+ .userId(userEntity.getId())
+ .changedNickname(userEntity.getNickname())
+ .build();
+ }
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/dto/UpdatePasswordDto.java b/src/main/java/com/minizin/travel/user/domain/dto/UpdatePasswordDto.java
new file mode 100644
index 0000000..63e46a9
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/dto/UpdatePasswordDto.java
@@ -0,0 +1,50 @@
+package com.minizin.travel.user.domain.dto;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.minizin.travel.user.domain.entity.UserEntity;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class UpdatePasswordDto {
+ @Getter
+ @Setter
+ public static class Request {
+
+ @NotBlank
+ @Size(min = 8, max = 20, message = "Password must be between 8 and 20 characters")
+ @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!-~]).*$",
+ message = "Password must contain at least one uppercase letter," +
+ " one lowercase letter, one number, and one special character")
+ private String originalPassword;
+
+ @NotBlank
+ @Size(min = 8, max = 20, message = "Password must be between 8 and 20 characters")
+ @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!-~]).*$",
+ message = "Password must contain at least one uppercase letter," +
+ " one lowercase letter, one number, and one special character")
+ private String changePassword;
+
+ }
+
+ @Getter
+ @Builder
+ public static class Response {
+ private Boolean success;
+ private Long userId;
+ private String changedPassword;
+
+ public static Response fromUserEntity(UserEntity userEntity) {
+ return Response.builder()
+ .success(true)
+ .userId(userEntity.getId())
+ .changedPassword(userEntity.getPassword())
+ .build();
+ }
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/dto/UserDto.java b/src/main/java/com/minizin/travel/user/domain/dto/UserDto.java
new file mode 100644
index 0000000..e028be7
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/dto/UserDto.java
@@ -0,0 +1,25 @@
+package com.minizin.travel.user.domain.dto;
+
+import com.minizin.travel.user.domain.entity.UserEntity;
+import lombok.Builder;
+import lombok.Getter;
+
+public class UserDto {
+ @Getter
+ @Builder
+ public static class Response {
+ private Long userId;
+ private String username;
+ private String email;
+ private String nickname;
+
+ public static Response fromUserEntity(UserEntity userEntity) {
+ return Response.builder()
+ .userId(userEntity.getId())
+ .username(userEntity.getUsername())
+ .email(userEntity.getEmail())
+ .nickname(userEntity.getNickname())
+ .build();
+ }
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/dto/VerifyAuthCodeDto.java b/src/main/java/com/minizin/travel/user/domain/dto/VerifyAuthCodeDto.java
new file mode 100644
index 0000000..cba172f
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/dto/VerifyAuthCodeDto.java
@@ -0,0 +1,24 @@
+package com.minizin.travel.user.domain.dto;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class VerifyAuthCodeDto {
+
+ @NotBlank
+ @Email
+ // 64 (local) + 1 (at) + 255 (domain) = 320
+ @Size(max = 320, message = "Email must be less than 320 characters")
+ private String email;
+
+ @NotBlank
+ @Pattern(regexp = "^\\d{6}$", message = "AuthCode must be 6 digits")
+ private String authCode;
+
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/entity/UserEntity.java b/src/main/java/com/minizin/travel/user/domain/entity/UserEntity.java
new file mode 100644
index 0000000..aa28783
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/entity/UserEntity.java
@@ -0,0 +1,49 @@
+package com.minizin.travel.user.domain.entity;
+
+import com.minizin.travel.user.domain.enums.LoginType;
+import com.minizin.travel.user.domain.enums.Role;
+import jakarta.persistence.*;
+import lombok.*;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor
+@Builder
+@EntityListeners(AuditingEntityListener.class)
+public class UserEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(unique = true)
+ private String username;
+
+ @Setter
+ private String password;
+
+ @Setter
+ private String email;
+
+ @Setter
+ private String nickname;
+
+ @Enumerated(EnumType.STRING)
+ private Role role;
+
+ @Enumerated(EnumType.STRING)
+ private LoginType loginType;
+
+ @CreatedDate
+ private LocalDateTime createdAt;
+
+ @LastModifiedDate
+ private LocalDateTime updatedAt;
+
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/enums/LoginType.java b/src/main/java/com/minizin/travel/user/domain/enums/LoginType.java
new file mode 100644
index 0000000..db19748
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/enums/LoginType.java
@@ -0,0 +1,6 @@
+package com.minizin.travel.user.domain.enums;
+
+public enum LoginType {
+ LOCAL,
+ KAKAO
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/enums/MailErrorCode.java b/src/main/java/com/minizin/travel/user/domain/enums/MailErrorCode.java
new file mode 100644
index 0000000..b572ab4
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/enums/MailErrorCode.java
@@ -0,0 +1,17 @@
+package com.minizin.travel.user.domain.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@RequiredArgsConstructor
+public enum MailErrorCode {
+
+ AUTH_CODE_SEND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "인증코드 발송에 실패했습니다."),
+ TEMPORARY_PASSWORD_SEND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "임시 비밀번호 발송에 실패했습니다."),
+ ;
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/enums/Role.java b/src/main/java/com/minizin/travel/user/domain/enums/Role.java
new file mode 100644
index 0000000..c332231
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/enums/Role.java
@@ -0,0 +1,5 @@
+package com.minizin.travel.user.domain.enums;
+
+public enum Role {
+ ROLE_USER,
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/enums/UserErrorCode.java b/src/main/java/com/minizin/travel/user/domain/enums/UserErrorCode.java
new file mode 100644
index 0000000..e5409ff
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/enums/UserErrorCode.java
@@ -0,0 +1,20 @@
+package com.minizin.travel.user.domain.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@RequiredArgsConstructor
+public enum UserErrorCode {
+
+ USER_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "이미 존재하는 사용자입니다."),
+ USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "존재하지 않는 사용자입니다."),
+ EMAIL_UN_REGISTERED(HttpStatus.BAD_REQUEST, "등록되지 않은 이메일입니다."),
+ USER_EMAIL_UN_MATCHED(HttpStatus.BAD_REQUEST, "사용자에게 등록된 이메일이 아닙니다."),
+ PASSWORD_UN_MATCHED(HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다."),
+ ;
+
+ private final HttpStatus status;
+ private final String message;
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/exception/CustomMailException.java b/src/main/java/com/minizin/travel/user/domain/exception/CustomMailException.java
new file mode 100644
index 0000000..a793bb8
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/exception/CustomMailException.java
@@ -0,0 +1,11 @@
+package com.minizin.travel.user.domain.exception;
+
+import com.minizin.travel.user.domain.enums.MailErrorCode;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public class CustomMailException extends RuntimeException {
+ private final MailErrorCode mailErrorCode;
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/exception/CustomUserException.java b/src/main/java/com/minizin/travel/user/domain/exception/CustomUserException.java
new file mode 100644
index 0000000..a769c8f
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/exception/CustomUserException.java
@@ -0,0 +1,11 @@
+package com.minizin.travel.user.domain.exception;
+
+import com.minizin.travel.user.domain.enums.UserErrorCode;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public class CustomUserException extends RuntimeException {
+ private final UserErrorCode userErrorCode;
+}
diff --git a/src/main/java/com/minizin/travel/user/domain/repository/UserRepository.java b/src/main/java/com/minizin/travel/user/domain/repository/UserRepository.java
new file mode 100644
index 0000000..ae4a63c
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/domain/repository/UserRepository.java
@@ -0,0 +1,19 @@
+package com.minizin.travel.user.domain.repository;
+
+import com.minizin.travel.user.domain.entity.UserEntity;
+import com.minizin.travel.user.domain.enums.LoginType;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface UserRepository extends JpaRepository {
+
+ Optional findByUsername(String username);
+
+ Boolean existsByUsername(String username);
+
+ Optional findByEmailAndLoginType(String email, LoginType loginType);
+
+}
diff --git a/src/main/java/com/minizin/travel/user/jwt/JwtAuthenticationFilter.java b/src/main/java/com/minizin/travel/user/jwt/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..47fdadd
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/jwt/JwtAuthenticationFilter.java
@@ -0,0 +1,67 @@
+package com.minizin.travel.user.jwt;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@RequiredArgsConstructor
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ private static final String TOKEN_HEADER = "Authorization";
+ private static final String TOKEN_PREFIX = "Bearer ";
+ private final TokenProvider tokenProvider;
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+
+ // 특정 경로 무시
+ String requestUri = request.getRequestURI();
+ if (requestUri.matches("^\\/login(?:\\/.*)?$") ||
+ requestUri.matches("^\\/oauth2(?:\\/.*)?$") ||
+ requestUri.matches("^\\/auth(?:\\/.*)?$") ||
+ requestUri.matches("^\\/mails\\/auth-code(?:\\/.*)?$") ||
+ requestUri.matches("^\\/users\\/find-id(?:\\/.*)?$") ||
+ requestUri.matches("^\\/users\\/find-password(?:\\/.*)?$") ||
+ requestUri.matches("^\\/plans\\/others(?:\\/.*)?$") ||
+ requestUri.matches("^\\/plans\\/popular\\/week(?:\\/.*)?$")
+ ) {
+
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ String token = resolveTokenFromRequest(request);
+
+ // 토큰이 유효하다면
+ if (StringUtils.hasText(token) && !tokenProvider.isExpired(token)) {
+
+ // 스프링 시큐리티 인증 토큰 생성
+ Authentication authToken = tokenProvider.getAuthentication(token);
+
+ // 세션에 사용자 등록
+ SecurityContextHolder.getContext().setAuthentication(authToken);
+ }
+
+ filterChain.doFilter(request, response);
+ }
+
+ private String resolveTokenFromRequest(HttpServletRequest request) {
+ String token = request.getHeader(TOKEN_HEADER);
+
+ if (!ObjectUtils.isEmpty(token) && token.startsWith(TOKEN_PREFIX)) {
+ return token.substring(TOKEN_PREFIX.length());
+ }
+
+ return null;
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/jwt/JwtExceptionFilter.java b/src/main/java/com/minizin/travel/user/jwt/JwtExceptionFilter.java
new file mode 100644
index 0000000..d1247fa
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/jwt/JwtExceptionFilter.java
@@ -0,0 +1,41 @@
+package com.minizin.travel.user.jwt;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.minizin.travel.user.domain.dto.ErrorResponse;
+import io.jsonwebtoken.JwtException;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+@Component
+public class JwtExceptionFilter extends OncePerRequestFilter {
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+ try {
+ filterChain.doFilter(request, response);
+ } catch (JwtException e) {
+ this.setErrorResponse(response, e);
+ }
+ }
+
+ private void setErrorResponse(HttpServletResponse response, Throwable e) throws IOException {
+ response.setCharacterEncoding("utf-8");
+ response.setContentType("application/json");
+
+ ErrorResponse errorResponse = new ErrorResponse(
+ HttpStatus.UNAUTHORIZED, e.getMessage()
+ );
+
+ ObjectMapper objectMapper = new ObjectMapper();
+ String result = objectMapper.writeValueAsString(errorResponse);
+
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ response.getWriter().write(result);
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/jwt/TokenProvider.java b/src/main/java/com/minizin/travel/user/jwt/TokenProvider.java
new file mode 100644
index 0000000..5648ff3
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/jwt/TokenProvider.java
@@ -0,0 +1,61 @@
+package com.minizin.travel.user.jwt;
+
+import com.minizin.travel.user.domain.dto.PrincipalDetails;
+import com.minizin.travel.user.domain.entity.UserEntity;
+import com.minizin.travel.user.domain.enums.Role;
+import io.jsonwebtoken.Jwts;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+@Component
+public class TokenProvider {
+
+ private final SecretKey secretKey;
+
+ public TokenProvider(@Value("${spring.jwt.secret}") String secret) {
+ secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
+ }
+
+ public String getUsername(String token) {
+
+ return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
+ }
+ public String getRole(String token) {
+
+ return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
+ }
+
+
+ public Boolean isExpired(String token) {
+
+ return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
+ }
+
+ public String createJwt(String username, String role, Long expiredMs) {
+ return Jwts.builder()
+ .claim("username", username)
+ .claim("role", role)
+ .issuedAt(new Date(System.currentTimeMillis()))
+ .expiration(new Date(System.currentTimeMillis() + expiredMs))
+ .signWith(secretKey)
+ .compact();
+ }
+
+ public Authentication getAuthentication(String token) {
+ UserEntity userEntity = UserEntity.builder()
+ .username(this.getUsername(token))
+ .role(Role.valueOf(this.getRole(token)))
+ .build();
+ PrincipalDetails principalDetails = new PrincipalDetails(userEntity);
+
+ return new UsernamePasswordAuthenticationToken(principalDetails,
+ "", principalDetails.getAuthorities());
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/oauth2/CustomSuccessHandler.java b/src/main/java/com/minizin/travel/user/oauth2/CustomSuccessHandler.java
new file mode 100644
index 0000000..4973b01
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/oauth2/CustomSuccessHandler.java
@@ -0,0 +1,54 @@
+package com.minizin.travel.user.oauth2;
+
+import com.minizin.travel.user.domain.dto.PrincipalDetails;
+import com.minizin.travel.user.domain.enums.Role;
+import com.minizin.travel.user.jwt.TokenProvider;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseCookie;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
+
+ private final TokenProvider tokenProvider;
+
+ @Override
+ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
+
+ //OAuth2User
+ PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
+
+ String username = principalDetails.getUsername();
+ Role role = principalDetails.getUserEntity().getRole();
+
+ String token = tokenProvider.createJwt(username, role.toString(), 24 * 60 * 60 * 1000L);
+
+ // 응답에 쿠키로 jwt 발급
+ response.addHeader("Set-Cookie", createCookie("Authorization", token).toString());
+
+ // 프론트 측 특정 uri로 리다이렉트
+ response.sendRedirect("https://fe-two-blond.vercel.app/kakao-redirect");
+ }
+
+ private ResponseCookie createCookie(String key, String value) {
+
+ ResponseCookie cookie = ResponseCookie.from(key, value)
+ .maxAge(60 * 60 * 60)
+ .secure(true)
+ .path("/")
+ .httpOnly(true)
+ .sameSite("None")
+ .build();
+
+ return cookie;
+ }
+
+}
diff --git a/src/main/java/com/minizin/travel/user/service/AuthService.java b/src/main/java/com/minizin/travel/user/service/AuthService.java
new file mode 100644
index 0000000..33b942e
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/service/AuthService.java
@@ -0,0 +1,39 @@
+package com.minizin.travel.user.service;
+
+import com.minizin.travel.user.domain.dto.JoinDto;
+import com.minizin.travel.user.domain.entity.UserEntity;
+import com.minizin.travel.user.domain.enums.LoginType;
+import com.minizin.travel.user.domain.enums.Role;
+import com.minizin.travel.user.domain.enums.UserErrorCode;
+import com.minizin.travel.user.domain.exception.CustomUserException;
+import com.minizin.travel.user.domain.repository.UserRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class AuthService {
+
+ private final UserRepository userRepository;
+ private final BCryptPasswordEncoder bCryptPasswordEncoder;
+
+ public JoinDto.Response join(JoinDto.Request request) {
+ if (userRepository.existsByUsername(request.getUsername())) {
+ throw new CustomUserException(UserErrorCode.USER_ALREADY_EXIST);
+ }
+
+ UserEntity saved = userRepository.save(UserEntity.builder()
+ .username(request.getUsername())
+ .password(
+ bCryptPasswordEncoder.encode(request.getPassword())
+ )
+ .nickname(request.getNickname())
+ .email(request.getEmail())
+ .role(Role.ROLE_USER)
+ .loginType(LoginType.LOCAL)
+ .build());
+
+ return JoinDto.Response.fromUserEntity(saved);
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/service/CustomOAuth2UserService.java b/src/main/java/com/minizin/travel/user/service/CustomOAuth2UserService.java
new file mode 100644
index 0000000..09cbcb8
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/service/CustomOAuth2UserService.java
@@ -0,0 +1,60 @@
+package com.minizin.travel.user.service;
+
+import com.minizin.travel.user.domain.dto.KakaoResponse;
+import com.minizin.travel.user.domain.dto.PrincipalDetails;
+import com.minizin.travel.user.domain.entity.UserEntity;
+import com.minizin.travel.user.domain.enums.LoginType;
+import com.minizin.travel.user.domain.enums.Role;
+import com.minizin.travel.user.domain.repository.UserRepository;
+import jakarta.transaction.Transactional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class CustomOAuth2UserService implements OAuth2UserService {
+
+ private final UserRepository userRepository;
+ private final DefaultOAuth2UserService delegate;
+
+ @Override
+ @Transactional
+ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
+
+ OAuth2User oAuth2User = delegate.loadUser(userRequest);
+
+ String registrationId = userRequest.getClientRegistration().getRegistrationId();
+ KakaoResponse kakaoResponse = null;
+ if ("kakao".equals(registrationId)) {
+ kakaoResponse = new KakaoResponse(oAuth2User.getAttributes());
+ } else {
+ return null;
+ }
+
+ //리소스 서버에서 발급 받은 정보로 사용자를 특정할 아이디 값을 만듬
+ String username = kakaoResponse.getProvider() + " " + kakaoResponse.getProviderId();
+
+ UserEntity userEntity = null;
+ Optional existUser = userRepository.findByUsername(username);
+ if (existUser.isEmpty()) {
+ userEntity = userRepository.save(UserEntity.builder()
+ .username(username)
+ .email(kakaoResponse.getEmail())
+ .nickname(kakaoResponse.getNickname())
+ .role(Role.ROLE_USER)
+ .loginType(LoginType.KAKAO)
+ .build());
+ } else {
+ userEntity = existUser.get();
+ }
+
+ return new PrincipalDetails(userEntity);
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/service/CustomUserDetailsService.java b/src/main/java/com/minizin/travel/user/service/CustomUserDetailsService.java
new file mode 100644
index 0000000..f15bb21
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/service/CustomUserDetailsService.java
@@ -0,0 +1,25 @@
+package com.minizin.travel.user.service;
+
+import com.minizin.travel.user.domain.dto.PrincipalDetails;
+import com.minizin.travel.user.domain.entity.UserEntity;
+import com.minizin.travel.user.domain.repository.UserRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class CustomUserDetailsService implements UserDetailsService {
+
+ private final UserRepository userRepository;
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ UserEntity userEntity = userRepository.findByUsername(username)
+ .orElseThrow(() -> new UsernameNotFoundException("존재하지 않는 사용자입니다."));
+
+ return new PrincipalDetails(userEntity);
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/service/MailService.java b/src/main/java/com/minizin/travel/user/service/MailService.java
new file mode 100644
index 0000000..41505d5
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/service/MailService.java
@@ -0,0 +1,152 @@
+package com.minizin.travel.user.service;
+
+import com.minizin.travel.user.domain.dto.FindPasswordDto;
+import com.minizin.travel.user.domain.dto.SendAuthCodeDto;
+import com.minizin.travel.user.domain.dto.VerifyAuthCodeDto;
+import com.minizin.travel.user.domain.enums.MailErrorCode;
+import com.minizin.travel.user.domain.exception.CustomMailException;
+import jakarta.mail.MessagingException;
+import jakarta.mail.internet.MimeMessage;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.stereotype.Service;
+import org.thymeleaf.context.Context;
+import org.thymeleaf.spring6.SpringTemplateEngine;
+import org.thymeleaf.templatemode.TemplateMode;
+import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
+
+import java.security.SecureRandom;
+import java.util.Random;
+
+@Service
+@RequiredArgsConstructor
+public class MailService {
+
+ private final JavaMailSender javaMailSender;
+ private final RedisService redisService;
+
+ @Value("${spring.mail.username}")
+ private String from;
+
+ public Boolean verifyAuthCode(VerifyAuthCodeDto verifyAuthCodeDto) {
+ String email = verifyAuthCodeDto.getEmail();
+ String authCode = verifyAuthCodeDto.getAuthCode();
+
+ String data = redisService.getData(email);
+ if (data == null) {
+ return false;
+ }
+
+ return authCode.equals(data);
+ }
+
+ public void sendAuthCode(SendAuthCodeDto sendAuthCodeDto) {
+ try {
+ javaMailSender.send(this.createEmailForm(sendAuthCodeDto.getEmail()));
+ } catch (Exception e) {
+ throw new CustomMailException(MailErrorCode.AUTH_CODE_SEND_FAILED);
+ }
+
+ }
+
+ public String sendTemporaryPassword(FindPasswordDto.Request request) {
+ String password = this.createPassword();
+ try {
+ javaMailSender.send(this.createPasswordForm(request.getEmail(), password));
+ } catch (Exception e) {
+ throw new CustomMailException(MailErrorCode.TEMPORARY_PASSWORD_SEND_FAILED);
+ }
+
+ return password;
+ }
+
+ private MimeMessage createPasswordForm(String email, String password) throws MessagingException {
+
+ MimeMessage message = javaMailSender.createMimeMessage();
+ message.addRecipients(MimeMessage.RecipientType.TO, email);
+ message.setSubject("안녕하세요. 임시 비밀번호입니다.");
+ message.setFrom(from);
+ message.setText(setPasswordContext(password), "utf-8", "html");
+
+ return message;
+ }
+
+ private MimeMessage createEmailForm(String email) throws MessagingException {
+
+ String authCode = createCode();
+
+ // redis 에 해당 인증코드 저장 (만료 시간도 함께 설정)
+ redisService.saveWithExpirationTime(email, authCode, 60 * 5L);
+
+ MimeMessage message = javaMailSender.createMimeMessage();
+ message.addRecipients(MimeMessage.RecipientType.TO, email);
+ message.setSubject("안녕하세요. 인증번호입니다.");
+ message.setFrom(from);
+ message.setText(setContext(authCode), "utf-8", "html");
+
+ return message;
+ }
+
+ private String createPassword() {
+ String upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ String lower = "abcdefghijklmnopqrstuvwxyz";
+ String digits = "0123456789";
+ String special = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
+
+ SecureRandom random = new SecureRandom();
+ StringBuilder password = new StringBuilder(8);
+
+ password.append(lower.charAt(random.nextInt(lower.length())));
+ password.append(upper.charAt(random.nextInt(upper.length())));
+ password.append(digits.charAt(random.nextInt(digits.length())));
+ password.append(special.charAt(random.nextInt(special.length())));
+
+ String allChars = upper + lower + digits + special;
+ for (int i = 4; i < 8; i++) {
+ password.append(allChars.charAt(random.nextInt(allChars.length())));
+ }
+
+ return password.toString();
+ }
+
+ // 인증코드 생성
+ private String createCode() {
+ return Integer.toString(100000 + new Random().nextInt(900000));
+ }
+
+ // 이메일 내용 초기화
+ private String setContext(String authCode) {
+ Context context = new Context();
+ SpringTemplateEngine templateEngine = new SpringTemplateEngine();
+ ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
+
+ context.setVariable("code", authCode);
+
+ templateResolver.setPrefix("templates/");
+ templateResolver.setSuffix(".html");
+ templateResolver.setTemplateMode(TemplateMode.HTML);
+ templateResolver.setCacheable(false);
+
+ templateEngine.setTemplateResolver(templateResolver);
+
+ return templateEngine.process("mailForm", context);
+ }
+
+ private String setPasswordContext(String password) {
+ Context context = new Context();
+ SpringTemplateEngine templateEngine = new SpringTemplateEngine();
+ ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
+
+ context.setVariable("password", password);
+
+ templateResolver.setPrefix("templates/");
+ templateResolver.setSuffix(".html");
+ templateResolver.setTemplateMode(TemplateMode.HTML);
+ templateResolver.setCacheable(false);
+
+ templateEngine.setTemplateResolver(templateResolver);
+
+ return templateEngine.process("passwordMailForm", context);
+ }
+}
diff --git a/src/main/java/com/minizin/travel/user/service/RedisService.java b/src/main/java/com/minizin/travel/user/service/RedisService.java
new file mode 100644
index 0000000..9880e29
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/service/RedisService.java
@@ -0,0 +1,23 @@
+package com.minizin.travel.user.service;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.time.Duration;
+
+@Service
+@RequiredArgsConstructor
+public class RedisService {
+
+ private final StringRedisTemplate stringRedisTemplate;
+
+ public void saveWithExpirationTime(String key, String value, long duration) {
+ stringRedisTemplate.opsForValue().set(key, value, Duration.ofSeconds(duration));
+ }
+
+ public String getData(String key) {
+ return stringRedisTemplate.opsForValue().get(key);
+ }
+
+}
diff --git a/src/main/java/com/minizin/travel/user/service/UserService.java b/src/main/java/com/minizin/travel/user/service/UserService.java
new file mode 100644
index 0000000..1bcc33a
--- /dev/null
+++ b/src/main/java/com/minizin/travel/user/service/UserService.java
@@ -0,0 +1,96 @@
+package com.minizin.travel.user.service;
+
+import com.minizin.travel.user.domain.dto.*;
+import com.minizin.travel.user.domain.entity.UserEntity;
+import com.minizin.travel.user.domain.enums.LoginType;
+import com.minizin.travel.user.domain.enums.UserErrorCode;
+import com.minizin.travel.user.domain.exception.CustomUserException;
+import com.minizin.travel.user.domain.repository.UserRepository;
+import jakarta.transaction.Transactional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import java.util.Objects;
+
+@Service
+@RequiredArgsConstructor
+public class UserService {
+
+ private final UserRepository userRepository;
+ private final MailService mailService;
+ private final BCryptPasswordEncoder bCryptPasswordEncoder;
+
+ public UserDto.Response getUserInfo(PrincipalDetails principalDetails) {
+ UserEntity userEntity = userRepository.findByUsername(principalDetails.getUsername())
+ .orElseThrow(() -> new CustomUserException(UserErrorCode.USER_NOT_FOUND));
+
+ return UserDto.Response.fromUserEntity(userEntity);
+ }
+
+ public FindIdDto.Response findId(FindIdDto.Request request) {
+ UserEntity userEntity = userRepository.findByEmailAndLoginType(request.getEmail(), LoginType.LOCAL)
+ .orElseThrow(() -> new CustomUserException(UserErrorCode.EMAIL_UN_REGISTERED));
+
+ return FindIdDto.Response.builder().username(userEntity.getUsername()).build();
+ }
+
+ @Transactional
+ public void findPassword(FindPasswordDto.Request request) {
+ UserEntity userEntity = userRepository.findByUsername(request.getUsername())
+ .orElseThrow(() -> new CustomUserException(UserErrorCode.USER_NOT_FOUND));
+ if (!Objects.equals(userEntity.getEmail(), request.getEmail())) {
+ throw new CustomUserException(UserErrorCode.USER_EMAIL_UN_MATCHED);
+ }
+
+ String password = mailService.sendTemporaryPassword(request);
+ userEntity.setPassword(bCryptPasswordEncoder.encode(password));
+ }
+
+ @Transactional
+ public UpdatePasswordDto.Response updatePassword(
+ UpdatePasswordDto.Request request, PrincipalDetails principalDetails) {
+ UserEntity userEntity = userRepository.findByUsername(principalDetails.getUsername())
+ .orElseThrow(() -> new CustomUserException(UserErrorCode.USER_NOT_FOUND));
+ if (!bCryptPasswordEncoder.matches(request.getOriginalPassword(), userEntity.getPassword())) {
+ throw new CustomUserException(UserErrorCode.PASSWORD_UN_MATCHED);
+ }
+
+ userEntity.setPassword(bCryptPasswordEncoder.encode(request.getChangePassword()));
+
+ return UpdatePasswordDto.Response.fromUserEntity(userEntity);
+ }
+
+ @Transactional
+ public UpdateEmailDto.Response updateEmail(
+ UpdateEmailDto.Request request, PrincipalDetails principalDetails
+ ) {
+ UserEntity userEntity = userRepository.findByUsername(principalDetails.getUsername())
+ .orElseThrow(() -> new CustomUserException(UserErrorCode.USER_NOT_FOUND));
+
+ userEntity.setEmail(request.getEmail());
+
+ return UpdateEmailDto.Response.fromUserEntity(userEntity);
+ }
+
+ @Transactional
+ public UpdateNicknameDto.Response updateNickname(
+ UpdateNicknameDto.Request request, PrincipalDetails principalDetails
+ ) {
+ UserEntity userEntity = userRepository.findByUsername(principalDetails.getUsername())
+ .orElseThrow(() -> new CustomUserException(UserErrorCode.USER_NOT_FOUND));
+
+ userEntity.setNickname(request.getNickname());
+
+ return UpdateNicknameDto.Response.fromUserEntity(userEntity);
+ }
+
+ public DeleteUserDto.Response deleteUser(PrincipalDetails principalDetails) {
+ UserEntity userEntity = userRepository.findByUsername(principalDetails.getUsername())
+ .orElseThrow(() -> new CustomUserException(UserErrorCode.USER_NOT_FOUND));
+
+ userRepository.delete(userEntity);
+
+ return DeleteUserDto.Response.fromUserEntity(userEntity);
+ }
+}
diff --git a/src/main/resources/application-oauth.yml b/src/main/resources/application-oauth.yml
new file mode 100644
index 0000000..7328351
--- /dev/null
+++ b/src/main/resources/application-oauth.yml
@@ -0,0 +1,21 @@
+spring:
+ security:
+ oauth2:
+ client:
+ registration:
+ kakao:
+ client-name: kakao
+ client-id: 3d90bb75347bd4b5962a422915dcad90
+ client-secret: yaE467VCfY8VKFrmiqOBWD6YCVG6BGhZ
+ client-authentication-method: client_secret_post
+ redirect-uri: https://lyckabc.synology.me:23080/login/oauth2/code/kakao
+ authorization-grant-type: authorization_code
+ scope:
+ - profile_nickname
+ - account_email
+ provider:
+ kakao:
+ authorization-uri: https://kauth.kakao.com/oauth/authorize
+ token-uri: https://kauth.kakao.com/oauth/token
+ user-info-uri: https://kapi.kakao.com/v2/user/me
+ user-name-attribute: id
\ No newline at end of file
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
deleted file mode 100644
index ea322ea..0000000
--- a/src/main/resources/application.yaml
+++ /dev/null
@@ -1,37 +0,0 @@
-spring:
- mvc:
- pathmatch:
- matching-strategy: ant_path_matcher
- datasource:
- h2:
- url: jdbc:h2:mem:test
- username: sa
- password:
- driverClassName: org.h2.Driver
- mariadb:
- url: jdbc:mariadb://localhost:3306/travel
- username: root
- password: 1q!1q!
- driverClassName: org.mariadb.jdbc.Driver
-
- jpa:
- hibernate:
- ddl-auto: update
- properties:
- hibernate:
- format_sql: true
- database-platform: org.hibernate.dialect.MariaDBDialect
-
- logging:
- level:
- root: INFO
- file:
- name: logs/myapp.log
- pattern:
- console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %class{50}.%M:%line - %msg%n"
- file: "%date{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50}:%line - %msg%n"
- org:
- springframework : DEBUG
- hibernate:
- SQL: DEBUG
- type: descriptor.sql.BasicBinder=TRACE
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..1807ec3
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,64 @@
+spring:
+ profiles:
+ include: oauth
+
+ datasource:
+ url: jdbc:mariadb://localhost:3306/travel
+ username: root
+ password: 1q!1q!
+ driver-class-name: org.mariadb.jdbc.Driver
+
+ jpa:
+ hibernate:
+ naming:
+ physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
+ ddl-auto: update
+ properties:
+ hibernate:
+ dialect: org.hibernate.dialect.MariaDBDialect
+ format_sql: true
+
+
+ data:
+ redis:
+ host: localhost
+ port: 6379
+
+ mail:
+ host: smtp.naver.com
+ port: 465
+ username: ENC(VjnM+wDg8lmtHcsk8QE9ECyZJYbH6R+5t5WyYDnFnWYNcb6Mw3yH0ksvC/MUed2laFNHUTlkIP/JHcHfvx8H0Q==)
+ password: ENC(CKqFnC3DcwpqaKq0jBK/xhV/gKlKk/iyKVGvnya9ci/cPhvn3XMDvXHcGBNKYYbl)
+
+ jwt:
+ secret: vmfhaltmskdlstkfkdgodyroqkfwkdbalroqkfwkdbalaaaaaaaaaaaaaaaabbbbb
+
+ logging:
+ level:
+ com.minizin.travel.config: INFO
+ root: INFO
+ file:
+ name: logs/myapp.log
+ pattern:
+ console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %class{50}.%M:%line - %msg%n"
+ file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50}:%line - %msg%n"
+ org:
+ springframework:
+ security: INFO
+ web: INFO
+ beans.factory: INFO
+ hibernate:
+ SQL: DEBUG
+ type:
+ descriptor.sql.BasicBinder: INFO
+springdoc:
+ swagger-ui:
+ path: /swagger-ui.html
+ api-docs:
+ path: /v3/api-docs
+server:
+ forward-headers-strategy: framework
+
+api-tour:
+ serviceKey_De: K9eWaIkQyqUUDZPWH+jI2Br1awNS1WaksaOZ6EiUbWTEzpVBeXnhTuPjni4n6auCsAUANYZ5o3Q89TF0sFU4bA==
+ serviceKey_En: K9eWaIkQyqUUDZPWH%2BjI2Br1awNS1WaksaOZ6EiUbWTEzpVBeXnhTuPjni4n6auCsAUANYZ5o3Q89TF0sFU4bA%3D%3D
diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml
new file mode 100644
index 0000000..ad6ca45
--- /dev/null
+++ b/src/main/resources/logback-spring.xml
@@ -0,0 +1,33 @@
+
+
+
+
+ ${LOG_FILE}
+
+ %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
+
+
+ logs/myapp-%d{yyyy-MM-dd}.%i.log
+
+ 10MB
+
+ 30
+
+
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/mailForm.html b/src/main/resources/templates/mailForm.html
new file mode 100644
index 0000000..5fe0345
--- /dev/null
+++ b/src/main/resources/templates/mailForm.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
인증 코드 메일입니다.
+
+ 아래 코드를 사이트에 입력해주세요.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/passwordMailForm.html b/src/main/resources/templates/passwordMailForm.html
new file mode 100644
index 0000000..f97c3cf
--- /dev/null
+++ b/src/main/resources/templates/passwordMailForm.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
임시 비밀번호입니다.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/java/com/minizin/travel/config/JasyptConfigTest.java b/src/test/java/com/minizin/travel/config/JasyptConfigTest.java
new file mode 100644
index 0000000..5037e57
--- /dev/null
+++ b/src/test/java/com/minizin/travel/config/JasyptConfigTest.java
@@ -0,0 +1,29 @@
+package com.minizin.travel.config;
+
+import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
+import org.jasypt.iv.RandomIvGenerator;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class JasyptConfigTest {
+
+ @Test
+ @DisplayName("암호화 결과 출력")
+ void stringEncryptor() {
+ String text = "text";
+
+ System.out.println("encrypted: " + jasyptEncoding(text));
+ }
+
+ private String jasyptEncoding(String value) {
+
+ String key = "my_jasypt_key";
+
+ StandardPBEStringEncryptor pbeEnc = new StandardPBEStringEncryptor();
+ pbeEnc.setAlgorithm("PBEWITHHMACSHA512ANDAES_256");
+ pbeEnc.setPassword(key);
+ pbeEnc.setIvGenerator(new RandomIvGenerator());
+
+ return pbeEnc.encrypt(value);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/minizin/travel/plan/CreatePlanTest.java b/src/test/java/com/minizin/travel/plan/CreatePlanTest.java
deleted file mode 100644
index 910bc18..0000000
--- a/src/test/java/com/minizin/travel/plan/CreatePlanTest.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package com.minizin.travel.plan;
-
-import com.minizin.travel.plan.dto.PlanBudgetDto;
-import com.minizin.travel.plan.dto.PlanDto;
-import com.minizin.travel.plan.dto.PlanScheduleDto;
-import com.minizin.travel.plan.entity.Plan;
-import com.minizin.travel.plan.entity.PlanBudget;
-import com.minizin.travel.plan.entity.PlanSchedule;
-import com.minizin.travel.plan.repository.PlanBudgetRepository;
-import com.minizin.travel.plan.repository.PlanRepository;
-import com.minizin.travel.plan.repository.PlanScheduleRepository;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.LocalTime;
-import java.time.format.DateTimeFormatter;
-
-@SpringBootTest
-public class CreatePlanTest {
-
- private final PlanRepository planRepository;
- private final PlanScheduleRepository planScheduleRepository;
- private final PlanBudgetRepository planBudgetRepository;
- final int INITIAL_VALUE = 0;
-
- @Autowired
- public CreatePlanTest(PlanRepository planRepository, PlanScheduleRepository planScheduleRepository, PlanBudgetRepository planBudgetRepository) {
- this.planRepository = planRepository;
- this.planScheduleRepository = planScheduleRepository;
- this.planBudgetRepository = planBudgetRepository;
- }
-
- @Test
- void createPlanTest() {
- // given
- // PlanDto
- PlanDto planDto = PlanDto.builder()
- .userId(1L)
- .planName("장소 계획 테스트")
- .theme("테마 계획 테스트")
- .startDate(LocalDate.parse("2024-06-10"))
- .endDate(LocalDate.parse("2024-07-01"))
- .scope(true)
- .numberOfMembers(3)
- .build();
-
- // PlanScheduleDto
- PlanScheduleDto planScheduleDto = PlanScheduleDto.builder()
- .scheduleDate(LocalDate.parse("2024-06-01"))
- .placeCategory("스케줄 카테고리 테스트")
- .placeName("스케줄 이름 테스트")
- .region("지역")
- .placeMemo("스케줄 메모 테스트")
- .arrivalTime("21:00:00")
- .build();
-
- // PlanBudgetDto
- PlanBudgetDto planBudgetDto = PlanBudgetDto.builder().build();
-
- // when
- // Plan 저장
- Plan newPlan = Plan.builder()
- .userId(planDto.getUserId())
- .planName(planDto.getPlanName())
- .theme(planDto.getTheme())
- .startDate(planDto.getStartDate())
- .endDate(planDto.getEndDate())
- .scope(planDto.isScope())
- .numberOfMembers(planDto.getNumberOfMembers())
- .numberOfLikes(INITIAL_VALUE)
- .numberOfScraps(INITIAL_VALUE)
- .createdAt(LocalDateTime.now())
- .modifiedAt(LocalDateTime.now())
- .build();
- Plan insertedPlan = planRepository.save(newPlan);
-
- // PlanSchedule 저장
- PlanSchedule newPlanSchedule = PlanSchedule.builder()
- .planId(insertedPlan.getId())
- .scheduleDate(planScheduleDto.getScheduleDate())
- .placeCategory(planScheduleDto.getPlaceCategory())
- .placeName(planScheduleDto.getPlaceName())
- .placeMemo(planScheduleDto.getPlaceMemo())
- .arrivalTime(LocalTime.parse(planScheduleDto.getArrivalTime(), DateTimeFormatter.ofPattern("HH:mm:ss")))
- .x(planScheduleDto.getX())
- .y(planScheduleDto.getY())
- .createdAt(LocalDateTime.now())
- .modifiedAt(LocalDateTime.now())
- .build();
- PlanSchedule insertedPlanSchedule = planScheduleRepository.save(newPlanSchedule);
-
- // PlanBudget 저장
- PlanBudget newPlanBudget = PlanBudget.builder()
- .scheduleId(insertedPlanSchedule.getId())
- .budgetCategory(planBudgetDto.getBudgetCategory())
- .cost(planBudgetDto.getCost())
- .budgetMemo(planBudgetDto.getBudgetMemo())
- .createdAt(LocalDateTime.now())
- .modifiedAt(LocalDateTime.now())
- .build();
- PlanBudget insertedPlanBudget = planBudgetRepository.save(newPlanBudget);
-
- // then
- Assertions.assertEquals(newPlan, insertedPlan);
- Assertions.assertEquals(newPlanSchedule, insertedPlanSchedule);
- Assertions.assertEquals(newPlanBudget, insertedPlanBudget);
- }
-}
diff --git a/src/test/java/com/minizin/travel/plan/SelectListPlanTest.java b/src/test/java/com/minizin/travel/plan/SelectListPlanTest.java
deleted file mode 100644
index 078b9d2..0000000
--- a/src/test/java/com/minizin/travel/plan/SelectListPlanTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.minizin.travel.plan;
-
-import com.minizin.travel.plan.entity.Plan;
-import com.minizin.travel.plan.repository.PlanRepository;
-import com.minizin.travel.plan.service.PlanService;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-public class SelectListPlanTest {
-
- private final PlanRepository planRepository;
- private final PlanService planService;
-
- @Autowired
- public SelectListPlanTest(PlanRepository planRepository, PlanService planService) {
- this.planRepository = planRepository;
- this.planService = planService;
- }
-
- @Test
- void selectListPlanTest() {
- // given
- for (int i = 0; i < 10; i++) {
- planRepository.save(Plan.builder()
- .planName("test")
- .userId(1L)
- .build());
- }
-
- // when
- Long cursorId = 10L;
- var result = planService.selectListPlan(cursorId);
-
- // then
- // cursorId 보다 작은 id 값부터 조회
- Assertions.assertEquals(9, result.getData().get(0).getId());
- // 현재 planService 에 지정된 조회되는 개수가 6개 (id: 9 ~ 4까지 조회)
- // nextCursor의 값은 4로 지정
- Assertions.assertEquals(4, result.getNextCursor());
- }
-}
diff --git a/src/test/java/com/minizin/travel/tour/service/TourServiceTest.java b/src/test/java/com/minizin/travel/tour/service/TourServiceTest.java
new file mode 100644
index 0000000..f2b086a
--- /dev/null
+++ b/src/test/java/com/minizin/travel/tour/service/TourServiceTest.java
@@ -0,0 +1,163 @@
+//package com.minizin.travel.tour.service;
+//
+//import com.minizin.travel.tour.domain.dto.TourAPIDto;
+//import com.minizin.travel.tour.domain.entity.TourAPI;
+//import com.minizin.travel.tour.domain.repository.TourAPIRepository;
+//import com.nimbusds.jose.shaded.gson.Gson;
+//import okhttp3.OkHttpClient;
+//import okhttp3.mockwebserver.MockResponse;
+//import okhttp3.mockwebserver.MockWebServer;
+//import org.junit.jupiter.api.*;
+//import org.mockito.InjectMocks;
+//import org.mockito.Mock;
+//import org.mockito.MockitoAnnotations;
+//import org.springframework.data.domain.Page;
+//import org.springframework.data.domain.PageImpl;
+//import org.springframework.data.domain.PageRequest;
+//
+//import java.io.IOException;
+//import java.util.Collections;
+//import java.util.List;
+//import java.util.concurrent.CompletableFuture;
+//
+//import static org.junit.jupiter.api.Assertions.assertEquals;
+//import static org.mockito.ArgumentMatchers.any;
+//import static org.mockito.Mockito.*;
+//
+///**
+// * Class: TourServiceTest Project: travel Package: com.minizin.travel.tour.service
+// *
+// * Description:
+// *
+// * @author dong-hoshin
+// * @date 6/27/24 18:34 Copyright (c) 2024
+// * @see GitHub Repository
+// */
+//class TourServiceTest {
+//
+// @Mock
+// private TourAPIRepository tourAPIRepository;
+//
+// @InjectMocks
+// private TourService tourService;
+//
+// private MockWebServer mockWebServer;
+//
+// private OkHttpClient client;
+//
+// @BeforeEach
+// void setUp() throws IOException {
+// MockitoAnnotations.openMocks(this);
+// mockWebServer = new MockWebServer();
+// mockWebServer.start();
+// client = new OkHttpClient();
+// }
+//
+// @AfterEach
+// void tearDown() throws IOException {
+// mockWebServer.shutdown();
+// }
+//
+// /*@Test
+// void testGetTourAPIFromSiteDetailCommon() throws Exception {
+// // given
+// TourAPIDto.TourRequest requestParam = new TourAPIDto.TourRequest();
+// requestParam.setPageNo("0");
+// requestParam.setNumOfRows("1");
+//
+// TourAPI tourAPI = new TourAPI();
+// tourAPI.setContentId("129459");
+// tourAPI.setContentTypeId("12");
+// Page tourAPIPage = new PageImpl<>(Collections.singletonList(tourAPI));
+// when(tourAPIRepository.findAll(any(PageRequest.class))).thenReturn(tourAPIPage);
+//
+// mockWebServer.enqueue(new MockResponse()
+// .setBody("{\"response\": {\"body\": {\"items\": {\"item\": [{\"contentId\": \"129459\", \"contentTypeId\": \"12\"}]}}}}")
+// .addHeader("Content-Type", "application/json"));
+//
+// // when
+// CompletableFuture future = tourService.getTourAPIFromSiteDetailCommon(requestParam);
+// String result = future.join();
+//
+// // then
+// assertEquals("getTourAPIFromSiteDetailCommon executed successfully", result);
+// verify(tourAPIRepository, times(1)).findAll(any(PageRequest.class));
+// }*/
+//
+// @Test
+// void testGetTourAPIFromSiteSearchKeyword() throws Exception {
+// // given
+// TourAPIDto.TourRequest requestParam = new TourAPIDto.TourRequest();
+// requestParam.setKeyword("test");
+//
+// mockWebServer.enqueue(new MockResponse()
+// .setBody("{\"response\": {\"body\": {\"items\": {\"item\": [{\"contentId\": \"1\", \"contentTypeId\": \"2\"}]}}}}")
+// .addHeader("Content-Type", "application/json"));
+//
+// // when
+// CompletableFuture> future = tourService.getTourAPIFromSiteSearchKeyword(requestParam);
+// List result = future.join();
+//
+// // then
+// assertEquals(1, result.size());
+// assertEquals("1", result.get(0).getContentId());
+// assertEquals("2", result.get(0).getContentTypeId());
+// }
+//
+// @Test
+// void testGetTourAPIFromSiteAreaBasedList() throws Exception {
+// // given
+// TourAPIDto.TourRequest requestParam = new TourAPIDto.TourRequest();
+//
+// mockWebServer.enqueue(new MockResponse()
+// .setBody("{\"response\": {\"body\": {\"items\": {\"item\": [{\"contentId\": \"1\", \"contentTypeId\": \"2\"}]}}}}")
+// .addHeader("Content-Type", "application/json"));
+//
+// // when
+// CompletableFuture> future = tourService.getTourAPIFromSiteAreaBasedList(requestParam);
+// List result = future.join();
+//
+// // then
+// assertEquals(1, result.size());
+// assertEquals("1", result.get(0).getContentId());
+// assertEquals("2", result.get(0).getContentTypeId());
+// }
+//
+// @Test
+// void testGetTourAPIFromSiteAreaBasedListSingle() throws Exception {
+// // given
+// TourAPIDto.TourRequest requestParam = new TourAPIDto.TourRequest();
+//
+// mockWebServer.enqueue(new MockResponse()
+// .setBody("{\"response\": {\"body\": {\"items\": {\"item\": [{\"contentId\": \"1\", \"contentTypeId\": \"2\"}]}}}}")
+// .addHeader("Content-Type", "application/json"));
+//
+// // when
+// CompletableFuture> future = tourService.getTourAPIFromSiteAreaBasedListSingle(requestParam);
+// List result = future.join();
+//
+// // then
+// assertEquals(1, result.size());
+// assertEquals("1", result.get(0).getContentId());
+// assertEquals("2", result.get(0).getContentTypeId());
+// }
+//
+// @Test
+// void testGetTourAPIFromSiteAreaCode() throws Exception {
+// // given
+// TourAPIDto.TourRequest requestParam = new TourAPIDto.TourRequest();
+//
+// mockWebServer.enqueue(new MockResponse()
+// .setBody("{\"response\": {\"body\": {\"items\": {\"item\": [{\"contentId\": \"1\", \"contentTypeId\": \"2\"}]}}}}")
+// .addHeader("Content-Type", "application/json"));
+//
+// // when
+// CompletableFuture> future = tourService.getTourAPIFromSiteAreaCode(requestParam);
+// List result = future.join();
+//
+// // then
+// assertEquals(1, result.size());
+// assertEquals("1", result.get(0).getContentId());
+// assertEquals("2", result.get(0).getContentTypeId());
+// }
+//}
\ No newline at end of file
diff --git a/src/test/java/com/minizin/travel/user/service/AuthServiceTest.java b/src/test/java/com/minizin/travel/user/service/AuthServiceTest.java
new file mode 100644
index 0000000..710a3fe
--- /dev/null
+++ b/src/test/java/com/minizin/travel/user/service/AuthServiceTest.java
@@ -0,0 +1,91 @@
+package com.minizin.travel.user.service;
+
+import com.minizin.travel.user.domain.dto.JoinDto;
+import com.minizin.travel.user.domain.entity.UserEntity;
+import com.minizin.travel.user.domain.enums.LoginType;
+import com.minizin.travel.user.domain.enums.Role;
+import com.minizin.travel.user.domain.enums.UserErrorCode;
+import com.minizin.travel.user.domain.exception.CustomUserException;
+import com.minizin.travel.user.domain.repository.UserRepository;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+@ExtendWith(MockitoExtension.class)
+class AuthServiceTest {
+
+ @Mock
+ private UserRepository userRepository;
+
+ @Mock
+ private BCryptPasswordEncoder bCryptPasswordEncoder;
+
+ @InjectMocks
+ private AuthService authService;
+
+ @Test
+ @DisplayName("회원가입 성공")
+ void join_success() {
+ //given
+ JoinDto.Request request = new JoinDto.Request();
+ request.setUsername("username");
+ request.setPassword("password");
+ request.setNickname("nickname");
+ request.setEmail("email");
+
+ given(userRepository.existsByUsername(request.getUsername()))
+ .willReturn(false);
+ given(bCryptPasswordEncoder.encode(request.getPassword()))
+ .willReturn("password");
+ given(userRepository.save(any(UserEntity.class)))
+ .willReturn(UserEntity.builder()
+ .username("username")
+ .password("password")
+ .nickname("nickname")
+ .email("email")
+ .role(Role.ROLE_USER)
+ .loginType(LoginType.LOCAL)
+ .build());
+
+ //when
+ JoinDto.Response response = authService.join(request);
+
+ //then
+ assertEquals("username", response.getUsername());
+ assertEquals("password", response.getPassword());
+ assertEquals("nickname", response.getNickname());
+ assertEquals("email", response.getEmail());
+ assertEquals(Role.ROLE_USER, response.getRole());
+ assertEquals(LoginType.LOCAL, response.getLoginType());
+
+ verify(userRepository, times(1)).save(any(UserEntity.class));
+ }
+
+ @Test
+ @DisplayName("회원가입 실패 - 이미 존재하는 사용자")
+ void join_fail_alreadyExistsUser() {
+ //given
+ JoinDto.Request request = new JoinDto.Request();
+ request.setUsername("username");
+ given(userRepository.existsByUsername("username"))
+ .willReturn(true);
+
+ //when
+ CustomUserException customUserException = assertThrows(CustomUserException.class,
+ () -> authService.join(request));
+
+ //then
+ assertEquals(UserErrorCode.USER_ALREADY_EXIST, customUserException.getUserErrorCode());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/minizin/travel/user/service/CustomOAuth2UserServiceTest.java b/src/test/java/com/minizin/travel/user/service/CustomOAuth2UserServiceTest.java
new file mode 100644
index 0000000..c6ee975
--- /dev/null
+++ b/src/test/java/com/minizin/travel/user/service/CustomOAuth2UserServiceTest.java
@@ -0,0 +1,82 @@
+package com.minizin.travel.user.service;
+
+import com.minizin.travel.user.domain.dto.PrincipalDetails;
+import com.minizin.travel.user.domain.entity.UserEntity;
+import com.minizin.travel.user.domain.enums.LoginType;
+import com.minizin.travel.user.domain.enums.Role;
+import com.minizin.travel.user.domain.repository.UserRepository;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class CustomOAuth2UserServiceTest {
+
+ @Mock
+ private UserRepository userRepository;
+
+ @Mock
+ private DefaultOAuth2UserService delegate;
+
+ @InjectMocks
+ private CustomOAuth2UserService customOAuth2UserService;
+
+ @Test
+ @DisplayName("소셜 로그인 시 사용자 정보 획득 성공")
+ void loadUser_success() {
+ //given
+ OAuth2UserRequest userRequest = mock(OAuth2UserRequest.class);
+ OAuth2User oAuth2User = mock(OAuth2User.class);
+ given(delegate.loadUser(userRequest)).willReturn(oAuth2User);
+
+ ClientRegistration clientRegistration = mock(ClientRegistration.class);
+ when(userRequest.getClientRegistration()).thenReturn(clientRegistration);
+ when(clientRegistration.getRegistrationId()).thenReturn("kakao");
+
+ Map attributes = Map.of(
+ "id", "12345",
+ "kakao_account", Map.of(
+ "email", "test@example.com",
+ "profile", Map.of("nickname", "Test User")
+ )
+ );
+ when(oAuth2User.getAttributes()).thenReturn(attributes);
+ given(userRepository.findByUsername("kakao 12345"))
+ .willReturn(Optional.empty());
+ given(userRepository.save(any(UserEntity.class)))
+ .willReturn(UserEntity.builder()
+ .username("kakao 12345")
+ .email("test@example.com")
+ .nickname("Test User")
+ .role(Role.ROLE_USER)
+ .loginType(LoginType.KAKAO)
+ .build());
+
+ //when
+ OAuth2User oAuth2User1 = customOAuth2UserService.loadUser(userRequest);
+
+ //then
+ assertNotNull(oAuth2User1);
+ assertEquals("kakao 12345", ((PrincipalDetails) oAuth2User1).getUsername());
+ assertEquals("test@example.com", ((PrincipalDetails) oAuth2User1).getUserEntity().getEmail());
+ assertEquals("Test User", ((PrincipalDetails) oAuth2User1).getUserEntity().getNickname());
+
+ verify(userRepository, times(1)).save(any(UserEntity.class));
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/minizin/travel/user/service/CustomUserDetailsServiceTest.java b/src/test/java/com/minizin/travel/user/service/CustomUserDetailsServiceTest.java
new file mode 100644
index 0000000..5eba119
--- /dev/null
+++ b/src/test/java/com/minizin/travel/user/service/CustomUserDetailsServiceTest.java
@@ -0,0 +1,75 @@
+package com.minizin.travel.user.service;
+
+import com.minizin.travel.user.domain.dto.PrincipalDetails;
+import com.minizin.travel.user.domain.entity.UserEntity;
+import com.minizin.travel.user.domain.enums.LoginType;
+import com.minizin.travel.user.domain.enums.Role;
+import com.minizin.travel.user.domain.repository.UserRepository;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.BDDMockito.given;
+
+@ExtendWith(MockitoExtension.class)
+class CustomUserDetailsServiceTest {
+
+ @Mock
+ private UserRepository userRepository;
+
+ @InjectMocks
+ private CustomUserDetailsService customUserDetailsService;
+
+ @Test
+ @DisplayName("username 으로 principalDetails 획득 성공")
+ void loadUserByUsername_success() {
+ //given
+ given(userRepository.findByUsername("username"))
+ .willReturn(Optional.of(
+ UserEntity.builder()
+ .username("username")
+ .password("password")
+ .email("email")
+ .nickname("nickname")
+ .role(Role.ROLE_USER)
+ .loginType(LoginType.LOCAL)
+ .build()
+ ));
+
+ //when
+ UserDetails userDetails = customUserDetailsService.loadUserByUsername("username");
+
+ //then
+ assertEquals("username", ((PrincipalDetails) userDetails).getUserEntity().getUsername());
+ assertEquals("password", ((PrincipalDetails) userDetails).getUserEntity().getPassword());
+ assertEquals("email", ((PrincipalDetails) userDetails).getUserEntity().getEmail());
+ assertEquals("nickname", ((PrincipalDetails) userDetails).getUserEntity().getNickname());
+ assertEquals(Role.ROLE_USER, ((PrincipalDetails) userDetails).getUserEntity().getRole());
+ assertEquals(LoginType.LOCAL, ((PrincipalDetails) userDetails).getUserEntity().getLoginType());
+ }
+
+ @Test
+ @DisplayName("username 으로 principalDetails 획득 실패 - 존재하지 않는 사용자")
+ void loadUserByUsername_fail_UserNotFound() {
+ //given
+ given(userRepository.findByUsername("username"))
+ .willReturn(Optional.empty());
+
+ //when
+ UsernameNotFoundException exception = assertThrows(UsernameNotFoundException.class,
+ () -> customUserDetailsService.loadUserByUsername("username"));
+
+ //then
+ assertEquals("존재하지 않는 사용자입니다.", exception.getMessage());
+
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/minizin/travel/user/service/MailServiceTest.java b/src/test/java/com/minizin/travel/user/service/MailServiceTest.java
new file mode 100644
index 0000000..5751bb3
--- /dev/null
+++ b/src/test/java/com/minizin/travel/user/service/MailServiceTest.java
@@ -0,0 +1,143 @@
+package com.minizin.travel.user.service;
+
+import com.minizin.travel.user.domain.dto.FindPasswordDto;
+import com.minizin.travel.user.domain.dto.SendAuthCodeDto;
+import com.minizin.travel.user.domain.dto.VerifyAuthCodeDto;
+import com.minizin.travel.user.domain.enums.MailErrorCode;
+import com.minizin.travel.user.domain.exception.CustomMailException;
+import jakarta.mail.internet.MimeMessage;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.mail.javamail.JavaMailSender;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class MailServiceTest {
+
+ @Mock
+ private JavaMailSender javaMailSender;
+
+ @Mock
+ private RedisService redisService;
+
+ @InjectMocks
+ private MailService mailService;
+
+ @Test
+ @DisplayName("인증 코드 메일 발송 성공")
+ void sendAuthCode_success() {
+ //given
+ SendAuthCodeDto sendAuthCodeDto = new SendAuthCodeDto();
+ sendAuthCodeDto.setEmail("email");
+
+ MimeMessage mimeMessage = mock(MimeMessage.class);
+ given(javaMailSender.createMimeMessage())
+ .willReturn(mimeMessage);
+
+ //when
+ mailService.sendAuthCode(sendAuthCodeDto);
+
+ //then
+ verify(javaMailSender, times(1)).send(any(MimeMessage.class));
+ }
+
+ @Test
+ @DisplayName("인증 코드 메일 발송 실패")
+ void sendAuthCode_fail() {
+ //given
+ SendAuthCodeDto sendAuthCodeDto = new SendAuthCodeDto();
+ sendAuthCodeDto.setEmail("email");
+
+ given(javaMailSender.createMimeMessage())
+ .willReturn(null);
+
+ //when
+ CustomMailException exception = assertThrows(CustomMailException.class,
+ () -> mailService.sendAuthCode(sendAuthCodeDto));
+
+ //then
+ assertEquals(MailErrorCode.AUTH_CODE_SEND_FAILED, exception.getMailErrorCode());
+ }
+
+ @Test
+ @DisplayName("인증 코드 일치")
+ void verifyAuthCode_true() {
+ //given
+ VerifyAuthCodeDto verifyAuthCodeDto = new VerifyAuthCodeDto();
+ verifyAuthCodeDto.setEmail("email");
+ verifyAuthCodeDto.setAuthCode("authCode");
+
+ given(redisService.getData("email"))
+ .willReturn("authCode");
+
+ //when
+ Boolean verified = mailService.verifyAuthCode(verifyAuthCodeDto);
+
+ //then
+ assertEquals(true, verified);
+ }
+
+ @Test
+ @DisplayName("인증 코드 불일치")
+ void verifyAuthCode_false() {
+ //given
+ VerifyAuthCodeDto verifyAuthCodeDto = new VerifyAuthCodeDto();
+ verifyAuthCodeDto.setEmail("email");
+ verifyAuthCodeDto.setAuthCode("authCode");
+
+ given(redisService.getData("email"))
+ .willReturn("authCode2");
+
+ //when
+ Boolean verified = mailService.verifyAuthCode(verifyAuthCodeDto);
+
+ //then
+ assertEquals(false, verified);
+ }
+
+ @Test
+ @DisplayName("임시 비밀번호 메일 발송 성공")
+ void sendTemporaryPassword_success() {
+ //given
+ FindPasswordDto.Request request = new FindPasswordDto.Request();
+ request.setEmail("email");
+ request.setUsername("username");
+
+ MimeMessage mimeMessage = mock(MimeMessage.class);
+ given(javaMailSender.createMimeMessage())
+ .willReturn(mimeMessage);
+
+ //when
+ mailService.sendTemporaryPassword(request);
+
+ //then
+ verify(javaMailSender, times(1)).send(any(MimeMessage.class));
+ }
+
+ @Test
+ @DisplayName("임시 비밀번호 메일 발송 실패")
+ void sendTemporaryPassword_fail() {
+ //given
+ FindPasswordDto.Request request = new FindPasswordDto.Request();
+ request.setEmail("email");
+ request.setUsername("username");
+
+ given(javaMailSender.createMimeMessage())
+ .willReturn(null);
+
+ //when
+ CustomMailException exception = assertThrows(CustomMailException.class,
+ () -> mailService.sendTemporaryPassword(request));
+
+ //then
+ assertEquals(MailErrorCode.TEMPORARY_PASSWORD_SEND_FAILED, exception.getMailErrorCode());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/minizin/travel/user/service/RedisServiceTest.java b/src/test/java/com/minizin/travel/user/service/RedisServiceTest.java
new file mode 100644
index 0000000..8f57ece
--- /dev/null
+++ b/src/test/java/com/minizin/travel/user/service/RedisServiceTest.java
@@ -0,0 +1,64 @@
+package com.minizin.travel.user.service;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+
+import java.time.Duration;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class RedisServiceTest {
+
+ @Mock
+ private StringRedisTemplate stringRedisTemplate;
+
+ @InjectMocks
+ private RedisService redisService;
+
+ @Test
+ @DisplayName("인증 코드 만료 시간과 함께 저장")
+ void saveWithExpirationTime() {
+ //given
+ String key = "key";
+ String value = "value";
+ long duration = 10L;
+
+ ValueOperations valueOperations = mock(ValueOperations.class);
+ given(stringRedisTemplate.opsForValue()).willReturn(valueOperations);
+
+ //when
+ redisService.saveWithExpirationTime(key, value, duration);
+
+ //then
+ verify(valueOperations, times(1)).set(eq(key), eq(value),
+ eq(Duration.ofSeconds(duration)));
+ }
+
+ @Test
+ @DisplayName("데이터 가져오기")
+ void getData() {
+ //given
+ String key = "key";
+ String value = "value";
+
+ ValueOperations valueOperations = mock(ValueOperations.class);
+ given(stringRedisTemplate.opsForValue()).willReturn(valueOperations);
+ when(valueOperations.get(key)).thenReturn(value);
+
+ //when
+ String data = redisService.getData(key);
+
+ //then
+ assertEquals(value, data);
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/minizin/travel/user/service/UserServiceTest.java b/src/test/java/com/minizin/travel/user/service/UserServiceTest.java
new file mode 100644
index 0000000..62ee844
--- /dev/null
+++ b/src/test/java/com/minizin/travel/user/service/UserServiceTest.java
@@ -0,0 +1,403 @@
+package com.minizin.travel.user.service;
+
+import com.minizin.travel.user.domain.dto.*;
+import com.minizin.travel.user.domain.entity.UserEntity;
+import com.minizin.travel.user.domain.enums.LoginType;
+import com.minizin.travel.user.domain.enums.UserErrorCode;
+import com.minizin.travel.user.domain.exception.CustomUserException;
+import com.minizin.travel.user.domain.repository.UserRepository;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.BDDMockito.given;
+
+@ExtendWith(MockitoExtension.class)
+class UserServiceTest {
+
+ @Mock
+ private UserRepository userRepository;
+
+ @Mock
+ private MailService mailService;
+
+ @Mock
+ private BCryptPasswordEncoder bCryptPasswordEncoder;
+
+ @InjectMocks
+ private UserService userService;
+
+ @Test
+ @DisplayName("사용자 정보 조회 성공")
+ void getUserInfo_success() {
+ //given
+ UserEntity userEntity = UserEntity.builder()
+ .id(1L)
+ .username("username")
+ .email("email")
+ .nickname("nickname")
+ .build();
+ PrincipalDetails principalDetails = new PrincipalDetails(userEntity);
+
+ given(userRepository.findByUsername(principalDetails.getUsername()))
+ .willReturn(Optional.of(userEntity));
+
+ //when
+ UserDto.Response response = userService.getUserInfo(principalDetails);
+
+ //then
+ assertEquals(1L, response.getUserId());
+ assertEquals("username", response.getUsername());
+ assertEquals("email", response.getEmail());
+ assertEquals("nickname", response.getNickname());
+ }
+
+ @Test
+ @DisplayName("사용자 정보 조회 실패 - 존재하지 않는 사용자")
+ void getUserInfo_fail_userNotFound() {
+ //given
+ UserEntity userEntity = UserEntity.builder()
+ .username("username")
+ .build();
+ PrincipalDetails principalDetails = new PrincipalDetails(userEntity);
+
+ given(userRepository.findByUsername(principalDetails.getUsername()))
+ .willReturn(Optional.empty());
+
+ //when
+ CustomUserException exception = assertThrows(CustomUserException.class,
+ () -> userService.getUserInfo(principalDetails));
+
+ //then
+ assertEquals(UserErrorCode.USER_NOT_FOUND, exception.getUserErrorCode());
+ }
+
+ @Test
+ @DisplayName("아이디 찾기 성공")
+ void findId_success() {
+ //given
+ FindIdDto.Request request = new FindIdDto.Request();
+ request.setEmail("email");
+ given(userRepository.findByEmailAndLoginType(request.getEmail(), LoginType.LOCAL))
+ .willReturn(Optional.of(
+ UserEntity.builder()
+ .username("username")
+ .build()
+ ));
+
+
+ //when
+ FindIdDto.Response response = userService.findId(request);
+
+ //then
+ assertEquals("username", response.getUsername());
+ }
+
+ @Test
+ @DisplayName("아이디 찾기 실패 - 등록되지 않은 이메일")
+ void findId_fail_unRegisteredEmail() {
+ //given
+ FindIdDto.Request request = new FindIdDto.Request();
+ request.setEmail("email");
+ given(userRepository.findByEmailAndLoginType(request.getEmail(), LoginType.LOCAL))
+ .willReturn(Optional.empty());
+
+ //when
+ CustomUserException exception = assertThrows(CustomUserException.class,
+ () -> userService.findId(request));
+
+ //then
+ assertEquals(UserErrorCode.EMAIL_UN_REGISTERED, exception.getUserErrorCode());
+ }
+
+ @Test
+ @DisplayName("임시 비밀번호 발급 성공")
+ void findPassword_success() {
+ //given
+ FindPasswordDto.Request request = new FindPasswordDto.Request();
+ request.setUsername("username");
+ request.setEmail("email");
+ UserEntity userEntity = UserEntity.builder()
+ .username("username")
+ .email("email")
+ .build();
+ given(userRepository.findByUsername(request.getUsername()))
+ .willReturn(Optional.of(userEntity));
+ given(mailService.sendTemporaryPassword(request))
+ .willReturn("password");
+ given(bCryptPasswordEncoder.encode("password"))
+ .willReturn("password");
+
+ //when
+ userService.findPassword(request);
+
+ //then
+ assertEquals("password", userEntity.getPassword());
+ }
+
+ @Test
+ @DisplayName("임시 비밀번호 발급 실패 - 존재하지 않는 사용자")
+ void findPassword_fail_userNotFound() {
+ //given
+ FindPasswordDto.Request request = new FindPasswordDto.Request();
+ request.setUsername("username");
+ request.setEmail("email");
+ given(userRepository.findByUsername(request.getUsername()))
+ .willReturn(Optional.empty());
+
+ //when
+ CustomUserException exception = assertThrows(CustomUserException.class,
+ () -> userService.findPassword(request));
+
+ //then
+ assertEquals(UserErrorCode.USER_NOT_FOUND, exception.getUserErrorCode());
+ }
+
+ @Test
+ @DisplayName("임시 비밀번호 발급 실패 - 사용자에게 등록된 이메일이 아님")
+ void findPassword_fail_userEmailUnMatched() {
+ //given
+ FindPasswordDto.Request request = new FindPasswordDto.Request();
+ request.setUsername("username");
+ request.setEmail("email2");
+ UserEntity userEntity = UserEntity.builder()
+ .username("username")
+ .email("email")
+ .build();
+ given(userRepository.findByUsername(request.getUsername()))
+ .willReturn(Optional.of(userEntity));
+
+ //when
+ CustomUserException exception = assertThrows(CustomUserException.class,
+ () -> userService.findPassword(request));
+
+ //then
+ assertEquals(UserErrorCode.USER_EMAIL_UN_MATCHED, exception.getUserErrorCode());
+ }
+
+ @Test
+ @DisplayName("비밀번호 수정 성공")
+ void updatePassword_success() {
+ //given
+ UpdatePasswordDto.Request request = new UpdatePasswordDto.Request();
+ request.setOriginalPassword("original");
+ request.setChangePassword("change");
+
+ UserEntity userEntity = UserEntity.builder()
+ .id(1L)
+ .username("username")
+ .password("original")
+ .build();
+ PrincipalDetails principalDetails = new PrincipalDetails(userEntity);
+
+ given(userRepository.findByUsername(principalDetails.getUsername()))
+ .willReturn(Optional.of(userEntity));
+ given(bCryptPasswordEncoder.matches(request.getOriginalPassword(), userEntity.getPassword()))
+ .willReturn(true);
+ given(bCryptPasswordEncoder.encode(request.getChangePassword()))
+ .willReturn("change");
+
+ //when
+ UpdatePasswordDto.Response response = userService.updatePassword(request, principalDetails);
+
+ //then
+ assertEquals(true, response.getSuccess());
+ assertEquals(1L, response.getUserId());
+ assertEquals("change", response.getChangedPassword());
+
+ }
+
+ @Test
+ @DisplayName("비밀번호 수정 실패 - 존재하지 않는 사용자")
+ void updatePassword_fail_userNotFound() {
+ //given
+ UpdatePasswordDto.Request request = new UpdatePasswordDto.Request();
+
+ UserEntity userEntity = UserEntity.builder()
+ .username("username")
+ .build();
+ PrincipalDetails principalDetails = new PrincipalDetails(userEntity);
+
+ given(userRepository.findByUsername(principalDetails.getUsername()))
+ .willReturn(Optional.empty());
+
+ //when
+ CustomUserException exception = assertThrows(CustomUserException.class,
+ () -> userService.updatePassword(request, principalDetails));
+
+ //then
+ assertEquals(UserErrorCode.USER_NOT_FOUND, exception.getUserErrorCode());
+
+ }
+
+ @Test
+ @DisplayName("비밀번호 수정 실패 - 일치하지 않는 비밀번호")
+ void updatePassword_fail_passwordUnMatched() {
+ //given
+ UpdatePasswordDto.Request request = new UpdatePasswordDto.Request();
+ request.setOriginalPassword("original");
+ request.setChangePassword("change");
+
+ UserEntity userEntity = UserEntity.builder()
+ .id(1L)
+ .username("username")
+ .password("password")
+ .build();
+ PrincipalDetails principalDetails = new PrincipalDetails(userEntity);
+
+ given(userRepository.findByUsername(principalDetails.getUsername()))
+ .willReturn(Optional.of(userEntity));
+ given(bCryptPasswordEncoder.matches(request.getOriginalPassword(), userEntity.getPassword()))
+ .willReturn(false);
+
+ //when
+ CustomUserException exception = assertThrows(CustomUserException.class,
+ () -> userService.updatePassword(request, principalDetails));
+
+ //then
+ assertEquals(UserErrorCode.PASSWORD_UN_MATCHED, exception.getUserErrorCode());
+
+ }
+
+ @Test
+ @DisplayName("이메일 수정 성공")
+ void updateEmail_success() {
+ //given
+ UpdateEmailDto.Request request = new UpdateEmailDto.Request();
+ request.setEmail("email2");
+ UserEntity userEntity = UserEntity.builder()
+ .id(1L)
+ .username("username")
+ .email("email")
+ .build();
+ PrincipalDetails principalDetails = new PrincipalDetails(userEntity);
+
+ given(userRepository.findByUsername(principalDetails.getUsername()))
+ .willReturn(Optional.of(userEntity));
+
+ //when
+ UpdateEmailDto.Response response = userService.updateEmail(request, principalDetails);
+
+ //then
+ assertEquals(true, response.getSuccess());
+ assertEquals(1L, response.getUserId());
+ assertEquals("email2", response.getChangedEmail());
+ }
+
+ @Test
+ @DisplayName("이메일 수정 실패 - 존재하지 않는 사용자")
+ void updateEmail_fail_userNotFound() {
+ //given
+ UpdateEmailDto.Request request = new UpdateEmailDto.Request();
+ UserEntity userEntity = UserEntity.builder()
+ .username("username")
+ .build();
+ PrincipalDetails principalDetails = new PrincipalDetails(userEntity);
+
+ given(userRepository.findByUsername(principalDetails.getUsername()))
+ .willReturn(Optional.empty());
+
+ //when
+ CustomUserException exception = assertThrows(CustomUserException.class,
+ () -> userService.updateEmail(request, principalDetails));
+
+ //then
+ assertEquals(UserErrorCode.USER_NOT_FOUND, exception.getUserErrorCode());
+
+ }
+
+ @Test
+ @DisplayName("닉네임 수정 성공")
+ void updateNickname_success() {
+ //given
+ UpdateNicknameDto.Request request = new UpdateNicknameDto.Request();
+ request.setNickname("nickname2");
+ UserEntity userEntity = UserEntity.builder()
+ .id(1L)
+ .username("username")
+ .nickname("nickname")
+ .build();
+ PrincipalDetails principalDetails = new PrincipalDetails(userEntity);
+
+ given(userRepository.findByUsername(principalDetails.getUsername()))
+ .willReturn(Optional.of(userEntity));
+
+ //when
+ UpdateNicknameDto.Response response = userService.updateNickname(request, principalDetails);
+
+ //then
+ assertEquals(true, response.getSuccess());
+ assertEquals(1L, response.getUserId());
+ assertEquals("nickname2", response.getChangedNickname());
+ }
+
+ @Test
+ @DisplayName("닉네임 수정 실패 - 존재하지 않는 사용자")
+ void updateNickname_fail_userNotFound() {
+ //given
+ UpdateNicknameDto.Request request = new UpdateNicknameDto.Request();
+ UserEntity userEntity = UserEntity.builder()
+ .username("username")
+ .build();
+ PrincipalDetails principalDetails = new PrincipalDetails(userEntity);
+
+ given(userRepository.findByUsername(principalDetails.getUsername()))
+ .willReturn(Optional.empty());
+
+ //when
+ CustomUserException exception = assertThrows(CustomUserException.class,
+ () -> userService.updateNickname(request, principalDetails));
+
+ //then
+ assertEquals(UserErrorCode.USER_NOT_FOUND, exception.getUserErrorCode());
+
+ }
+
+ @Test
+ @DisplayName("사용자 삭제 성공")
+ void deleteUser_success() {
+ //given
+ UserEntity userEntity = UserEntity.builder()
+ .id(1L)
+ .build();
+ PrincipalDetails principalDetails = new PrincipalDetails(userEntity);
+
+ given(userRepository.findByUsername(principalDetails.getUsername()))
+ .willReturn(Optional.of(userEntity));
+
+ //when
+ DeleteUserDto.Response response = userService.deleteUser(principalDetails);
+
+ //then
+ assertEquals(true, response.getSuccess());
+ assertEquals(1L, response.getUserId());
+ }
+
+ @Test
+ @DisplayName("사용자 삭제 실패 - 존재하지 않는 사용자")
+ void deleteUser_fail_userNotFound() {
+ //given
+ UserEntity userEntity = UserEntity.builder()
+ .username("username")
+ .build();
+ PrincipalDetails principalDetails = new PrincipalDetails(userEntity);
+
+ given(userRepository.findByUsername(principalDetails.getUsername()))
+ .willReturn(Optional.empty());
+
+ //when
+ CustomUserException exception = assertThrows(CustomUserException.class,
+ () -> userService.deleteUser(principalDetails));
+
+ //then
+ assertEquals(UserErrorCode.USER_NOT_FOUND, exception.getUserErrorCode());
+ }
+}
\ No newline at end of file