mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2025-12-06 06:46:25 +00:00
add common module
This commit is contained in:
parent
8029d8f9ce
commit
ea82ff4748
BIN
.gradle/8.10/checksums/checksums.lock
Normal file
BIN
.gradle/8.10/checksums/checksums.lock
Normal file
Binary file not shown.
BIN
.gradle/8.10/checksums/md5-checksums.bin
Normal file
BIN
.gradle/8.10/checksums/md5-checksums.bin
Normal file
Binary file not shown.
BIN
.gradle/8.10/checksums/sha1-checksums.bin
Normal file
BIN
.gradle/8.10/checksums/sha1-checksums.bin
Normal file
Binary file not shown.
0
.gradle/8.10/dependencies-accessors/gc.properties
Normal file
0
.gradle/8.10/dependencies-accessors/gc.properties
Normal file
BIN
.gradle/8.10/executionHistory/executionHistory.bin
Normal file
BIN
.gradle/8.10/executionHistory/executionHistory.bin
Normal file
Binary file not shown.
BIN
.gradle/8.10/executionHistory/executionHistory.lock
Normal file
BIN
.gradle/8.10/executionHistory/executionHistory.lock
Normal file
Binary file not shown.
BIN
.gradle/8.10/fileChanges/last-build.bin
Normal file
BIN
.gradle/8.10/fileChanges/last-build.bin
Normal file
Binary file not shown.
BIN
.gradle/8.10/fileHashes/fileHashes.bin
Normal file
BIN
.gradle/8.10/fileHashes/fileHashes.bin
Normal file
Binary file not shown.
BIN
.gradle/8.10/fileHashes/fileHashes.lock
Normal file
BIN
.gradle/8.10/fileHashes/fileHashes.lock
Normal file
Binary file not shown.
BIN
.gradle/8.10/fileHashes/resourceHashesCache.bin
Normal file
BIN
.gradle/8.10/fileHashes/resourceHashesCache.bin
Normal file
Binary file not shown.
0
.gradle/8.10/gc.properties
Normal file
0
.gradle/8.10/gc.properties
Normal file
BIN
.gradle/9.1.0/checksums/checksums.lock
Normal file
BIN
.gradle/9.1.0/checksums/checksums.lock
Normal file
Binary file not shown.
BIN
.gradle/9.1.0/executionHistory/executionHistory.bin
Normal file
BIN
.gradle/9.1.0/executionHistory/executionHistory.bin
Normal file
Binary file not shown.
BIN
.gradle/9.1.0/executionHistory/executionHistory.lock
Normal file
BIN
.gradle/9.1.0/executionHistory/executionHistory.lock
Normal file
Binary file not shown.
BIN
.gradle/9.1.0/fileChanges/last-build.bin
Normal file
BIN
.gradle/9.1.0/fileChanges/last-build.bin
Normal file
Binary file not shown.
BIN
.gradle/9.1.0/fileHashes/fileHashes.bin
Normal file
BIN
.gradle/9.1.0/fileHashes/fileHashes.bin
Normal file
Binary file not shown.
BIN
.gradle/9.1.0/fileHashes/fileHashes.lock
Normal file
BIN
.gradle/9.1.0/fileHashes/fileHashes.lock
Normal file
Binary file not shown.
0
.gradle/9.1.0/gc.properties
Normal file
0
.gradle/9.1.0/gc.properties
Normal file
BIN
.gradle/buildOutputCleanup/buildOutputCleanup.lock
Normal file
BIN
.gradle/buildOutputCleanup/buildOutputCleanup.lock
Normal file
Binary file not shown.
2
.gradle/buildOutputCleanup/cache.properties
Normal file
2
.gradle/buildOutputCleanup/cache.properties
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
#Thu Oct 23 17:51:21 KST 2025
|
||||||
|
gradle.version=8.10
|
||||||
BIN
.gradle/buildOutputCleanup/outputFiles.bin
Normal file
BIN
.gradle/buildOutputCleanup/outputFiles.bin
Normal file
Binary file not shown.
BIN
.gradle/file-system.probe
Normal file
BIN
.gradle/file-system.probe
Normal file
Binary file not shown.
0
.gradle/vcs-1/gc.properties
Normal file
0
.gradle/vcs-1/gc.properties
Normal file
17
ai-service/build.gradle
Normal file
17
ai-service/build.gradle
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
dependencies {
|
||||||
|
// Kafka Consumer
|
||||||
|
implementation 'org.springframework.kafka:spring-kafka'
|
||||||
|
|
||||||
|
// Redis for result caching
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||||
|
|
||||||
|
// OpenFeign for Claude/GPT API
|
||||||
|
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
|
||||||
|
|
||||||
|
// Resilience4j for Circuit Breaker
|
||||||
|
implementation "io.github.resilience4j:resilience4j-spring-boot3:${resilience4jVersion}"
|
||||||
|
implementation "io.github.resilience4j:resilience4j-circuitbreaker:${resilience4jVersion}"
|
||||||
|
|
||||||
|
// Jackson for JSON
|
||||||
|
implementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||||
|
}
|
||||||
17
analytics-service/build.gradle
Normal file
17
analytics-service/build.gradle
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
dependencies {
|
||||||
|
// Kafka Consumer
|
||||||
|
implementation 'org.springframework.kafka:spring-kafka'
|
||||||
|
|
||||||
|
// Redis for caching
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||||
|
|
||||||
|
// OpenFeign for external APIs (우리동네TV, 지니TV, SNS)
|
||||||
|
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
|
||||||
|
|
||||||
|
// Resilience4j for Circuit Breaker
|
||||||
|
implementation "io.github.resilience4j:resilience4j-spring-boot3:${resilience4jVersion}"
|
||||||
|
implementation "io.github.resilience4j:resilience4j-circuitbreaker:${resilience4jVersion}"
|
||||||
|
|
||||||
|
// Jackson for JSON
|
||||||
|
implementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||||
|
}
|
||||||
124
build.gradle
Normal file
124
build.gradle
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'org.springframework.boot' version '3.3.0' apply false
|
||||||
|
id 'io.spring.dependency-management' version '1.1.6' apply false
|
||||||
|
id 'io.freefair.lombok' version '8.10' apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
group = 'com.kt.event'
|
||||||
|
version = '1.0.0'
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
apply plugin: 'java'
|
||||||
|
apply plugin: 'io.freefair.lombok'
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
compileOnly {
|
||||||
|
extendsFrom annotationProcessor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named('test') {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common versions for all subprojects
|
||||||
|
ext {
|
||||||
|
jjwtVersion = '0.12.5'
|
||||||
|
springdocVersion = '2.5.0'
|
||||||
|
mapstructVersion = '1.5.5.Final'
|
||||||
|
commonsLang3Version = '3.14.0'
|
||||||
|
commonsIoVersion = '2.16.1'
|
||||||
|
hypersistenceVersion = '3.7.3'
|
||||||
|
openaiVersion = '0.18.2'
|
||||||
|
feignJacksonVersion = '13.1'
|
||||||
|
resilience4jVersion = '2.2.0'
|
||||||
|
azureStorageVersion = '12.25.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure all subprojects with Spring dependency management
|
||||||
|
subprojects {
|
||||||
|
apply plugin: 'io.spring.dependency-management'
|
||||||
|
|
||||||
|
dependencyManagement {
|
||||||
|
imports {
|
||||||
|
mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure only service modules (exclude common)
|
||||||
|
configure(subprojects.findAll { it.name != 'common' }) {
|
||||||
|
apply plugin: 'org.springframework.boot'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Common module dependency
|
||||||
|
implementation project(':common')
|
||||||
|
|
||||||
|
// Spring Boot Starters
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
|
|
||||||
|
// Actuator for health checks and monitoring
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||||
|
|
||||||
|
// Kafka
|
||||||
|
implementation 'org.springframework.kafka:spring-kafka'
|
||||||
|
|
||||||
|
// API Documentation (common across all services)
|
||||||
|
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springdocVersion}"
|
||||||
|
|
||||||
|
// Database
|
||||||
|
runtimeOnly 'org.postgresql:postgresql'
|
||||||
|
|
||||||
|
// Testing
|
||||||
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||||
|
testImplementation 'org.springframework.security:spring-security-test'
|
||||||
|
testImplementation 'org.springframework.kafka:spring-kafka-test'
|
||||||
|
testImplementation 'org.mockito:mockito-junit-jupiter'
|
||||||
|
|
||||||
|
// Configuration Processor
|
||||||
|
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure bootJar task for each service
|
||||||
|
bootJar {
|
||||||
|
archiveFileName = "${project.name}.jar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Java version consistency check for all modules
|
||||||
|
tasks.register('checkJavaVersion') {
|
||||||
|
doLast {
|
||||||
|
println "Java Version: ${System.getProperty('java.version')}"
|
||||||
|
println "Java Home: ${System.getProperty('java.home')}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean task for all subprojects
|
||||||
|
tasks.register('cleanAll') {
|
||||||
|
dependsOn subprojects.collect { it.tasks.named('clean') }
|
||||||
|
description = 'Clean all subprojects'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build task for all subprojects
|
||||||
|
tasks.register('buildAll') {
|
||||||
|
dependsOn subprojects.collect { it.tasks.named('build') }
|
||||||
|
description = 'Build all subprojects'
|
||||||
|
}
|
||||||
662
claude/dev-backend.md
Normal file
662
claude/dev-backend.md
Normal file
@ -0,0 +1,662 @@
|
|||||||
|
# 백엔드 개발 가이드
|
||||||
|
|
||||||
|
[요청사항]
|
||||||
|
- <개발원칙>을 준용하여 개발
|
||||||
|
- <개발순서>에 따라 아래 3단계로 개발
|
||||||
|
- '0. 준비'를 수행하고 완료 후 다음 단계 진행여부를 사용자에게 확인
|
||||||
|
- '1. common 모듈 개발'을 수행하고 완료 후 다음 단계 진행여부를 사용자에게 확인
|
||||||
|
- '2. 각 서비스별 구현'은 사용자와 함께 각 서비스를 개발
|
||||||
|
|
||||||
|
[가이드]
|
||||||
|
<개발원칙>
|
||||||
|
- '개발주석표준'에 맞게 주석 작성
|
||||||
|
- API설계서와 일관성 있게 설계. Controller에 API를 누락하지 말고 모두 개발
|
||||||
|
- '외부시퀀스설계서'와 '내부시퀀스설계서'와 일치되도록 개발
|
||||||
|
- 각 서비스별 지정된 {설계 아키텍처 패턴}을 적용하여 개발
|
||||||
|
- Layered 아키텍처 적용 시 Service레이어에 Interface 사용
|
||||||
|
- Clean아키텍처 적용 시 Port/Adapter라는 용어 대신 Clean 아키텍처에 맞는 용어 사용
|
||||||
|
- 백킹서비스 연동은 '데이터베이스설치결과서', '캐시설치결과서', 'MQ설치결과서'를 기반으로 개발
|
||||||
|
- 빌드도구는 Gradle 사용
|
||||||
|
- 설정 Manifest(src/main/resources/application*.yml) 작성 시 '[설정 Manifest 표준]' 준용
|
||||||
|
<개발순서>
|
||||||
|
- 0. 준비:
|
||||||
|
- 참고자료 분석 및 이해
|
||||||
|
- '패키지구조표준'의 예시를 참조하여 모든 클래스와 파일이 포함된 패키지 구조도를 작성
|
||||||
|
- plantuml 스크립트가 아니라 트리구조 텍스트로 작성
|
||||||
|
- 결과파일: develop/dev/package-structure.md
|
||||||
|
- settings.gralde 파일 작성
|
||||||
|
- build.gradle 작성
|
||||||
|
- '<Build.gradle 구성 최적화>' 가이드대로 최상위와 각 서비스별 build.gradle 작성
|
||||||
|
- '[루트 build.gradle 표준]'대로 최상위 build.gradle 작성
|
||||||
|
- SpringBoot 3.3.0, Java 21 사용
|
||||||
|
- common을 제외한 각 서비스에서 공통으로 사용되는 설정과 Dependency는 루트 build.gradle에 지정
|
||||||
|
- 서비스별 build.gradle 작성
|
||||||
|
- 최상위 build.gradle에 정의한 설정은 각 마이크로서비스의 build.gradle에 중복하여 정의하지 않도록 함
|
||||||
|
- 각 서비스의 실행 jar 파일명은 서비스명과 동일하게 함
|
||||||
|
- 각 서비스별 설정 파일 작성
|
||||||
|
- 설정 Manifest(application.yml) 작성: '[설정 Manifest 표준]' 준용
|
||||||
|
|
||||||
|
- 1. common 모듈 개발
|
||||||
|
- 각 서비스에서 공통으로 사용되는 클래스를 개발
|
||||||
|
- 외부(웹브라우저, 데이터베이스, Message Queue, 외부시스템)와의 인터페이스를 위한 클래스는 포함하지 않음
|
||||||
|
- 개발 완료 후 컴파일 및 에러 해결: {프로젝트 루트}/gradlew common:compileJava
|
||||||
|
|
||||||
|
- 2. 각 서비스별 개발
|
||||||
|
- 사용자가 제공한 서비스의 유저스토리, 외부시퀀스설계서, 내부시퀀스설계서, API설계서 파악
|
||||||
|
- 기존 개발 결과 파악
|
||||||
|
- API 설계서의 각 API를 순차적으로 개발
|
||||||
|
- Controller -> Service -> Data 레이어순으로 순차적으로 개발
|
||||||
|
- 컴파일 및 에러 해결: {프로젝트 루트}/gradlew {service-name}:compileJava
|
||||||
|
- 컴파일까지만 하고 서버 실행은 하지 않음
|
||||||
|
- 모든 API개발 후 아래 수행
|
||||||
|
- 컴파일 및 에러 해결: {프로젝트 루트}/gradlew {service-name}:compileJava
|
||||||
|
- 빌드 및 에러 해결: {프로젝트 루트}/gradlew {service-name}:build
|
||||||
|
- SecurityConfig 클래스 작성: '<SecurityConfig 예제>' 참조
|
||||||
|
- JWT 인증 처리 클래스 작성: '<JWT 인증처리 예제>' 참조
|
||||||
|
- Swagger Config 클래스 작성: '<SwaggerConfig 예제>' 참조
|
||||||
|
- 테스트 코드 작성은 하지 않음
|
||||||
|
|
||||||
|
|
||||||
|
<Build.gradle 구성 최적화>
|
||||||
|
- **중앙 버전 관리**: 루트 build.gradle의 `ext` 블록에서 모든 외부 라이브러리 버전 통일 관리
|
||||||
|
- **Spring Boot BOM 활용**: Spring Boot/Cloud에서 관리하는 라이브러리는 버전 명시 불필요 (자동 호환성 보장)
|
||||||
|
- **Common 모듈 설정**: `java-library` + Spring Boot 플러그인 조합, `bootJar` 비활성화로 일반 jar 생성
|
||||||
|
- **서비스별 최적화**: 공통 의존성(API 문서화, 테스트 등)은 루트에서 일괄 적용
|
||||||
|
- **JWT 버전 통일**: 라이브러리 버전 변경시 API 호환성 확인 필수 (`parserBuilder()` → `parser()`)
|
||||||
|
- **dependency-management 적용**: 모든 서브프로젝트에 Spring BOM 적용으로 버전 충돌 방지
|
||||||
|
|
||||||
|
[참고자료]
|
||||||
|
- 유저스토리
|
||||||
|
- API설계서
|
||||||
|
- 외부시퀀스설계서
|
||||||
|
- 내부시퀀스설계서
|
||||||
|
- 데이터베이스설치결과서
|
||||||
|
- 캐시설치결과서
|
||||||
|
- MQ설치결과서
|
||||||
|
- 테스트코드표준
|
||||||
|
- 패키지구조표준
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[설정 Manifest 표준]
|
||||||
|
- common모듈은 작성하지 않음
|
||||||
|
- application.yml에 작성
|
||||||
|
- 하드코딩하지 않고 환경변수 사용
|
||||||
|
특히, 데이터베이스, MQ 등의 연결 정보는 반드시 환경변수로 변환해야 함: '<DB/Redis 설정 예제>' 참조
|
||||||
|
- spring.application.name은 서비스명과 동일하게 함
|
||||||
|
- Redis Database는 각 서비스마다 다르게 설정
|
||||||
|
- 민감한 정보의 디폴트값은 생략하거나 간략한 값으로 지정
|
||||||
|
- JWT Secret Key는 모든 서비스가 동일해야 함
|
||||||
|
- '[JWT,CORS,Actuaotr,OpenAPI Documentation,Loggings 표준]'을 준수하여 설정
|
||||||
|
|
||||||
|
[JWT, CORS, Actuaotr,OpenAPI Documentation,Loggings 표준]
|
||||||
|
```
|
||||||
|
# JWT
|
||||||
|
jwt:
|
||||||
|
secret: ${JWT_SECRET:}
|
||||||
|
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:1800}
|
||||||
|
refresh-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:86400}
|
||||||
|
|
||||||
|
# CORS Configuration
|
||||||
|
cors:
|
||||||
|
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:*}
|
||||||
|
|
||||||
|
# Actuator
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
exposure:
|
||||||
|
include: health,info,metrics,prometheus
|
||||||
|
base-path: /actuator
|
||||||
|
endpoint:
|
||||||
|
health:
|
||||||
|
show-details: always
|
||||||
|
show-components: always
|
||||||
|
health:
|
||||||
|
livenessState:
|
||||||
|
enabled: true
|
||||||
|
readinessState:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# OpenAPI Documentation
|
||||||
|
springdoc:
|
||||||
|
api-docs:
|
||||||
|
path: /v3/api-docs
|
||||||
|
swagger-ui:
|
||||||
|
path: /swagger-ui.html
|
||||||
|
tags-sorter: alpha
|
||||||
|
operations-sorter: alpha
|
||||||
|
show-actuator: false
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
{서비스 패키지 경로}: ${LOG_LEVEL_APP:DEBUG}
|
||||||
|
org.springframework.web: ${LOG_LEVEL_WEB:INFO}
|
||||||
|
org.hibernate.SQL: ${LOG_LEVEL_SQL:DEBUG}
|
||||||
|
org.hibernate.type: ${LOG_LEVEL_SQL_TYPE:TRACE}
|
||||||
|
pattern:
|
||||||
|
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
|
||||||
|
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||||
|
file:
|
||||||
|
name: ${LOG_FILE_PATH:logs/{서비스명}.log}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
[루트 build.gradle 표준]
|
||||||
|
```
|
||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'org.springframework.boot' version '3.3.0' apply false
|
||||||
|
id 'io.spring.dependency-management' version '1.1.6' apply false
|
||||||
|
id 'io.freefair.lombok' version '8.10' apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
group = 'com.unicorn.{시스템명}'
|
||||||
|
version = '1.0.0'
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
apply plugin: 'java'
|
||||||
|
apply plugin: 'io.freefair.lombok'
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
compileOnly {
|
||||||
|
extendsFrom annotationProcessor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named('test') {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common versions for all subprojects
|
||||||
|
ext {
|
||||||
|
jjwtVersion = '0.12.5'
|
||||||
|
springdocVersion = '2.5.0'
|
||||||
|
mapstructVersion = '1.5.5.Final'
|
||||||
|
commonsLang3Version = '3.14.0'
|
||||||
|
commonsIoVersion = '2.16.1'
|
||||||
|
hypersistenceVersion = '3.7.3'
|
||||||
|
openaiVersion = '0.18.2'
|
||||||
|
feignJacksonVersion = '13.1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure all subprojects with Spring dependency management
|
||||||
|
subprojects {
|
||||||
|
apply plugin: 'io.spring.dependency-management'
|
||||||
|
|
||||||
|
dependencyManagement {
|
||||||
|
imports {
|
||||||
|
mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure only service modules (exclude common)
|
||||||
|
configure(subprojects.findAll { it.name != 'common' }) {
|
||||||
|
apply plugin: 'org.springframework.boot'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Common module dependency
|
||||||
|
implementation project(':common')
|
||||||
|
|
||||||
|
// Actuator for health checks and monitoring
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||||
|
|
||||||
|
// API Documentation (common across all services)
|
||||||
|
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springdocVersion}"
|
||||||
|
|
||||||
|
// Testing
|
||||||
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||||
|
testImplementation 'org.springframework.security:spring-security-test'
|
||||||
|
testImplementation 'org.testcontainers:junit-jupiter'
|
||||||
|
testImplementation 'org.mockito:mockito-junit-jupiter'
|
||||||
|
|
||||||
|
// Configuration Processor
|
||||||
|
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Java version consistency check for all modules
|
||||||
|
tasks.register('checkJavaVersion') {
|
||||||
|
doLast {
|
||||||
|
println "Java Version: ${System.getProperty('java.version')}"
|
||||||
|
println "Java Home: ${System.getProperty('java.home')}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean task for all subprojects
|
||||||
|
tasks.register('cleanAll') {
|
||||||
|
dependsOn subprojects.collect { it.tasks.named('clean') }
|
||||||
|
description = 'Clean all subprojects'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build task for all subprojects
|
||||||
|
tasks.register('buildAll') {
|
||||||
|
dependsOn subprojects.collect { it.tasks.named('build') }
|
||||||
|
description = 'Build all subprojects'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<DB/Redis 설정 예제>
|
||||||
|
```
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:phonebill_auth}
|
||||||
|
username: ${DB_USERNAME:phonebill_user}
|
||||||
|
password: ${DB_PASSWORD:phonebill_pass}
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
hikari:
|
||||||
|
maximum-pool-size: 20
|
||||||
|
minimum-idle: 5
|
||||||
|
connection-timeout: 30000
|
||||||
|
idle-timeout: 600000
|
||||||
|
max-lifetime: 1800000
|
||||||
|
leak-detection-threshold: 60000
|
||||||
|
# JPA 설정
|
||||||
|
jpa:
|
||||||
|
show-sql: ${SHOW_SQL:true}
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
format_sql: true
|
||||||
|
use_sql_comments: true
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: ${DDL_AUTO:update}
|
||||||
|
|
||||||
|
# Redis 설정
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: ${REDIS_HOST:localhost}
|
||||||
|
port: ${REDIS_PORT:6379}
|
||||||
|
password: ${REDIS_PASSWORD:}
|
||||||
|
timeout: 2000ms
|
||||||
|
lettuce:
|
||||||
|
pool:
|
||||||
|
max-active: 8
|
||||||
|
max-idle: 8
|
||||||
|
min-idle: 0
|
||||||
|
max-wait: -1ms
|
||||||
|
database: ${REDIS_DATABASE:0}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
<SecurityConfig 예제>
|
||||||
|
```
|
||||||
|
/**
|
||||||
|
* Spring Security 설정
|
||||||
|
* JWT 기반 인증 및 API 보안 설정
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
|
||||||
|
@Value("${cors.allowed-origins:http://localhost:3000,http://localhost:8080,http://localhost:8081,http://localhost:8082,http://localhost:8083,http://localhost:8084}")
|
||||||
|
private String allowedOrigins;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
return http
|
||||||
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||||
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
// Actuator endpoints
|
||||||
|
.requestMatchers("/actuator/**").permitAll()
|
||||||
|
// Swagger UI endpoints - context path와 상관없이 접근 가능하도록 설정
|
||||||
|
.requestMatchers("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**").permitAll()
|
||||||
|
// Health check
|
||||||
|
.requestMatchers("/health").permitAll()
|
||||||
|
// All other requests require authentication
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
|
||||||
|
UsernamePasswordAuthenticationFilter.class)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
|
CorsConfiguration configuration = new CorsConfiguration();
|
||||||
|
|
||||||
|
// 환경변수에서 허용할 Origin 패턴 설정
|
||||||
|
String[] origins = allowedOrigins.split(",");
|
||||||
|
configuration.setAllowedOriginPatterns(Arrays.asList(origins));
|
||||||
|
|
||||||
|
// 허용할 HTTP 메소드
|
||||||
|
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
|
||||||
|
|
||||||
|
// 허용할 헤더
|
||||||
|
configuration.setAllowedHeaders(Arrays.asList(
|
||||||
|
"Authorization", "Content-Type", "X-Requested-With", "Accept",
|
||||||
|
"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"
|
||||||
|
));
|
||||||
|
|
||||||
|
// 자격 증명 허용
|
||||||
|
configuration.setAllowCredentials(true);
|
||||||
|
|
||||||
|
// Pre-flight 요청 캐시 시간
|
||||||
|
configuration.setMaxAge(3600L);
|
||||||
|
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/**", configuration);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<JWT 인증처리 예제>
|
||||||
|
|
||||||
|
1) JwtAuthenticationFilter
|
||||||
|
```
|
||||||
|
/**
|
||||||
|
* JWT 인증 필터
|
||||||
|
* HTTP 요청에서 JWT 토큰을 추출하여 인증을 수행
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
|
||||||
|
String token = jwtTokenProvider.resolveToken(request);
|
||||||
|
|
||||||
|
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
|
||||||
|
String userId = jwtTokenProvider.getUserId(token);
|
||||||
|
String username = null;
|
||||||
|
String authority = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
username = jwtTokenProvider.getUsername(token);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("JWT에 username 클레임이 없음: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
authority = jwtTokenProvider.getAuthority(token);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("JWT에 authority 클레임이 없음: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.hasText(userId)) {
|
||||||
|
// UserPrincipal 객체 생성 (username과 authority가 없어도 동작)
|
||||||
|
UserPrincipal userPrincipal = UserPrincipal.builder()
|
||||||
|
.userId(userId)
|
||||||
|
.username(username != null ? username : "unknown")
|
||||||
|
.authority(authority != null ? authority : "USER")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
UsernamePasswordAuthenticationToken authentication =
|
||||||
|
new UsernamePasswordAuthenticationToken(
|
||||||
|
userPrincipal,
|
||||||
|
null,
|
||||||
|
Collections.singletonList(new SimpleGrantedAuthority(authority != null ? authority : "USER"))
|
||||||
|
);
|
||||||
|
|
||||||
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
|
||||||
|
log.debug("인증된 사용자: {} ({})", userPrincipal.getUsername(), userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||||
|
String path = request.getRequestURI();
|
||||||
|
return path.startsWith("/actuator") ||
|
||||||
|
path.startsWith("/swagger-ui") ||
|
||||||
|
path.startsWith("/v3/api-docs") ||
|
||||||
|
path.equals("/health");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
1) JwtTokenProvider
|
||||||
|
```
|
||||||
|
/**
|
||||||
|
* JWT 토큰 제공자
|
||||||
|
* JWT 토큰의 생성, 검증, 파싱을 담당
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class JwtTokenProvider {
|
||||||
|
|
||||||
|
private final SecretKey secretKey;
|
||||||
|
private final long tokenValidityInMilliseconds;
|
||||||
|
|
||||||
|
public JwtTokenProvider(@Value("${jwt.secret}") String secret,
|
||||||
|
@Value("${jwt.access-token-validity:3600}") long tokenValidityInSeconds) {
|
||||||
|
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||||
|
this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP 요청에서 JWT 토큰 추출
|
||||||
|
*/
|
||||||
|
public String resolveToken(HttpServletRequest request) {
|
||||||
|
String bearerToken = request.getHeader("Authorization");
|
||||||
|
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
||||||
|
return bearerToken.substring(7);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 토큰 유효성 검증
|
||||||
|
*/
|
||||||
|
public boolean validateToken(String token) {
|
||||||
|
try {
|
||||||
|
Jwts.parser()
|
||||||
|
.setSigningKey(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token);
|
||||||
|
return true;
|
||||||
|
} catch (SecurityException | MalformedJwtException e) {
|
||||||
|
log.debug("Invalid JWT signature: {}", e.getMessage());
|
||||||
|
} catch (ExpiredJwtException e) {
|
||||||
|
log.debug("Expired JWT token: {}", e.getMessage());
|
||||||
|
} catch (UnsupportedJwtException e) {
|
||||||
|
log.debug("Unsupported JWT token: {}", e.getMessage());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.debug("JWT token compact of handler are invalid: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 토큰에서 사용자 ID 추출
|
||||||
|
*/
|
||||||
|
public String getUserId(String token) {
|
||||||
|
Claims claims = Jwts.parser()
|
||||||
|
.setSigningKey(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
|
||||||
|
return claims.getSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 토큰에서 사용자명 추출
|
||||||
|
*/
|
||||||
|
public String getUsername(String token) {
|
||||||
|
Claims claims = Jwts.parser()
|
||||||
|
.setSigningKey(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
|
||||||
|
return claims.get("username", String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 토큰에서 권한 정보 추출
|
||||||
|
*/
|
||||||
|
public String getAuthority(String token) {
|
||||||
|
Claims claims = Jwts.parser()
|
||||||
|
.setSigningKey(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
|
||||||
|
return claims.get("authority", String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰 만료 시간 확인
|
||||||
|
*/
|
||||||
|
public boolean isTokenExpired(String token) {
|
||||||
|
try {
|
||||||
|
Claims claims = Jwts.parser()
|
||||||
|
.setSigningKey(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
|
||||||
|
return claims.getExpiration().before(new Date());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰에서 만료 시간 추출
|
||||||
|
*/
|
||||||
|
public Date getExpirationDate(String token) {
|
||||||
|
Claims claims = Jwts.parser()
|
||||||
|
.setSigningKey(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
|
||||||
|
return claims.getExpiration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
1) UserPrincipal
|
||||||
|
```
|
||||||
|
/**
|
||||||
|
* 인증된 사용자 정보
|
||||||
|
* JWT 토큰에서 추출된 사용자 정보를 담는 Principal 객체
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserPrincipal {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 고유 ID
|
||||||
|
*/
|
||||||
|
private final String userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자명
|
||||||
|
*/
|
||||||
|
private final String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 권한
|
||||||
|
*/
|
||||||
|
private final String authority;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 ID 반환 (별칭)
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관리자 권한 여부 확인
|
||||||
|
*/
|
||||||
|
public boolean isAdmin() {
|
||||||
|
return "ADMIN".equals(authority);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일반 사용자 권한 여부 확인
|
||||||
|
*/
|
||||||
|
public boolean isUser() {
|
||||||
|
return "USER".equals(authority) || authority == null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<SwaggerConfig 예제>
|
||||||
|
```
|
||||||
|
/**
|
||||||
|
* Swagger/OpenAPI 설정
|
||||||
|
* AI Service API 문서화를 위한 설정
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class SwaggerConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI openAPI() {
|
||||||
|
return new OpenAPI()
|
||||||
|
.info(apiInfo())
|
||||||
|
.addServersItem(new Server()
|
||||||
|
.url("http://localhost:8084")
|
||||||
|
.description("Local Development"))
|
||||||
|
.addServersItem(new Server()
|
||||||
|
.url("{protocol}://{host}:{port}")
|
||||||
|
.description("Custom Server")
|
||||||
|
.variables(new io.swagger.v3.oas.models.servers.ServerVariables()
|
||||||
|
.addServerVariable("protocol", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||||
|
._default("http")
|
||||||
|
.description("Protocol (http or https)")
|
||||||
|
.addEnumItem("http")
|
||||||
|
.addEnumItem("https"))
|
||||||
|
.addServerVariable("host", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||||
|
._default("localhost")
|
||||||
|
.description("Server host"))
|
||||||
|
.addServerVariable("port", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||||
|
._default("8084")
|
||||||
|
.description("Server port"))))
|
||||||
|
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
|
||||||
|
.components(new Components()
|
||||||
|
.addSecuritySchemes("Bearer Authentication", createAPIKeyScheme()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Info apiInfo() {
|
||||||
|
return new Info()
|
||||||
|
.title("AI Service API")
|
||||||
|
.description("AI 기반 시간별 상세 일정 생성 및 장소 추천 정보 API")
|
||||||
|
.version("1.0.0")
|
||||||
|
.contact(new Contact()
|
||||||
|
.name("TripGen Development Team")
|
||||||
|
.email("dev@tripgen.com"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private SecurityScheme createAPIKeyScheme() {
|
||||||
|
return new SecurityScheme()
|
||||||
|
.type(SecurityScheme.Type.HTTP)
|
||||||
|
.bearerFormat("JWT")
|
||||||
|
.scheme("bearer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
518
claude/standard_comment.md
Normal file
518
claude/standard_comment.md
Normal file
@ -0,0 +1,518 @@
|
|||||||
|
# 개발주석표준 가이드
|
||||||
|
|
||||||
|
## 📋 개요
|
||||||
|
|
||||||
|
이 문서는 CMS 프로젝트의 JavaDoc 주석 작성 표준을 정의합니다. 일관된 주석 스타일을 통해 코드의 가독성과 유지보수성을 향상시키는 것을 목표로 합니다.
|
||||||
|
|
||||||
|
## 🎯 주석 작성 원칙
|
||||||
|
|
||||||
|
### 1. **기본 원칙**
|
||||||
|
- **명확성**: 코드의 의도와 동작을 명확하게 설명
|
||||||
|
- **일관성**: 프로젝트 전체에서 동일한 스타일 적용
|
||||||
|
- **완전성**: 모든 public 메서드와 클래스에 주석 작성
|
||||||
|
- **최신성**: 코드 변경 시 주석도 함께 업데이트
|
||||||
|
|
||||||
|
### 2. **주석 대상**
|
||||||
|
- **필수**: public 클래스, 인터페이스, 메서드
|
||||||
|
- **권장**: protected 메서드, 중요한 필드
|
||||||
|
- **선택**: private 메서드 (복잡한 로직인 경우)
|
||||||
|
|
||||||
|
## 📝 JavaDoc 기본 문법
|
||||||
|
|
||||||
|
### 1. **기본 구조**
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 클래스나 메서드의 간단한 설명 (첫 번째 문장)
|
||||||
|
*
|
||||||
|
* <p>상세한 설명이 필요한 경우 여기에 작성합니다.</p>
|
||||||
|
*
|
||||||
|
* @param paramName 파라미터 설명
|
||||||
|
* @return 반환값 설명
|
||||||
|
* @throws ExceptionType 예외 상황 설명
|
||||||
|
* @since 1.0
|
||||||
|
* @author 작성자명
|
||||||
|
* @see 관련클래스#메서드
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **주요 JavaDoc 태그**
|
||||||
|
|
||||||
|
| 태그 | 설명 | 사용 위치 |
|
||||||
|
|------|------|-----------|
|
||||||
|
| `@param` | 메서드 파라미터 설명 | 메서드 |
|
||||||
|
| `@return` | 반환값 설명 | 메서드 |
|
||||||
|
| `@throws` | 예외 상황 설명 | 메서드 |
|
||||||
|
| `@since` | 도입 버전 | 클래스, 메서드 |
|
||||||
|
| `@author` | 작성자 | 클래스 |
|
||||||
|
| `@version` | 버전 정보 | 클래스 |
|
||||||
|
| `@see` | 관련 항목 참조 | 모든 곳 |
|
||||||
|
| `@apiNote` | API 사용 시 주의사항 | 메서드 |
|
||||||
|
| `@implNote` | 구현 관련 참고사항 | 메서드 |
|
||||||
|
|
||||||
|
## 🎨 HTML 태그 활용 가이드
|
||||||
|
|
||||||
|
### 1. **HTML 태그 사용 이유**
|
||||||
|
|
||||||
|
JavaDoc은 소스코드 주석을 파싱하여 **HTML 형태의 API 문서**를 자동 생성합니다. HTML 태그를 사용하면:
|
||||||
|
|
||||||
|
- **가독성 향상**: 구조화된 문서로 이해하기 쉬움
|
||||||
|
- **자동 문서화**: JavaDoc 도구가 예쁜 HTML 문서 생성
|
||||||
|
- **IDE 지원**: 개발 도구에서 리치 텍스트로 표시
|
||||||
|
- **표준 준수**: Oracle JavaDoc 스타일 가이드 준수
|
||||||
|
|
||||||
|
### 2. **자주 사용되는 HTML 태그**
|
||||||
|
|
||||||
|
#### **텍스트 서식**
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* <p>단락을 구분할 때 사용합니다.</p>
|
||||||
|
* <b>중요한 내용</b>을 강조할 때 사용합니다.
|
||||||
|
* <i>이탤릭체</i>로 표시할 때 사용합니다.
|
||||||
|
* <code>method()</code>와 같은 코드를 표시할 때 사용합니다.
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **목록 작성**
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* <p><b>주요 기능:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>첫 번째 기능</li>
|
||||||
|
* <li>두 번째 기능</li>
|
||||||
|
* <li>세 번째 기능</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><b>처리 과정:</b></p>
|
||||||
|
* <ol>
|
||||||
|
* <li>첫 번째 단계</li>
|
||||||
|
* <li>두 번째 단계</li>
|
||||||
|
* <li>세 번째 단계</li>
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **코드 블록**
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* <p>사용 예시:</p>
|
||||||
|
* <pre>
|
||||||
|
* AuthController controller = new AuthController();
|
||||||
|
* LoginRequest request = new LoginRequest("user", "password");
|
||||||
|
* ResponseEntity<LoginResponse> response = controller.login(request);
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **테이블**
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* <p><b>HTTP 상태 코드:</b></p>
|
||||||
|
* <table>
|
||||||
|
* <tr><th>상태 코드</th><th>설명</th></tr>
|
||||||
|
* <tr><td>200</td><td>성공</td></tr>
|
||||||
|
* <tr><td>400</td><td>잘못된 요청</td></tr>
|
||||||
|
* <tr><td>401</td><td>인증 실패</td></tr>
|
||||||
|
* </table>
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **HTML 태그 사용 규칙**
|
||||||
|
|
||||||
|
- **<와 >**: 제네릭 타입 표현 시 `<T>` 사용
|
||||||
|
- **줄바꿈**: `<br>` 태그 사용 (가급적 `<p>` 태그 권장)
|
||||||
|
- **링크**: `{@link ClassName#methodName}` 사용
|
||||||
|
- **인라인 코드**: `{@code variableName}` 또는 `<code>` 사용
|
||||||
|
|
||||||
|
## 📋 클래스 주석 표준
|
||||||
|
|
||||||
|
### 1. **클래스 주석 템플릿**
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 클래스의 간단한 설명
|
||||||
|
*
|
||||||
|
* <p>클래스의 상세한 설명과 목적을 여기에 작성합니다.</p>
|
||||||
|
*
|
||||||
|
* <p><b>주요 기능:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>기능 1</li>
|
||||||
|
* <li>기능 2</li>
|
||||||
|
* <li>기능 3</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><b>사용 예시:</b></p>
|
||||||
|
* <pre>
|
||||||
|
* ClassName instance = new ClassName();
|
||||||
|
* instance.someMethod();
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <p><b>주의사항:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>주의사항 1</li>
|
||||||
|
* <li>주의사항 2</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author 작성자명
|
||||||
|
* @version 1.0
|
||||||
|
* @since 2024-01-01
|
||||||
|
*
|
||||||
|
* @see 관련클래스1
|
||||||
|
* @see 관련클래스2
|
||||||
|
*/
|
||||||
|
public class ClassName {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Controller 클래스 주석 예시**
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 사용자 관리 API 컨트롤러
|
||||||
|
*
|
||||||
|
* <p>사용자 등록, 조회, 수정, 삭제 기능을 제공하는 REST API 컨트롤러입니다.</p>
|
||||||
|
*
|
||||||
|
* <p><b>주요 기능:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>사용자 등록 및 인증</li>
|
||||||
|
* <li>사용자 정보 조회 및 수정</li>
|
||||||
|
* <li>사용자 권한 관리</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><b>API 엔드포인트:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>POST /api/users - 사용자 등록</li>
|
||||||
|
* <li>GET /api/users/{id} - 사용자 조회</li>
|
||||||
|
* <li>PUT /api/users/{id} - 사용자 수정</li>
|
||||||
|
* <li>DELETE /api/users/{id} - 사용자 삭제</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><b>보안 고려사항:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>모든 엔드포인트는 인증이 필요합니다</li>
|
||||||
|
* <li>개인정보 처리 시 데이터 마스킹 적용</li>
|
||||||
|
* <li>입력값 검증 및 XSS 방지</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author cms-team
|
||||||
|
* @version 1.0
|
||||||
|
* @since 2024-01-01
|
||||||
|
*
|
||||||
|
* @see UserService
|
||||||
|
* @see UserRepository
|
||||||
|
* @see UserDTO
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/users")
|
||||||
|
public class UserController {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 메서드 주석 표준
|
||||||
|
|
||||||
|
### 1. **메서드 주석 템플릿**
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 메서드의 간단한 설명
|
||||||
|
*
|
||||||
|
* <p>메서드의 상세한 설명과 동작을 여기에 작성합니다.</p>
|
||||||
|
*
|
||||||
|
* <p><b>처리 과정:</b></p>
|
||||||
|
* <ol>
|
||||||
|
* <li>첫 번째 단계</li>
|
||||||
|
* <li>두 번째 단계</li>
|
||||||
|
* <li>세 번째 단계</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p><b>주의사항:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>주의사항 1</li>
|
||||||
|
* <li>주의사항 2</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param param1 첫 번째 파라미터 설명
|
||||||
|
* - 추가 설명이 필요한 경우
|
||||||
|
* @param param2 두 번째 파라미터 설명
|
||||||
|
*
|
||||||
|
* @return 반환값 설명
|
||||||
|
* - 성공 시: 설명
|
||||||
|
* - 실패 시: 설명
|
||||||
|
*
|
||||||
|
* @throws ExceptionType1 예외 상황 1 설명
|
||||||
|
* @throws ExceptionType2 예외 상황 2 설명
|
||||||
|
*
|
||||||
|
* @apiNote API 사용 시 주의사항
|
||||||
|
*
|
||||||
|
* @see 관련메서드1
|
||||||
|
* @see 관련메서드2
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public ReturnType methodName(Type param1, Type param2) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **API 메서드 주석 예시**
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 사용자 로그인 처리
|
||||||
|
*
|
||||||
|
* <p>사용자 ID와 비밀번호를 검증하여 JWT 토큰을 생성합니다.</p>
|
||||||
|
*
|
||||||
|
* <p><b>처리 과정:</b></p>
|
||||||
|
* <ol>
|
||||||
|
* <li>입력값 검증 (@Valid 어노테이션)</li>
|
||||||
|
* <li>사용자 인증 정보 확인</li>
|
||||||
|
* <li>JWT 토큰 생성</li>
|
||||||
|
* <li>사용자 세션 시작</li>
|
||||||
|
* <li>로그인 메트릭 업데이트</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p><b>보안 고려사항:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>비밀번호는 BCrypt로 암호화된 값과 비교</li>
|
||||||
|
* <li>로그인 실패 시 상세 정보 노출 방지</li>
|
||||||
|
* <li>로그인 시도 로그 기록</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @param request 로그인 요청 정보
|
||||||
|
* - username: 사용자 ID (3-50자, 필수)
|
||||||
|
* - password: 비밀번호 (6-100자, 필수)
|
||||||
|
*
|
||||||
|
* @return ResponseEntity<LoginResponse> 로그인 응답 정보
|
||||||
|
* - 성공 시: 200 OK + JWT 토큰, 사용자 역할, 만료 시간
|
||||||
|
* - 실패 시: 401 Unauthorized + 에러 메시지
|
||||||
|
*
|
||||||
|
* @throws InvalidCredentialsException 인증 정보가 올바르지 않은 경우
|
||||||
|
* @throws RuntimeException 로그인 처리 중 시스템 오류 발생 시
|
||||||
|
*
|
||||||
|
* @apiNote 보안상 이유로 로그인 실패 시 구체적인 실패 사유를 반환하지 않습니다.
|
||||||
|
*
|
||||||
|
* @see AuthService#login(LoginRequest)
|
||||||
|
* @see UserSessionService#startSession(String, String, java.time.Instant)
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
@PostMapping("/login")
|
||||||
|
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 필드 주석 표준
|
||||||
|
|
||||||
|
### 1. **필드 주석 템플릿**
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 필드의 간단한 설명
|
||||||
|
*
|
||||||
|
* <p>필드의 상세한 설명과 용도를 여기에 작성합니다.</p>
|
||||||
|
*
|
||||||
|
* <p><b>주의사항:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>주의사항 1</li>
|
||||||
|
* <li>주의사항 2</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
private final ServiceType serviceName;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **의존성 주입 필드 예시**
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 인증 서비스
|
||||||
|
*
|
||||||
|
* <p>사용자 로그인/로그아웃 처리 및 JWT 토큰 관리를 담당합니다.</p>
|
||||||
|
*
|
||||||
|
* <p><b>주요 기능:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>사용자 인증 정보 검증</li>
|
||||||
|
* <li>JWT 토큰 생성 및 검증</li>
|
||||||
|
* <li>로그인/로그아웃 처리</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @see AuthService
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
private final AuthService authService;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 예외 클래스 주석 표준
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 사용자 인증 실패 예외
|
||||||
|
*
|
||||||
|
* <p>로그인 시 사용자 ID 또는 비밀번호가 올바르지 않을 때 발생하는 예외입니다.</p>
|
||||||
|
*
|
||||||
|
* <p><b>발생 상황:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>존재하지 않는 사용자 ID</li>
|
||||||
|
* <li>잘못된 비밀번호</li>
|
||||||
|
* <li>계정 잠금 상태</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><b>처리 방법:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>사용자에게 일반적인 오류 메시지 표시</li>
|
||||||
|
* <li>보안 로그에 상세 정보 기록</li>
|
||||||
|
* <li>브루트 포스 공격 방지 로직 실행</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author cms-team
|
||||||
|
* @version 1.0
|
||||||
|
* @since 2024-01-01
|
||||||
|
*
|
||||||
|
* @see AuthService
|
||||||
|
* @see SecurityException
|
||||||
|
*/
|
||||||
|
public class InvalidCredentialsException extends RuntimeException {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 인터페이스 주석 표준
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 사용자 인증 서비스 인터페이스
|
||||||
|
*
|
||||||
|
* <p>사용자 로그인, 로그아웃, 토큰 관리 등 인증 관련 기능을 정의합니다.</p>
|
||||||
|
*
|
||||||
|
* <p><b>구현 클래스:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link AuthServiceImpl} - 기본 구현체</li>
|
||||||
|
* <li>{@link LdapAuthService} - LDAP 연동 구현체</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><b>주요 기능:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>사용자 인증 및 토큰 생성</li>
|
||||||
|
* <li>로그아웃 및 토큰 무효화</li>
|
||||||
|
* <li>토큰 유효성 검증</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author cms-team
|
||||||
|
* @version 1.0
|
||||||
|
* @since 2024-01-01
|
||||||
|
*
|
||||||
|
* @see AuthServiceImpl
|
||||||
|
* @see TokenProvider
|
||||||
|
*/
|
||||||
|
public interface AuthService {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Enum 주석 표준
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 사용자 역할 열거형
|
||||||
|
*
|
||||||
|
* <p>시스템 사용자의 권한 수준을 정의합니다.</p>
|
||||||
|
*
|
||||||
|
* <p><b>권한 계층:</b></p>
|
||||||
|
* <ol>
|
||||||
|
* <li>{@link #ADMIN} - 최고 관리자 권한</li>
|
||||||
|
* <li>{@link #MANAGER} - 관리자 권한</li>
|
||||||
|
* <li>{@link #USER} - 일반 사용자 권한</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @author cms-team
|
||||||
|
* @version 1.0
|
||||||
|
* @since 2024-01-01
|
||||||
|
*/
|
||||||
|
public enum Role {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 시스템 관리자
|
||||||
|
*
|
||||||
|
* <p>모든 시스템 기능에 대한 접근 권한을 가집니다.</p>
|
||||||
|
*
|
||||||
|
* <p><b>주요 권한:</b></p>
|
||||||
|
* <ul>
|
||||||
|
* <li>사용자 관리</li>
|
||||||
|
* <li>시스템 설정</li>
|
||||||
|
* <li>모든 데이터 접근</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
ADMIN,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관리자
|
||||||
|
*
|
||||||
|
* <p>제한된 관리 기능에 대한 접근 권한을 가집니다.</p>
|
||||||
|
*/
|
||||||
|
MANAGER,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일반 사용자
|
||||||
|
*
|
||||||
|
* <p>기본적인 시스템 기능에 대한 접근 권한을 가집니다.</p>
|
||||||
|
*/
|
||||||
|
USER
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 주석 작성 체크리스트
|
||||||
|
|
||||||
|
### ✅ **클래스 주석 체크리스트**
|
||||||
|
- [ ] 클래스의 목적과 역할 명시
|
||||||
|
- [ ] 주요 기능 목록 작성
|
||||||
|
- [ ] 사용 예시 코드 포함
|
||||||
|
- [ ] 주의사항 및 제약사항 명시
|
||||||
|
- [ ] @author, @version, @since 태그 작성
|
||||||
|
- [ ] 관련 클래스 @see 태그 추가
|
||||||
|
|
||||||
|
### ✅ **메서드 주석 체크리스트**
|
||||||
|
- [ ] 메서드의 목적과 동작 설명
|
||||||
|
- [ ] 처리 과정 단계별 설명
|
||||||
|
- [ ] 모든 @param 태그 작성
|
||||||
|
- [ ] @return 태그 작성 (void 메서드 제외)
|
||||||
|
- [ ] 가능한 예외 @throws 태그 작성
|
||||||
|
- [ ] 보안 관련 주의사항 명시
|
||||||
|
- [ ] 관련 메서드 @see 태그 추가
|
||||||
|
|
||||||
|
### ✅ **HTML 태그 체크리스트**
|
||||||
|
- [ ] 목록은 `<ul>`, `<ol>`, `<li>` 태그 사용
|
||||||
|
- [ ] 강조는 `<b>` 태그 사용
|
||||||
|
- [ ] 단락 구분은 `<p>` 태그 사용
|
||||||
|
- [ ] 코드는 `<code>` 또는 `<pre>` 태그 사용
|
||||||
|
- [ ] 제네릭 타입은 `<`, `>` 사용
|
||||||
|
|
||||||
|
## 📋 도구 및 설정
|
||||||
|
|
||||||
|
### 1. **JavaDoc 생성**
|
||||||
|
```bash
|
||||||
|
# Gradle 프로젝트
|
||||||
|
./gradlew javadoc
|
||||||
|
|
||||||
|
# Maven 프로젝트
|
||||||
|
mvn javadoc:javadoc
|
||||||
|
|
||||||
|
# 직접 실행
|
||||||
|
javadoc -d docs -cp classpath src/**/*.java
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **IDE 설정**
|
||||||
|
- **IntelliJ IDEA**: Settings > Editor > Code Style > Java > JavaDoc
|
||||||
|
- **Eclipse**: Window > Preferences > Java > Code Style > Code Templates
|
||||||
|
- **VS Code**: Java Extension Pack + JavaDoc 플러그인
|
||||||
|
|
||||||
|
### 3. **정적 분석 도구**
|
||||||
|
- **Checkstyle**: JavaDoc 누락 검사
|
||||||
|
- **SpotBugs**: 주석 품질 검사
|
||||||
|
- **SonarQube**: 문서화 품질 메트릭
|
||||||
|
|
||||||
|
## 📋 참고 자료
|
||||||
|
|
||||||
|
- [Oracle JavaDoc 가이드](https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html)
|
||||||
|
- [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html)
|
||||||
|
- [Spring Framework 주석 스타일](https://github.com/spring-projects/spring-framework/wiki/Code-Style)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **💡 팁**: 이 가이드를 팀 내에서 공유하고, 코드 리뷰 시 주석 품질도 함께 검토하세요!
|
||||||
173
claude/standard_package_structure.md
Normal file
173
claude/standard_package_structure.md
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
패키지 구조 표준
|
||||||
|
|
||||||
|
레이어드 아키텍처 패키지 구조
|
||||||
|
|
||||||
|
├── {SERVICE}
|
||||||
|
│ ├── domain
|
||||||
|
│ ├── service
|
||||||
|
│ ├── controller
|
||||||
|
│ ├── dto
|
||||||
|
│ ├── repository
|
||||||
|
│ │ ├── jpa
|
||||||
|
│ │ └── entity
|
||||||
|
│ ├── config
|
||||||
|
└── common
|
||||||
|
├── dto
|
||||||
|
├── util
|
||||||
|
├── response
|
||||||
|
└── exception
|
||||||
|
|
||||||
|
Package명:
|
||||||
|
- com.{ORG}.{ROOT}.{SERVICE}
|
||||||
|
예) com.unicorn.lifesub.mysub, com.unicorn.lifesub.common
|
||||||
|
|
||||||
|
변수:
|
||||||
|
- ORG: 회사 또는 조직명
|
||||||
|
- ROOT: Root Project 명
|
||||||
|
- SERVICE: 서비스명으로 Root Project의 서브 프로젝트임
|
||||||
|
|
||||||
|
|
||||||
|
예시
|
||||||
|
|
||||||
|
com.unicorn.lifesub.member
|
||||||
|
├── MemberApplication.java
|
||||||
|
├── controller
|
||||||
|
│ └── MemberController.java
|
||||||
|
├── dto
|
||||||
|
│ ├── LoginRequest.java
|
||||||
|
│ ├── LogoutRequest.java
|
||||||
|
│ └── LogoutResponse.java
|
||||||
|
├── service
|
||||||
|
│ ├── MemberService.java
|
||||||
|
│ └── MemberServiceImpl.java
|
||||||
|
├── domain
|
||||||
|
│ └── Member.java
|
||||||
|
├── repository
|
||||||
|
│ ├── entity
|
||||||
|
│ │ └── MemberEntity.java
|
||||||
|
│ └── jpa
|
||||||
|
│ └── MemberRepository.java
|
||||||
|
└── config
|
||||||
|
├── SecurityConfig.java
|
||||||
|
├── DataLoader.java
|
||||||
|
├── SwaggerConfig.java
|
||||||
|
└── jwt
|
||||||
|
├── JwtAuthenticationFilter.java
|
||||||
|
├── JwtTokenProvider.java
|
||||||
|
└── CustomUserDetailsService.java
|
||||||
|
|
||||||
|
|
||||||
|
클린 아키텍처 패키지 구조
|
||||||
|
|
||||||
|
├── biz
|
||||||
|
│ ├── usecase
|
||||||
|
│ │ ├── in
|
||||||
|
│ │ ├── out
|
||||||
|
│ ├── service
|
||||||
|
│ └── domain
|
||||||
|
│ └── dto
|
||||||
|
├── infra
|
||||||
|
│ ├── controller
|
||||||
|
│ ├── dto
|
||||||
|
│ ├── gateway
|
||||||
|
│ │ ├── repository
|
||||||
|
│ │ └── entity
|
||||||
|
│ └── config
|
||||||
|
|
||||||
|
|
||||||
|
Package명:
|
||||||
|
- com.{ORG}.{ROOT}.{SERVICE}.biz
|
||||||
|
- com.{ORG}.{ROOT}.{SERVICE}.infra
|
||||||
|
예) com.unicorn.lifesub.mysub.biz, com.unicorn.lifesub.common
|
||||||
|
|
||||||
|
변수:
|
||||||
|
- ORG: 회사 또는 조직명
|
||||||
|
- ROOT: Root Project 명
|
||||||
|
- SERVICE: 서비스명으로 Root Project의 서브 프로젝트임
|
||||||
|
|
||||||
|
예시
|
||||||
|
|
||||||
|
|
||||||
|
com.unicorn.lifesub.mysub
|
||||||
|
├── biz
|
||||||
|
│ ├── dto
|
||||||
|
│ │ ├── CategoryResponse.java
|
||||||
|
│ │ ├── ServiceListResponse.java
|
||||||
|
│ │ ├── MySubResponse.java
|
||||||
|
│ │ ├── SubDetailResponse.java
|
||||||
|
│ │ └── TotalFeeResponse.java
|
||||||
|
│ ├── service
|
||||||
|
│ │ ├── FeeLevel.java
|
||||||
|
│ │ └── MySubscriptionService.java
|
||||||
|
│ ├── usecase
|
||||||
|
│ │ ├── in
|
||||||
|
│ │ │ ├── CancelSubscriptionUseCase.java
|
||||||
|
│ │ │ ├── CategoryUseCase.java
|
||||||
|
│ │ │ ├── MySubscriptionsUseCase.java
|
||||||
|
│ │ │ ├── SubscribeUseCase.java
|
||||||
|
│ │ │ ├── SubscriptionDetailUseCase.java
|
||||||
|
│ │ │ └── TotalFeeUseCase.java
|
||||||
|
│ │ └── out
|
||||||
|
│ │ ├── MySubscriptionReader.java
|
||||||
|
│ │ ├── MySubscriptionWriter.java
|
||||||
|
│ │ └── SubscriptionReader.java
|
||||||
|
│ └── domain
|
||||||
|
│ ├── Category.java
|
||||||
|
│ ├── MySubscription.java
|
||||||
|
│ └── Subscription.java
|
||||||
|
└── infra
|
||||||
|
├── MySubApplication.java
|
||||||
|
├── controller
|
||||||
|
│ ├── CategoryController.java
|
||||||
|
│ ├── MySubController.java
|
||||||
|
│ └── ServiceController.java
|
||||||
|
├── config
|
||||||
|
│ ├── DataLoader.java
|
||||||
|
│ ├── SecurityConfig.java
|
||||||
|
│ ├── SwaggerConfig.java
|
||||||
|
│ └── jwt
|
||||||
|
│ ├── JwtAuthenticationFilter.java
|
||||||
|
│ └── JwtTokenProvider.java
|
||||||
|
└── gateway
|
||||||
|
├── entity
|
||||||
|
│ ├── CategoryEntity.java
|
||||||
|
│ ├── MySubscriptionEntity.java
|
||||||
|
│ └── SubscriptionEntity.java
|
||||||
|
├── repository
|
||||||
|
│ ├── CategoryJpaRepository.java
|
||||||
|
│ ├── MySubscriptionJpaRepository.java
|
||||||
|
│ └── SubscriptionJpaRepository.java
|
||||||
|
├── MySubscriptionGateway.java
|
||||||
|
└── SubscriptionGateway.java
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
common 모듈 패키지 구조
|
||||||
|
|
||||||
|
├── common
|
||||||
|
├── dto
|
||||||
|
├── entity
|
||||||
|
├── config
|
||||||
|
├── util
|
||||||
|
└── exception
|
||||||
|
|
||||||
|
|
||||||
|
com.unicorn.lifesub.common
|
||||||
|
├── dto
|
||||||
|
│ ├── ApiResponse.java
|
||||||
|
│ ├── JwtTokenDTO.java
|
||||||
|
│ ├── JwtTokenRefreshDTO.java
|
||||||
|
│ └── JwtTokenVerifyDTO.java
|
||||||
|
├── config
|
||||||
|
│ └── JpaConfig.java
|
||||||
|
├── entity
|
||||||
|
│ └── BaseTimeEntity.java
|
||||||
|
├── aop
|
||||||
|
│ └── LoggingAspect.java
|
||||||
|
└── exception
|
||||||
|
├── ErrorCode.java
|
||||||
|
├── InfraException.java
|
||||||
|
└── BusinessException.java
|
||||||
|
|
||||||
|
|
||||||
214
claude/standard_testcode.md
Normal file
214
claude/standard_testcode.md
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
1.TDD 기본 이해
|
||||||
|
|
||||||
|
1) TDD 목적
|
||||||
|
코드 품질 향상으로 유지보수 비용 절감
|
||||||
|
- 설계 품질 향상: 테스트를 먼저 작성하면서 코드 구조와 인터페이스를 먼저 고민
|
||||||
|
- 회귀 버그 방지: 테스트 자동화로 코드 변경 시 기존 기능의 오작동을 빠르게 감지
|
||||||
|
- 리팩토링 검증: 코드 개선 후 테스트 코드로 검증할 수 있어 리팩토링에 대한 자신감 확보
|
||||||
|
- 살아있는 문서: 테스트 코드에 샘플 데이터를 이용한 예시가 있으므로 실제 코드의 동작 방식을 문서화
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
2) 테스트 유형
|
||||||
|
- 단위 테스트(Unit Test): 외부 기술요소(DB, 웹서버 등)와의 인터페이스 없이 단위 클래스의 퍼블릭 메소드 테스트
|
||||||
|
- 통합 테스트(Integration Test): 일부 아키텍처 영역에서 외부 기술 요소와 인터페이스까지 테스트
|
||||||
|
- E2E 테스트(E2E Test): 모든 아키텍처 영역에서 외부 기술 요소와 인터페이스를 테스트
|
||||||
|
|
||||||
|
* 아키텍처 영역: 클래스를 아키텍처적으로 나눈 레이어를 의미함(예: controller, service, domain, repository)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
3) 테스트 피라미드
|
||||||
|
|
||||||
|
- 단위 테스트 70%, 통합 테스트 20%, E2E 테스트 10%의 비율로 권장
|
||||||
|
- Mike Cohn이 "Succeeding with Agile"에서 처음 제시한 개념
|
||||||
|
- 단위 테스트에서 E2E 테스트로 가면서 속도는 느려지고 비용은 높아짐
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
4) Red-Green-Refactor 사이클
|
||||||
|
|
||||||
|
Red-Green-Refactor는 TDD(Test-Driven Development)를 수행하는 핵심 사이클임
|
||||||
|
- Red (실패하는 테스트 작성)
|
||||||
|
- 새로운 기능에 대한 테스트 코드를 먼저 작성
|
||||||
|
- 아직 구현이 없으므로 테스트는 실패
|
||||||
|
- 이 단계에서 기능의 인터페이스를 설계
|
||||||
|
- Green (테스트 통과하는 코드 작성)
|
||||||
|
- 테스트를 통과하는 최소한의 코드 작성
|
||||||
|
- 품질보다는 동작에 초점
|
||||||
|
- Refactor (리팩토링)
|
||||||
|
- 중복 제거, 가독성 개선
|
||||||
|
- 테스트는 계속 통과하도록 유지
|
||||||
|
- 코드 품질 개선
|
||||||
|
|
||||||
|
---
|
||||||
|
2. 테스트 전략
|
||||||
|
|
||||||
|
1) 테스트 수행 원칙: FIRST 원칙
|
||||||
|
- Fast: 테스트는 빠르게 실행되어야 함
|
||||||
|
- Isolated: 각 테스트는 독립적이어야 함
|
||||||
|
- Repeatable: 어떤 환경에서도 동일한 결과가 나와야 함
|
||||||
|
- Self-validating: 테스트는 성공/실패가 명확해야 함
|
||||||
|
- Timely: 테스트는 실제 코드 작성 전/직후에 작성되어야 함
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
2) 공통 전략: 테스트 코드 작성 관련
|
||||||
|
- 한 테스트는 한 가지만 테스트
|
||||||
|
- Given-When-Then 패턴 사용
|
||||||
|
- Given(준비): 테스트에 필요한 상태와 데이터를 설정
|
||||||
|
- When(실행): 테스트하려는 동작을 수행
|
||||||
|
- Then(검증): 기대하는 결과가 나왔는지 확인
|
||||||
|
- 깨끗한 테스트 코드 작성
|
||||||
|
- 테스트 의도를 명확히 하는 네이밍
|
||||||
|
- 테스트 케이스는 시나리오 중심으로 구성
|
||||||
|
- 공통 설정은 별도 메서드로 분리
|
||||||
|
- 매직넘버 대신 상수 사용
|
||||||
|
- 테스트 데이터는 최소한으로 사용
|
||||||
|
- 경계값 테스트가 중요
|
||||||
|
- null 값
|
||||||
|
- 빈 컬렉션
|
||||||
|
- 최대/최소값
|
||||||
|
- 0이나 1과 같은 특수값
|
||||||
|
- 잘못된 포맷의 입력값
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
2) 공통 전략: 테스트 코드 관리 관련
|
||||||
|
- 비용 효율적인 테스트 전략
|
||||||
|
- 자주 변경되는 비즈니스 로직에 대한 테스트 강화
|
||||||
|
- 실제 운영 환경과 유사한 통합 테스트 구성
|
||||||
|
- 테스트 실행 시간과 리소스 사용량 모니터링
|
||||||
|
- 지속적인 테스트 개선
|
||||||
|
- 테스트 커버리지보다 테스트 품질 중시
|
||||||
|
- 깨진 테스트는 즉시 수정하는 문화 정착
|
||||||
|
- 테스트 코드도 실제 코드만큼 중요하게 관리
|
||||||
|
- 팀 협업을 위한 가이드라인 수립
|
||||||
|
- 테스트 네이밍 컨벤션 수립
|
||||||
|
- 테스트 데이터 관리 전략 합의
|
||||||
|
- 테스트 실패 시 대응 프로세스 수립
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
3) 단위 테스트 전략
|
||||||
|
- 테스트 범위 명확화
|
||||||
|
- 클래스의 각 public 메소드가 수행하는 단일 책임을 검증
|
||||||
|
- private 메서드는 public 메서드를 통해 간접적으로 테스트
|
||||||
|
- 외부 의존성 처리
|
||||||
|
- DB, 파일, 네트워크 등 외부 시스템은 가짜 객체로 대체(Mocking)
|
||||||
|
- 테스트 더블(스턴트맨을 Stunt Double이라고 함. 대역으로 이해)은 꼭 필요한 동작만 구현
|
||||||
|
- Mock: 메소드 호출 여부와 파라미터 검증
|
||||||
|
- Stub: 반환값의 일치 여부 검증
|
||||||
|
- Spy: Mocking하지 않고 실제 메소드를 감싸서 호출횟수, 호출순서등 추가 정보 검증
|
||||||
|
- 격리성 확보
|
||||||
|
- 테스트 간 상호 영향 없도록 설계: 동일 공유 자원/객체를 사용하지 않게 함
|
||||||
|
- 테스트 실행 순서와 무관하게 동작
|
||||||
|
- 가독성과 유지보수성
|
||||||
|
- 테스트 대상 클래스당 하나의 테스트 클래스
|
||||||
|
- 테스트 메서드는 한 가지 시나리오만 검증
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
4) 단위 테스트 시 Mocking 전략
|
||||||
|
- 외부 시스템(DB, 외부 API 등)은 반드시 Mocking
|
||||||
|
- 같은 레이어의 의존성 있는 클래스는 실제 객체 사용
|
||||||
|
- 예외적으로 의존 객체가 매우 복잡하거나 무거운 경우 Mocking 고려
|
||||||
|
|
||||||
|
* 참고: 모의 객체 테스트 균형점 찾기
|
||||||
|
출처: When to mocking by Uncle Bob(https://blog.cleancoder.com/uncle-bob/2014/05/10/WhenToMock.html)
|
||||||
|
- 모의 객체를 이용 안 하면: 테스트가 오래 걸리고 결과를 신뢰하기 어려우며 인프라에 너무 많은 영향을 받음
|
||||||
|
- 모의 객체를 지나치게 사용하면: 복잡하고 수정에 영향을 너무 많이 받으며 모의 인터페이스가 폭발적으로 증가
|
||||||
|
- 균형점 찾기
|
||||||
|
- 아키텍처적으로 중요한 경계에서만 모의 테스트를 수행하고, 그 경계 안에서는 하지 않는다.
|
||||||
|
(Mock across architecturally significant boundaries, but not within those boundaries.)
|
||||||
|
- 여기서 경계란 Controller, Service, Repository, Domain등의 레이어를 의미함
|
||||||
|
|
||||||
|
---
|
||||||
|
5) 통합 테스트 전략
|
||||||
|
- 웹 서버 인터페이스
|
||||||
|
- @WebMvcTest, @WebFluxTest 활용
|
||||||
|
- Controller 계층의 요청/응답 검증
|
||||||
|
- Service 계층은 Mocking 처리
|
||||||
|
|
||||||
|
- Database 인터페이스
|
||||||
|
- @DataJpaTest 활용
|
||||||
|
- TestContainer로 실제 DB 엔진 실행
|
||||||
|
|
||||||
|
- 외부 서비스 인터페이스
|
||||||
|
- WireMock 등을 활용한 Mocking
|
||||||
|
- 실제 API 스펙 기반 테스트
|
||||||
|
|
||||||
|
- 테스트 환경 구성
|
||||||
|
- 테스트용 별도 설정 파일 구성
|
||||||
|
- 테스트 데이터는 테스트 시작 시 초기화
|
||||||
|
- @Transactional을 활용한 테스트 격리
|
||||||
|
- 테스트 간 독립성 보장
|
||||||
|
|
||||||
|
---
|
||||||
|
6) E2E 테스트 전략
|
||||||
|
- 원칙
|
||||||
|
- 단위 테스트나 컴포넌트 테스트에서 놓칠 수 있는 시나리오를 찾아내는 것이 목표임
|
||||||
|
- 조건별 로직이나 분기 상황(edge cases)이 아닌 상위 수준의 일반적인 시나리오만 테스트
|
||||||
|
- 만약 어떤 시스템 테스트 시나리오가 실패 했는데 단위 테스트나 통합 테스트가 없다면 만들어야 함
|
||||||
|
|
||||||
|
- 운영과 동일한 테스트 환경 구성: 웹서버/WAS, DB, 캐시, MQ, 외부시스템
|
||||||
|
- 테스트 데이터 관리
|
||||||
|
- 테스트용 마스터 데이터 구성
|
||||||
|
- 시나리오별 테스트 데이터 세트 준비
|
||||||
|
- 데이터 초기화 및 정리 자동화
|
||||||
|
- 테스트 자동화 전략
|
||||||
|
- UI 테스트: Selenium, Cucumber, Playwright 등 도구 활용
|
||||||
|
- API 테스트: Rest-Assured, Postman 등 도구 활용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
7) 테스트 코드 네이밍 컨벤션
|
||||||
|
|
||||||
|
- 패키지 네이밍
|
||||||
|
```
|
||||||
|
[Syntax]
|
||||||
|
{프로덕션패키지}.test.{테스트유형}
|
||||||
|
|
||||||
|
[Example]
|
||||||
|
- 단위테스트: com.company.order.test.unit
|
||||||
|
- 통합테스트: com.company.order.test.integration
|
||||||
|
- E2E테스트: com.company.order.test.e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
- 클래스 네이밍
|
||||||
|
```
|
||||||
|
[Syntax]
|
||||||
|
{대상클래스}{테스트유형}Test
|
||||||
|
|
||||||
|
[Example]
|
||||||
|
- 단위테스트: OrderServiceUnitTest
|
||||||
|
- 통합테스트: OrderServiceIntegrationTest
|
||||||
|
- E2E테스트: OrderServiceE2ETest
|
||||||
|
```
|
||||||
|
|
||||||
|
- 메소드 네이밍
|
||||||
|
```
|
||||||
|
[Syntax]
|
||||||
|
given{초기상태}_when{행위}_then{결과}
|
||||||
|
|
||||||
|
[Example]
|
||||||
|
givenEmptyCart_whenAddItem_thenSuccess()
|
||||||
|
givenInvalidToken_whenAuthenticate_thenThrowException()
|
||||||
|
givenExistingUser_whenUpdateProfile_thenProfileUpdated()
|
||||||
|
```
|
||||||
|
|
||||||
|
- 테스트 데이터 네이밍
|
||||||
|
```
|
||||||
|
[Syntax]
|
||||||
|
상수: {상태}_{대상}
|
||||||
|
변수: {상태}{대상}
|
||||||
|
|
||||||
|
[Example]
|
||||||
|
// 상수
|
||||||
|
VALID_USER_ID = 1L
|
||||||
|
EMPTY_ORDER_LIST = Collections.emptyList()
|
||||||
|
|
||||||
|
// 변수
|
||||||
|
normalUser = new User(...)
|
||||||
|
emptyCart = new Cart()
|
||||||
|
```
|
||||||
35
common/build.gradle
Normal file
35
common/build.gradle
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java-library'
|
||||||
|
id 'org.springframework.boot'
|
||||||
|
id 'io.spring.dependency-management'
|
||||||
|
}
|
||||||
|
|
||||||
|
// common 모듈은 실행 가능한 jar가 아니므로 bootJar 비활성화
|
||||||
|
bootJar {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
jar {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Spring Boot Starters
|
||||||
|
api 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
api 'org.springframework.boot:spring-boot-starter-security'
|
||||||
|
api 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||||
|
api 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
|
|
||||||
|
// JWT
|
||||||
|
api "io.jsonwebtoken:jjwt-api:${jjwtVersion}"
|
||||||
|
runtimeOnly "io.jsonwebtoken:jjwt-impl:${jjwtVersion}"
|
||||||
|
runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jjwtVersion}"
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
api "org.apache.commons:commons-lang3:${commonsLang3Version}"
|
||||||
|
api "commons-io:commons-io:${commonsIoVersion}"
|
||||||
|
|
||||||
|
// Jackson for JSON
|
||||||
|
api 'com.fasterxml.jackson.core:jackson-databind'
|
||||||
|
api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
|
||||||
|
}
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
package com.kt.event.common.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 공통 API 응답 래퍼
|
||||||
|
* 모든 API 응답을 감싸는 표준 응답 포맷
|
||||||
|
*
|
||||||
|
* @param <T> 응답 데이터 타입
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class ApiResponse<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 성공 여부
|
||||||
|
*/
|
||||||
|
private final boolean success;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 응답 데이터
|
||||||
|
*/
|
||||||
|
private final T data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 코드 (실패 시)
|
||||||
|
*/
|
||||||
|
private final String errorCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 메시지 (실패 시)
|
||||||
|
*/
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 응답 시간
|
||||||
|
*/
|
||||||
|
private final LocalDateTime timestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 성공 응답 생성 (데이터 포함)
|
||||||
|
*
|
||||||
|
* @param data 응답 데이터
|
||||||
|
* @param <T> 응답 데이터 타입
|
||||||
|
* @return API 응답
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> success(T data) {
|
||||||
|
return new ApiResponse<>(true, data, null, null, LocalDateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 성공 응답 생성 (데이터 없음)
|
||||||
|
*
|
||||||
|
* @param <T> 응답 데이터 타입
|
||||||
|
* @return API 응답
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> success() {
|
||||||
|
return new ApiResponse<>(true, null, null, null, LocalDateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실패 응답 생성
|
||||||
|
*
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @param message 에러 메시지
|
||||||
|
* @param <T> 응답 데이터 타입
|
||||||
|
* @return API 응답
|
||||||
|
*/
|
||||||
|
public static <T> ApiResponse<T> error(String errorCode, String message) {
|
||||||
|
return new ApiResponse<>(false, null, errorCode, message, LocalDateTime.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
122
common/src/main/java/com/kt/event/common/dto/ErrorResponse.java
Normal file
122
common/src/main/java/com/kt/event/common/dto/ErrorResponse.java
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package com.kt.event.common.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 응답
|
||||||
|
* API 에러 발생 시 반환되는 상세 에러 정보
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class ErrorResponse {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 성공 여부 (항상 false)
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private final boolean success = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 코드
|
||||||
|
*/
|
||||||
|
private final String errorCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 메시지
|
||||||
|
*/
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 상세 에러 정보 (선택)
|
||||||
|
*/
|
||||||
|
private final String details;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 유효성 검증 에러 목록 (선택)
|
||||||
|
*/
|
||||||
|
private final List<FieldError> fieldErrors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 발생 시간
|
||||||
|
*/
|
||||||
|
@Builder.Default
|
||||||
|
private final LocalDateTime timestamp = LocalDateTime.now();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 필드 유효성 검증 에러
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class FieldError {
|
||||||
|
/**
|
||||||
|
* 필드명
|
||||||
|
*/
|
||||||
|
private final String field;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 입력된 값
|
||||||
|
*/
|
||||||
|
private final Object rejectedValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 메시지
|
||||||
|
*/
|
||||||
|
private final String message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기본 에러 응답 생성
|
||||||
|
*
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @param message 에러 메시지
|
||||||
|
* @return ErrorResponse
|
||||||
|
*/
|
||||||
|
public static ErrorResponse of(String errorCode, String message) {
|
||||||
|
return ErrorResponse.builder()
|
||||||
|
.errorCode(errorCode)
|
||||||
|
.message(message)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 상세 정보가 포함된 에러 응답 생성
|
||||||
|
*
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @param message 에러 메시지
|
||||||
|
* @param details 상세 에러 정보
|
||||||
|
* @return ErrorResponse
|
||||||
|
*/
|
||||||
|
public static ErrorResponse of(String errorCode, String message, String details) {
|
||||||
|
return ErrorResponse.builder()
|
||||||
|
.errorCode(errorCode)
|
||||||
|
.message(message)
|
||||||
|
.details(details)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 필드 유효성 검증 에러 응답 생성
|
||||||
|
*
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @param message 에러 메시지
|
||||||
|
* @param fieldErrors 필드 에러 목록
|
||||||
|
* @return ErrorResponse
|
||||||
|
*/
|
||||||
|
public static ErrorResponse of(String errorCode, String message, List<FieldError> fieldErrors) {
|
||||||
|
return ErrorResponse.builder()
|
||||||
|
.errorCode(errorCode)
|
||||||
|
.message(message)
|
||||||
|
.fieldErrors(fieldErrors)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
package com.kt.event.common.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 페이지네이션 응답
|
||||||
|
* 목록 조회 시 페이징 정보를 포함하는 응답
|
||||||
|
*
|
||||||
|
* @param <T> 목록 아이템 타입
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PageResponse<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 목록 데이터
|
||||||
|
*/
|
||||||
|
private List<T> content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 페이지 번호 (0부터 시작)
|
||||||
|
*/
|
||||||
|
private int page;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 페이지 크기
|
||||||
|
*/
|
||||||
|
private int size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 요소 수
|
||||||
|
*/
|
||||||
|
private long totalElements;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 페이지 수
|
||||||
|
*/
|
||||||
|
private int totalPages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 첫 페이지 여부
|
||||||
|
*/
|
||||||
|
private boolean first;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 마지막 페이지 여부
|
||||||
|
*/
|
||||||
|
private boolean last;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Data Page를 PageResponse로 변환
|
||||||
|
*
|
||||||
|
* @param page Spring Data Page 객체
|
||||||
|
* @param <T> 목록 아이템 타입
|
||||||
|
* @return PageResponse
|
||||||
|
*/
|
||||||
|
public static <T> PageResponse<T> of(org.springframework.data.domain.Page<T> page) {
|
||||||
|
return PageResponse.<T>builder()
|
||||||
|
.content(page.getContent())
|
||||||
|
.page(page.getNumber())
|
||||||
|
.size(page.getSize())
|
||||||
|
.totalElements(page.getTotalElements())
|
||||||
|
.totalPages(page.getTotalPages())
|
||||||
|
.first(page.isFirst())
|
||||||
|
.last(page.isLast())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package com.kt.event.common.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.EntityListeners;
|
||||||
|
import jakarta.persistence.MappedSuperclass;
|
||||||
|
import lombok.Getter;
|
||||||
|
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
|
||||||
|
@MappedSuperclass
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
public abstract class BaseTimeEntity {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성일시
|
||||||
|
*/
|
||||||
|
@CreatedDate
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수정일시
|
||||||
|
*/
|
||||||
|
@LastModifiedDate
|
||||||
|
@Column(name = "updated_at", nullable = false)
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
package com.kt.event.common.exception;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비즈니스 예외
|
||||||
|
* 비즈니스 로직 처리 중 발생하는 예외
|
||||||
|
* (예: 중복 데이터, 권한 없음, 유효하지 않은 상태 전환 등)
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public class BusinessException extends RuntimeException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 코드
|
||||||
|
*/
|
||||||
|
private final ErrorCode errorCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 상세 에러 정보
|
||||||
|
*/
|
||||||
|
private final String details;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비즈니스 예외 생성 (기본 메시지 사용)
|
||||||
|
*
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
*/
|
||||||
|
public BusinessException(ErrorCode errorCode) {
|
||||||
|
super(errorCode.getMessage());
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.details = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비즈니스 예외 생성 (커스텀 메시지 사용)
|
||||||
|
*
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @param message 커스텀 에러 메시지
|
||||||
|
*/
|
||||||
|
public BusinessException(ErrorCode errorCode, String message) {
|
||||||
|
super(message);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.details = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비즈니스 예외 생성 (상세 정보 포함)
|
||||||
|
*
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @param message 커스텀 에러 메시지
|
||||||
|
* @param details 상세 에러 정보
|
||||||
|
*/
|
||||||
|
public BusinessException(ErrorCode errorCode, String message, String details) {
|
||||||
|
super(message);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비즈니스 예외 생성 (원인 예외 포함)
|
||||||
|
*
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @param cause 원인 예외
|
||||||
|
*/
|
||||||
|
public BusinessException(ErrorCode errorCode, Throwable cause) {
|
||||||
|
super(errorCode.getMessage(), cause);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.details = cause.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비즈니스 예외 생성 (모든 정보 포함)
|
||||||
|
*
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @param message 커스텀 에러 메시지
|
||||||
|
* @param details 상세 에러 정보
|
||||||
|
* @param cause 원인 예외
|
||||||
|
*/
|
||||||
|
public BusinessException(ErrorCode errorCode, String message, String details, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
package com.kt.event.common.exception;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 코드 정의
|
||||||
|
* 시스템 전체에서 사용하는 에러 코드와 메시지를 관리
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public enum ErrorCode {
|
||||||
|
|
||||||
|
// 공통 에러 (COMMON_XXX)
|
||||||
|
COMMON_001("COMMON_001", "잘못된 요청입니다"),
|
||||||
|
COMMON_002("COMMON_002", "필수 파라미터가 누락되었습니다"),
|
||||||
|
COMMON_003("COMMON_003", "유효성 검증에 실패했습니다"),
|
||||||
|
COMMON_004("COMMON_004", "서버 내부 오류가 발생했습니다"),
|
||||||
|
COMMON_005("COMMON_005", "지원하지 않는 작업입니다"),
|
||||||
|
|
||||||
|
// 인증/인가 에러 (AUTH_XXX)
|
||||||
|
AUTH_001("AUTH_001", "인증에 실패했습니다"),
|
||||||
|
AUTH_002("AUTH_002", "유효하지 않은 토큰입니다"),
|
||||||
|
AUTH_003("AUTH_003", "만료된 토큰입니다"),
|
||||||
|
AUTH_004("AUTH_004", "권한이 없습니다"),
|
||||||
|
AUTH_005("AUTH_005", "토큰이 제공되지 않았습니다"),
|
||||||
|
|
||||||
|
// 사용자 에러 (USER_XXX)
|
||||||
|
USER_001("USER_001", "이미 존재하는 사용자입니다"),
|
||||||
|
USER_002("USER_002", "사업자번호 검증에 실패했습니다"),
|
||||||
|
USER_003("USER_003", "사용자를 찾을 수 없습니다"),
|
||||||
|
USER_004("USER_004", "비밀번호가 일치하지 않습니다"),
|
||||||
|
USER_005("USER_005", "휴폐업 사업자번호입니다"),
|
||||||
|
|
||||||
|
// 이벤트 에러 (EVENT_XXX)
|
||||||
|
EVENT_001("EVENT_001", "이벤트를 찾을 수 없습니다"),
|
||||||
|
EVENT_002("EVENT_002", "유효하지 않은 상태 전환입니다"),
|
||||||
|
EVENT_003("EVENT_003", "필수 데이터가 누락되었습니다"),
|
||||||
|
EVENT_004("EVENT_004", "이벤트 생성에 실패했습니다"),
|
||||||
|
EVENT_005("EVENT_005", "이벤트 수정 권한이 없습니다"),
|
||||||
|
|
||||||
|
// Job 에러 (JOB_XXX)
|
||||||
|
JOB_001("JOB_001", "Job을 찾을 수 없습니다"),
|
||||||
|
JOB_002("JOB_002", "Job 처리에 실패했습니다"),
|
||||||
|
JOB_003("JOB_003", "Job이 아직 처리 중입니다"),
|
||||||
|
JOB_004("JOB_004", "Job 타임아웃이 발생했습니다"),
|
||||||
|
|
||||||
|
// AI 에러 (AI_XXX)
|
||||||
|
AI_001("AI_001", "AI 추천 생성에 실패했습니다"),
|
||||||
|
AI_002("AI_002", "트렌드 분석에 실패했습니다"),
|
||||||
|
AI_003("AI_003", "AI API 호출에 실패했습니다"),
|
||||||
|
AI_004("AI_004", "AI 추천 결과를 찾을 수 없습니다"),
|
||||||
|
|
||||||
|
// 콘텐츠 에러 (CONTENT_XXX)
|
||||||
|
CONTENT_001("CONTENT_001", "이미지 생성에 실패했습니다"),
|
||||||
|
CONTENT_002("CONTENT_002", "이미지를 찾을 수 없습니다"),
|
||||||
|
CONTENT_003("CONTENT_003", "CDN 업로드에 실패했습니다"),
|
||||||
|
CONTENT_004("CONTENT_004", "콘텐츠를 찾을 수 없습니다"),
|
||||||
|
|
||||||
|
// 배포 에러 (DIST_XXX)
|
||||||
|
DIST_001("DIST_001", "배포에 실패했습니다"),
|
||||||
|
DIST_002("DIST_002", "채널 연동에 실패했습니다"),
|
||||||
|
DIST_003("DIST_003", "서킷 브레이커가 열려있습니다"),
|
||||||
|
DIST_004("DIST_004", "배포 상태를 찾을 수 없습니다"),
|
||||||
|
|
||||||
|
// 참여 에러 (PART_XXX)
|
||||||
|
PART_001("PART_001", "이미 참여한 이벤트입니다"),
|
||||||
|
PART_002("PART_002", "이벤트 참여 기간이 아닙니다"),
|
||||||
|
PART_003("PART_003", "참여자를 찾을 수 없습니다"),
|
||||||
|
PART_004("PART_004", "당첨자 추첨에 실패했습니다"),
|
||||||
|
PART_005("PART_005", "이벤트가 종료되었습니다"),
|
||||||
|
|
||||||
|
// 분석 에러 (ANALYTICS_XXX)
|
||||||
|
ANALYTICS_001("ANALYTICS_001", "분석 데이터를 찾을 수 없습니다"),
|
||||||
|
ANALYTICS_002("ANALYTICS_002", "외부 API 호출에 실패했습니다"),
|
||||||
|
ANALYTICS_003("ANALYTICS_003", "통계 계산에 실패했습니다"),
|
||||||
|
|
||||||
|
// 외부 연동 에러 (EXTERNAL_XXX)
|
||||||
|
EXTERNAL_001("EXTERNAL_001", "외부 API 호출에 실패했습니다"),
|
||||||
|
EXTERNAL_002("EXTERNAL_002", "외부 API 타임아웃이 발생했습니다"),
|
||||||
|
EXTERNAL_003("EXTERNAL_003", "외부 API 응답 형식이 올바르지 않습니다"),
|
||||||
|
|
||||||
|
// 데이터베이스 에러 (DB_XXX)
|
||||||
|
DB_001("DB_001", "데이터베이스 연결에 실패했습니다"),
|
||||||
|
DB_002("DB_002", "데이터 저장에 실패했습니다"),
|
||||||
|
DB_003("DB_003", "데이터 조회에 실패했습니다"),
|
||||||
|
DB_004("DB_004", "데이터 삭제에 실패했습니다"),
|
||||||
|
|
||||||
|
// Redis 에러 (REDIS_XXX)
|
||||||
|
REDIS_001("REDIS_001", "Redis 연결에 실패했습니다"),
|
||||||
|
REDIS_002("REDIS_002", "캐시 저장에 실패했습니다"),
|
||||||
|
REDIS_003("REDIS_003", "캐시 조회에 실패했습니다"),
|
||||||
|
|
||||||
|
// Kafka 에러 (KAFKA_XXX)
|
||||||
|
KAFKA_001("KAFKA_001", "Kafka 메시지 발행에 실패했습니다"),
|
||||||
|
KAFKA_002("KAFKA_002", "Kafka 메시지 소비에 실패했습니다"),
|
||||||
|
KAFKA_003("KAFKA_003", "Kafka 연결에 실패했습니다");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 코드
|
||||||
|
*/
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 메시지
|
||||||
|
*/
|
||||||
|
private final String message;
|
||||||
|
}
|
||||||
@ -0,0 +1,198 @@
|
|||||||
|
package com.kt.event.common.exception;
|
||||||
|
|
||||||
|
import com.kt.event.common.dto.ErrorResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.validation.BindException;
|
||||||
|
import org.springframework.validation.FieldError;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전역 예외 핸들러
|
||||||
|
* 애플리케이션 전체에서 발생하는 예외를 일관된 형식으로 처리
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비즈니스 예외 처리
|
||||||
|
*
|
||||||
|
* @param ex 비즈니스 예외
|
||||||
|
* @return 에러 응답
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(BusinessException.class)
|
||||||
|
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) {
|
||||||
|
log.warn("Business exception occurred: {}", ex.getMessage());
|
||||||
|
|
||||||
|
ErrorResponse errorResponse = ErrorResponse.of(
|
||||||
|
ex.getErrorCode().getCode(),
|
||||||
|
ex.getMessage(),
|
||||||
|
ex.getDetails()
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인프라 예외 처리
|
||||||
|
*
|
||||||
|
* @param ex 인프라 예외
|
||||||
|
* @return 에러 응답
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(InfraException.class)
|
||||||
|
public ResponseEntity<ErrorResponse> handleInfraException(InfraException ex) {
|
||||||
|
log.error("Infrastructure exception occurred: {}", ex.getMessage(), ex);
|
||||||
|
|
||||||
|
ErrorResponse errorResponse = ErrorResponse.of(
|
||||||
|
ex.getErrorCode().getCode(),
|
||||||
|
ex.getMessage(),
|
||||||
|
ex.getDetails()
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증 예외 처리
|
||||||
|
*
|
||||||
|
* @param ex 인증 예외
|
||||||
|
* @return 에러 응답
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(AuthenticationException.class)
|
||||||
|
public ResponseEntity<ErrorResponse> handleAuthenticationException(AuthenticationException ex) {
|
||||||
|
log.warn("Authentication exception occurred: {}", ex.getMessage());
|
||||||
|
|
||||||
|
ErrorResponse errorResponse = ErrorResponse.of(
|
||||||
|
ErrorCode.AUTH_001.getCode(),
|
||||||
|
ErrorCode.AUTH_001.getMessage(),
|
||||||
|
ex.getMessage()
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.UNAUTHORIZED)
|
||||||
|
.body(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 권한 예외 처리
|
||||||
|
*
|
||||||
|
* @param ex 권한 예외
|
||||||
|
* @return 에러 응답
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(AccessDeniedException.class)
|
||||||
|
public ResponseEntity<ErrorResponse> handleAccessDeniedException(AccessDeniedException ex) {
|
||||||
|
log.warn("Access denied exception occurred: {}", ex.getMessage());
|
||||||
|
|
||||||
|
ErrorResponse errorResponse = ErrorResponse.of(
|
||||||
|
ErrorCode.AUTH_004.getCode(),
|
||||||
|
ErrorCode.AUTH_004.getMessage(),
|
||||||
|
ex.getMessage()
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.FORBIDDEN)
|
||||||
|
.body(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 유효성 검증 예외 처리 (RequestBody)
|
||||||
|
*
|
||||||
|
* @param ex 유효성 검증 예외
|
||||||
|
* @return 에러 응답
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
|
||||||
|
log.warn("Validation exception occurred: {}", ex.getMessage());
|
||||||
|
|
||||||
|
List<ErrorResponse.FieldError> fieldErrors = ex.getBindingResult()
|
||||||
|
.getFieldErrors()
|
||||||
|
.stream()
|
||||||
|
.map(this::mapToFieldError)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
ErrorResponse errorResponse = ErrorResponse.of(
|
||||||
|
ErrorCode.COMMON_003.getCode(),
|
||||||
|
ErrorCode.COMMON_003.getMessage(),
|
||||||
|
fieldErrors
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 유효성 검증 예외 처리 (ModelAttribute)
|
||||||
|
*
|
||||||
|
* @param ex 유효성 검증 예외
|
||||||
|
* @return 에러 응답
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(BindException.class)
|
||||||
|
public ResponseEntity<ErrorResponse> handleBindException(BindException ex) {
|
||||||
|
log.warn("Bind exception occurred: {}", ex.getMessage());
|
||||||
|
|
||||||
|
List<ErrorResponse.FieldError> fieldErrors = ex.getBindingResult()
|
||||||
|
.getFieldErrors()
|
||||||
|
.stream()
|
||||||
|
.map(this::mapToFieldError)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
ErrorResponse errorResponse = ErrorResponse.of(
|
||||||
|
ErrorCode.COMMON_003.getCode(),
|
||||||
|
ErrorCode.COMMON_003.getMessage(),
|
||||||
|
fieldErrors
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 일반 예외 처리
|
||||||
|
*
|
||||||
|
* @param ex 일반 예외
|
||||||
|
* @return 에러 응답
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<ErrorResponse> handleException(Exception ex) {
|
||||||
|
log.error("Unexpected exception occurred: {}", ex.getMessage(), ex);
|
||||||
|
|
||||||
|
ErrorResponse errorResponse = ErrorResponse.of(
|
||||||
|
ErrorCode.COMMON_004.getCode(),
|
||||||
|
ErrorCode.COMMON_004.getMessage(),
|
||||||
|
ex.getMessage()
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring FieldError를 ErrorResponse.FieldError로 변환
|
||||||
|
*
|
||||||
|
* @param fieldError Spring FieldError
|
||||||
|
* @return ErrorResponse.FieldError
|
||||||
|
*/
|
||||||
|
private ErrorResponse.FieldError mapToFieldError(FieldError fieldError) {
|
||||||
|
return ErrorResponse.FieldError.builder()
|
||||||
|
.field(fieldError.getField())
|
||||||
|
.rejectedValue(fieldError.getRejectedValue())
|
||||||
|
.message(fieldError.getDefaultMessage())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
package com.kt.event.common.exception;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인프라 예외
|
||||||
|
* 인프라 계층에서 발생하는 예외
|
||||||
|
* (예: DB 연결 실패, Redis 오류, Kafka 오류, 외부 API 호출 실패 등)
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public class InfraException extends RuntimeException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 에러 코드
|
||||||
|
*/
|
||||||
|
private final ErrorCode errorCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 상세 에러 정보
|
||||||
|
*/
|
||||||
|
private final String details;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인프라 예외 생성 (기본 메시지 사용)
|
||||||
|
*
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
*/
|
||||||
|
public InfraException(ErrorCode errorCode) {
|
||||||
|
super(errorCode.getMessage());
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.details = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인프라 예외 생성 (커스텀 메시지 사용)
|
||||||
|
*
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @param message 커스텀 에러 메시지
|
||||||
|
*/
|
||||||
|
public InfraException(ErrorCode errorCode, String message) {
|
||||||
|
super(message);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.details = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인프라 예외 생성 (상세 정보 포함)
|
||||||
|
*
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @param message 커스텀 에러 메시지
|
||||||
|
* @param details 상세 에러 정보
|
||||||
|
*/
|
||||||
|
public InfraException(ErrorCode errorCode, String message, String details) {
|
||||||
|
super(message);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인프라 예외 생성 (원인 예외 포함)
|
||||||
|
*
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @param cause 원인 예외
|
||||||
|
*/
|
||||||
|
public InfraException(ErrorCode errorCode, Throwable cause) {
|
||||||
|
super(errorCode.getMessage(), cause);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.details = cause.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인프라 예외 생성 (모든 정보 포함)
|
||||||
|
*
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @param message 커스텀 에러 메시지
|
||||||
|
* @param details 상세 에러 정보
|
||||||
|
* @param cause 원인 예외
|
||||||
|
*/
|
||||||
|
public InfraException(ErrorCode errorCode, String message, String details, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,130 @@
|
|||||||
|
package com.kt.event.common.security;
|
||||||
|
|
||||||
|
import com.kt.event.common.util.StringUtil;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 인증 필터
|
||||||
|
* 요청 헤더에서 JWT 토큰을 추출하고 인증 처리
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorization 헤더 이름
|
||||||
|
*/
|
||||||
|
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bearer 토큰 접두사
|
||||||
|
*/
|
||||||
|
private static final String BEARER_PREFIX = "Bearer ";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 토큰 제공자
|
||||||
|
*/
|
||||||
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 필터 실행
|
||||||
|
*
|
||||||
|
* @param request HTTP 요청
|
||||||
|
* @param response HTTP 응답
|
||||||
|
* @param filterChain 필터 체인
|
||||||
|
* @throws ServletException 서블릿 예외
|
||||||
|
* @throws IOException 입출력 예외
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
try {
|
||||||
|
// 요청에서 JWT 토큰 추출
|
||||||
|
String token = extractTokenFromRequest(request);
|
||||||
|
|
||||||
|
// 토큰이 존재하고 유효한 경우 인증 처리
|
||||||
|
if (StringUtil.isNotBlank(token) && jwtTokenProvider.validateToken(token)) {
|
||||||
|
// Access Token인지 확인
|
||||||
|
if (jwtTokenProvider.isAccessToken(token)) {
|
||||||
|
authenticateUser(token, request);
|
||||||
|
} else {
|
||||||
|
log.warn("Refresh token used for authentication: {}", request.getRequestURI());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Could not set user authentication in security context", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 요청에서 JWT 토큰 추출
|
||||||
|
*
|
||||||
|
* @param request HTTP 요청
|
||||||
|
* @return JWT 토큰 (없으면 null)
|
||||||
|
*/
|
||||||
|
private String extractTokenFromRequest(HttpServletRequest request) {
|
||||||
|
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
|
||||||
|
|
||||||
|
if (StringUtil.isNotBlank(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
|
||||||
|
return bearerToken.substring(BEARER_PREFIX.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 인증 처리
|
||||||
|
*
|
||||||
|
* @param token JWT 토큰
|
||||||
|
* @param request HTTP 요청
|
||||||
|
*/
|
||||||
|
private void authenticateUser(String token, HttpServletRequest request) {
|
||||||
|
// 토큰에서 사용자 정보 추출
|
||||||
|
UserPrincipal userPrincipal = jwtTokenProvider.getUserPrincipalFromToken(token);
|
||||||
|
|
||||||
|
// Spring Security 인증 객체 생성
|
||||||
|
UsernamePasswordAuthenticationToken authentication =
|
||||||
|
new UsernamePasswordAuthenticationToken(
|
||||||
|
userPrincipal,
|
||||||
|
null,
|
||||||
|
userPrincipal.getAuthorities()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 요청 상세 정보 설정
|
||||||
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
|
||||||
|
// SecurityContext에 인증 정보 저장
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
|
||||||
|
log.debug("Set authentication for user: {} (userId: {})",
|
||||||
|
userPrincipal.getEmail(), userPrincipal.getUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 필터 적용 여부 결정
|
||||||
|
* OPTIONS 요청은 필터를 적용하지 않음
|
||||||
|
*
|
||||||
|
* @param request HTTP 요청
|
||||||
|
* @return 필터 적용 제외 여부
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||||
|
return "OPTIONS".equalsIgnoreCase(request.getMethod());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,215 @@
|
|||||||
|
package com.kt.event.common.security;
|
||||||
|
|
||||||
|
import com.kt.event.common.exception.ErrorCode;
|
||||||
|
import com.kt.event.common.exception.InfraException;
|
||||||
|
import io.jsonwebtoken.*;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 토큰 생성 및 검증 제공자
|
||||||
|
* Access Token 및 Refresh Token 생성/검증 기능 제공
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class JwtTokenProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT 서명 키
|
||||||
|
*/
|
||||||
|
private final SecretKey secretKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access Token 유효기간 (밀리초)
|
||||||
|
*/
|
||||||
|
private final long accessTokenValidityMs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh Token 유효기간 (밀리초)
|
||||||
|
*/
|
||||||
|
private final long refreshTokenValidityMs;
|
||||||
|
|
||||||
|
public JwtTokenProvider(
|
||||||
|
@Value("${jwt.secret}") String secret,
|
||||||
|
@Value("${jwt.access-token-validity:3600000}") long accessTokenValidityMs,
|
||||||
|
@Value("${jwt.refresh-token-validity:604800000}") long refreshTokenValidityMs) {
|
||||||
|
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||||
|
this.accessTokenValidityMs = accessTokenValidityMs;
|
||||||
|
this.refreshTokenValidityMs = refreshTokenValidityMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access Token 생성
|
||||||
|
*
|
||||||
|
* @param userId 사용자 ID
|
||||||
|
* @param email 이메일
|
||||||
|
* @param name 이름
|
||||||
|
* @param roles 역할 목록
|
||||||
|
* @return Access Token
|
||||||
|
*/
|
||||||
|
public String createAccessToken(Long userId, String email, String name, List<String> roles) {
|
||||||
|
Date now = new Date();
|
||||||
|
Date expiryDate = new Date(now.getTime() + accessTokenValidityMs);
|
||||||
|
|
||||||
|
return Jwts.builder()
|
||||||
|
.subject(userId.toString())
|
||||||
|
.claim("email", email)
|
||||||
|
.claim("name", name)
|
||||||
|
.claim("roles", roles)
|
||||||
|
.claim("type", "access")
|
||||||
|
.issuedAt(now)
|
||||||
|
.expiration(expiryDate)
|
||||||
|
.signWith(secretKey)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh Token 생성
|
||||||
|
*
|
||||||
|
* @param userId 사용자 ID
|
||||||
|
* @return Refresh Token
|
||||||
|
*/
|
||||||
|
public String createRefreshToken(Long userId) {
|
||||||
|
Date now = new Date();
|
||||||
|
Date expiryDate = new Date(now.getTime() + refreshTokenValidityMs);
|
||||||
|
|
||||||
|
return Jwts.builder()
|
||||||
|
.subject(userId.toString())
|
||||||
|
.claim("type", "refresh")
|
||||||
|
.issuedAt(now)
|
||||||
|
.expiration(expiryDate)
|
||||||
|
.signWith(secretKey)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰에서 사용자 ID 추출
|
||||||
|
*
|
||||||
|
* @param token JWT 토큰
|
||||||
|
* @return 사용자 ID
|
||||||
|
*/
|
||||||
|
public Long getUserIdFromToken(String token) {
|
||||||
|
Claims claims = parseToken(token);
|
||||||
|
return Long.parseLong(claims.getSubject());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰에서 UserPrincipal 추출
|
||||||
|
*
|
||||||
|
* @param token JWT 토큰
|
||||||
|
* @return UserPrincipal
|
||||||
|
*/
|
||||||
|
public UserPrincipal getUserPrincipalFromToken(String token) {
|
||||||
|
Claims claims = parseToken(token);
|
||||||
|
|
||||||
|
Long userId = Long.parseLong(claims.getSubject());
|
||||||
|
String email = claims.get("email", String.class);
|
||||||
|
String name = claims.get("name", String.class);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> roles = claims.get("roles", List.class);
|
||||||
|
|
||||||
|
return new UserPrincipal(userId, email, name, roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰 유효성 검증
|
||||||
|
*
|
||||||
|
* @param token JWT 토큰
|
||||||
|
* @return 유효 여부
|
||||||
|
*/
|
||||||
|
public boolean validateToken(String token) {
|
||||||
|
try {
|
||||||
|
parseToken(token);
|
||||||
|
return true;
|
||||||
|
} catch (SecurityException | MalformedJwtException e) {
|
||||||
|
log.error("Invalid JWT signature: {}", e.getMessage());
|
||||||
|
} catch (ExpiredJwtException e) {
|
||||||
|
log.error("Expired JWT token: {}", e.getMessage());
|
||||||
|
} catch (UnsupportedJwtException e) {
|
||||||
|
log.error("Unsupported JWT token: {}", e.getMessage());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.error("JWT claims string is empty: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰 타입 확인 (access/refresh)
|
||||||
|
*
|
||||||
|
* @param token JWT 토큰
|
||||||
|
* @return 토큰 타입
|
||||||
|
*/
|
||||||
|
public String getTokenType(String token) {
|
||||||
|
Claims claims = parseToken(token);
|
||||||
|
return claims.get("type", String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access Token 여부 확인
|
||||||
|
*
|
||||||
|
* @param token JWT 토큰
|
||||||
|
* @return Access Token 여부
|
||||||
|
*/
|
||||||
|
public boolean isAccessToken(String token) {
|
||||||
|
return "access".equals(getTokenType(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh Token 여부 확인
|
||||||
|
*
|
||||||
|
* @param token JWT 토큰
|
||||||
|
* @return Refresh Token 여부
|
||||||
|
*/
|
||||||
|
public boolean isRefreshToken(String token) {
|
||||||
|
return "refresh".equals(getTokenType(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰 파싱
|
||||||
|
*
|
||||||
|
* @param token JWT 토큰
|
||||||
|
* @return Claims
|
||||||
|
*/
|
||||||
|
private Claims parseToken(String token) {
|
||||||
|
try {
|
||||||
|
return Jwts.parser()
|
||||||
|
.verifyWith(secretKey)
|
||||||
|
.build()
|
||||||
|
.parseSignedClaims(token)
|
||||||
|
.getPayload();
|
||||||
|
} catch (ExpiredJwtException e) {
|
||||||
|
throw new InfraException(ErrorCode.AUTH_002, e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new InfraException(ErrorCode.AUTH_003, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰 만료 시간 조회
|
||||||
|
*
|
||||||
|
* @param token JWT 토큰
|
||||||
|
* @return 만료 시간
|
||||||
|
*/
|
||||||
|
public Date getExpirationFromToken(String token) {
|
||||||
|
Claims claims = parseToken(token);
|
||||||
|
return claims.getExpiration();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰 발급 시간 조회
|
||||||
|
*
|
||||||
|
* @param token JWT 토큰
|
||||||
|
* @return 발급 시간
|
||||||
|
*/
|
||||||
|
public Date getIssuedAtFromToken(String token) {
|
||||||
|
Claims claims = parseToken(token);
|
||||||
|
return claims.getIssuedAt();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,122 @@
|
|||||||
|
package com.kt.event.common.security;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Security 인증 주체
|
||||||
|
* JWT 토큰에서 추출한 사용자 정보를 담는 객체
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class UserPrincipal implements UserDetails {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 ID
|
||||||
|
*/
|
||||||
|
private final Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 이메일
|
||||||
|
*/
|
||||||
|
private final String email;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 이름
|
||||||
|
*/
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 역할 목록
|
||||||
|
*/
|
||||||
|
private final List<String> roles;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Security 권한 목록 반환
|
||||||
|
*
|
||||||
|
* @return 권한 목록
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||||
|
return roles.stream()
|
||||||
|
.map(SimpleGrantedAuthority::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비밀번호 반환 (JWT 인증에서는 사용하지 않음)
|
||||||
|
*
|
||||||
|
* @return null
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getPassword() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자명 반환 (이메일 사용)
|
||||||
|
*
|
||||||
|
* @return 이메일
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getUsername() {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 계정 만료 여부
|
||||||
|
*
|
||||||
|
* @return true (만료되지 않음)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isAccountNonExpired() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 계정 잠김 여부
|
||||||
|
*
|
||||||
|
* @return true (잠기지 않음)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isAccountNonLocked() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 자격증명 만료 여부
|
||||||
|
*
|
||||||
|
* @return true (만료되지 않음)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isCredentialsNonExpired() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 계정 활성화 여부
|
||||||
|
*
|
||||||
|
* @return true (활성화됨)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 역할 보유 여부 확인
|
||||||
|
*
|
||||||
|
* @param role 역할명
|
||||||
|
* @return 역할 보유 여부
|
||||||
|
*/
|
||||||
|
public boolean hasRole(String role) {
|
||||||
|
return roles.contains(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
148
common/src/main/java/com/kt/event/common/util/DateTimeUtil.java
Normal file
148
common/src/main/java/com/kt/event/common/util/DateTimeUtil.java
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package com.kt.event.common.util;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 날짜/시간 유틸리티
|
||||||
|
* 날짜와 시간 관련 공통 기능 제공
|
||||||
|
*/
|
||||||
|
public class DateTimeUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기본 날짜 시간 포맷터 (yyyy-MM-dd HH:mm:ss)
|
||||||
|
*/
|
||||||
|
private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 날짜 포맷터 (yyyy-MM-dd)
|
||||||
|
*/
|
||||||
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 시간 포맷터 (HH:mm:ss)
|
||||||
|
*/
|
||||||
|
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기본 타임존 (Asia/Seoul)
|
||||||
|
*/
|
||||||
|
private static final ZoneId DEFAULT_ZONE = ZoneId.of("Asia/Seoul");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 시간 조회 (Asia/Seoul 타임존)
|
||||||
|
*
|
||||||
|
* @return 현재 시간
|
||||||
|
*/
|
||||||
|
public static LocalDateTime now() {
|
||||||
|
return LocalDateTime.now(DEFAULT_ZONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LocalDateTime을 기본 포맷 문자열로 변환
|
||||||
|
*
|
||||||
|
* @param dateTime LocalDateTime 객체
|
||||||
|
* @return 포맷된 문자열 (yyyy-MM-dd HH:mm:ss)
|
||||||
|
*/
|
||||||
|
public static String format(LocalDateTime dateTime) {
|
||||||
|
if (dateTime == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return dateTime.format(DEFAULT_FORMATTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LocalDateTime을 날짜 포맷 문자열로 변환
|
||||||
|
*
|
||||||
|
* @param dateTime LocalDateTime 객체
|
||||||
|
* @return 포맷된 문자열 (yyyy-MM-dd)
|
||||||
|
*/
|
||||||
|
public static String formatDate(LocalDateTime dateTime) {
|
||||||
|
if (dateTime == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return dateTime.format(DATE_FORMATTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LocalDateTime을 시간 포맷 문자열로 변환
|
||||||
|
*
|
||||||
|
* @param dateTime LocalDateTime 객체
|
||||||
|
* @return 포맷된 문자열 (HH:mm:ss)
|
||||||
|
*/
|
||||||
|
public static String formatTime(LocalDateTime dateTime) {
|
||||||
|
if (dateTime == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return dateTime.format(TIME_FORMATTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 문자열을 LocalDateTime으로 파싱
|
||||||
|
*
|
||||||
|
* @param dateTimeStr 날짜 시간 문자열 (yyyy-MM-dd HH:mm:ss)
|
||||||
|
* @return LocalDateTime 객체
|
||||||
|
*/
|
||||||
|
public static LocalDateTime parse(String dateTimeStr) {
|
||||||
|
if (dateTimeStr == null || dateTimeStr.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return LocalDateTime.parse(dateTimeStr, DEFAULT_FORMATTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 두 날짜 사이의 차이 계산 (일 단위)
|
||||||
|
*
|
||||||
|
* @param start 시작 날짜
|
||||||
|
* @param end 종료 날짜
|
||||||
|
* @return 일수 차이
|
||||||
|
*/
|
||||||
|
public static long daysBetween(LocalDateTime start, LocalDateTime end) {
|
||||||
|
if (start == null || end == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return java.time.Duration.between(start, end).toDays();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 날짜가 특정 범위 내에 있는지 확인
|
||||||
|
*
|
||||||
|
* @param target 확인할 날짜
|
||||||
|
* @param start 시작 날짜
|
||||||
|
* @param end 종료 날짜
|
||||||
|
* @return 범위 내 여부
|
||||||
|
*/
|
||||||
|
public static boolean isBetween(LocalDateTime target, LocalDateTime start, LocalDateTime end) {
|
||||||
|
if (target == null || start == null || end == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !target.isBefore(start) && !target.isAfter(end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 날짜가 현재보다 이전인지 확인
|
||||||
|
*
|
||||||
|
* @param dateTime 확인할 날짜
|
||||||
|
* @return 과거 날짜 여부
|
||||||
|
*/
|
||||||
|
public static boolean isPast(LocalDateTime dateTime) {
|
||||||
|
if (dateTime == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return dateTime.isBefore(now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 날짜가 현재보다 이후인지 확인
|
||||||
|
*
|
||||||
|
* @param dateTime 확인할 날짜
|
||||||
|
* @return 미래 날짜 여부
|
||||||
|
*/
|
||||||
|
public static boolean isFuture(LocalDateTime dateTime) {
|
||||||
|
if (dateTime == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return dateTime.isAfter(now());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,157 @@
|
|||||||
|
package com.kt.event.common.util;
|
||||||
|
|
||||||
|
import com.kt.event.common.exception.InfraException;
|
||||||
|
import com.kt.event.common.exception.ErrorCode;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.GCMParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 암호화 유틸리티
|
||||||
|
* 비밀번호 해싱 및 데이터 암호화 기능 제공
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class EncryptionUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BCrypt 인코더 (Cost Factor: 10)
|
||||||
|
*/
|
||||||
|
private static final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES-256-GCM 설정
|
||||||
|
*/
|
||||||
|
private static final String ALGORITHM = "AES/GCM/NoPadding";
|
||||||
|
private static final int GCM_TAG_LENGTH = 128;
|
||||||
|
private static final int GCM_IV_LENGTH = 12;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비밀번호 해싱 (BCrypt)
|
||||||
|
*
|
||||||
|
* @param rawPassword 원본 비밀번호
|
||||||
|
* @return 해싱된 비밀번호
|
||||||
|
*/
|
||||||
|
public static String hashPassword(String rawPassword) {
|
||||||
|
if (StringUtil.isBlank(rawPassword)) {
|
||||||
|
throw new InfraException(ErrorCode.COMMON_002, "비밀번호는 필수입니다");
|
||||||
|
}
|
||||||
|
return passwordEncoder.encode(rawPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 비밀번호 검증 (BCrypt)
|
||||||
|
*
|
||||||
|
* @param rawPassword 원본 비밀번호
|
||||||
|
* @param hashedPassword 해싱된 비밀번호
|
||||||
|
* @return 일치 여부
|
||||||
|
*/
|
||||||
|
public static boolean verifyPassword(String rawPassword, String hashedPassword) {
|
||||||
|
if (StringUtil.isBlank(rawPassword) || StringUtil.isBlank(hashedPassword)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return passwordEncoder.matches(rawPassword, hashedPassword);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Password verification failed", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES-256-GCM 암호화
|
||||||
|
*
|
||||||
|
* @param plainText 평문
|
||||||
|
* @param secretKey 비밀키 (32바이트)
|
||||||
|
* @return Base64 인코딩된 암호문
|
||||||
|
*/
|
||||||
|
public static String encrypt(String plainText, String secretKey) {
|
||||||
|
if (StringUtil.isBlank(plainText)) {
|
||||||
|
return plainText;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// IV 생성 (12바이트)
|
||||||
|
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||||
|
SecureRandom random = new SecureRandom();
|
||||||
|
random.nextBytes(iv);
|
||||||
|
|
||||||
|
// 암호화
|
||||||
|
SecretKey key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "AES");
|
||||||
|
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||||
|
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
|
||||||
|
|
||||||
|
byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
// IV + 암호문 결합
|
||||||
|
ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + cipherText.length);
|
||||||
|
byteBuffer.put(iv);
|
||||||
|
byteBuffer.put(cipherText);
|
||||||
|
|
||||||
|
// Base64 인코딩
|
||||||
|
return Base64.getEncoder().encodeToString(byteBuffer.array());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Encryption failed", e);
|
||||||
|
throw new InfraException(ErrorCode.COMMON_004, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES-256-GCM 복호화
|
||||||
|
*
|
||||||
|
* @param cipherText Base64 인코딩된 암호문
|
||||||
|
* @param secretKey 비밀키 (32바이트)
|
||||||
|
* @return 평문
|
||||||
|
*/
|
||||||
|
public static String decrypt(String cipherText, String secretKey) {
|
||||||
|
if (StringUtil.isBlank(cipherText)) {
|
||||||
|
return cipherText;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Base64 디코딩
|
||||||
|
byte[] decodedData = Base64.getDecoder().decode(cipherText);
|
||||||
|
|
||||||
|
// IV와 암호문 분리
|
||||||
|
ByteBuffer byteBuffer = ByteBuffer.wrap(decodedData);
|
||||||
|
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||||
|
byteBuffer.get(iv);
|
||||||
|
byte[] encrypted = new byte[byteBuffer.remaining()];
|
||||||
|
byteBuffer.get(encrypted);
|
||||||
|
|
||||||
|
// 복호화
|
||||||
|
SecretKey key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "AES");
|
||||||
|
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||||
|
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec);
|
||||||
|
|
||||||
|
byte[] decryptedData = cipher.doFinal(encrypted);
|
||||||
|
return new String(decryptedData, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Decryption failed", e);
|
||||||
|
throw new InfraException(ErrorCode.COMMON_004, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 32바이트 비밀키 생성 (개발/테스트용)
|
||||||
|
* 실제 운영에서는 환경변수나 Key Management Service 사용 권장
|
||||||
|
*
|
||||||
|
* @param seed 시드 문자열
|
||||||
|
* @return 32바이트 비밀키
|
||||||
|
*/
|
||||||
|
public static String generateSecretKey(String seed) {
|
||||||
|
String paddedSeed = (seed + "00000000000000000000000000000000").substring(0, 32);
|
||||||
|
return paddedSeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
178
common/src/main/java/com/kt/event/common/util/StringUtil.java
Normal file
178
common/src/main/java/com/kt/event/common/util/StringUtil.java
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
package com.kt.event.common.util;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 문자열 유틸리티
|
||||||
|
* 문자열 처리 관련 공통 기능 제공
|
||||||
|
*/
|
||||||
|
public class StringUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 문자열이 null이거나 공백인지 확인
|
||||||
|
*
|
||||||
|
* @param str 확인할 문자열
|
||||||
|
* @return null 또는 공백 여부
|
||||||
|
*/
|
||||||
|
public static boolean isBlank(String str) {
|
||||||
|
return StringUtils.isBlank(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 문자열이 null이 아니고 공백이 아닌지 확인
|
||||||
|
*
|
||||||
|
* @param str 확인할 문자열
|
||||||
|
* @return null 또는 공백이 아닌지 여부
|
||||||
|
*/
|
||||||
|
public static boolean isNotBlank(String str) {
|
||||||
|
return StringUtils.isNotBlank(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전화번호 마스킹 처리
|
||||||
|
* 예: 010-1234-5678 → 010-****-5678
|
||||||
|
*
|
||||||
|
* @param phoneNumber 전화번호
|
||||||
|
* @return 마스킹된 전화번호
|
||||||
|
*/
|
||||||
|
public static String maskPhoneNumber(String phoneNumber) {
|
||||||
|
if (isBlank(phoneNumber)) {
|
||||||
|
return phoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
String cleaned = phoneNumber.replaceAll("[^0-9]", "");
|
||||||
|
|
||||||
|
if (cleaned.length() == 11) {
|
||||||
|
return cleaned.substring(0, 3) + "-****-" + cleaned.substring(7);
|
||||||
|
} else if (cleaned.length() == 10) {
|
||||||
|
return cleaned.substring(0, 3) + "-***-" + cleaned.substring(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
return phoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사업자번호 마스킹 처리
|
||||||
|
* 예: 123-45-67890 → 123-**-****0
|
||||||
|
*
|
||||||
|
* @param businessNumber 사업자번호
|
||||||
|
* @return 마스킹된 사업자번호
|
||||||
|
*/
|
||||||
|
public static String maskBusinessNumber(String businessNumber) {
|
||||||
|
if (isBlank(businessNumber)) {
|
||||||
|
return businessNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
String cleaned = businessNumber.replaceAll("[^0-9]", "");
|
||||||
|
|
||||||
|
if (cleaned.length() == 10) {
|
||||||
|
return cleaned.substring(0, 3) + "-**-****" + cleaned.substring(9);
|
||||||
|
}
|
||||||
|
|
||||||
|
return businessNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이메일 마스킹 처리
|
||||||
|
* 예: user@example.com → u***@example.com
|
||||||
|
*
|
||||||
|
* @param email 이메일
|
||||||
|
* @return 마스킹된 이메일
|
||||||
|
*/
|
||||||
|
public static String maskEmail(String email) {
|
||||||
|
if (isBlank(email) || !email.contains("@")) {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] parts = email.split("@");
|
||||||
|
String localPart = parts[0];
|
||||||
|
String domain = parts[1];
|
||||||
|
|
||||||
|
if (localPart.length() <= 1) {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
String masked = localPart.charAt(0) + "***";
|
||||||
|
return masked + "@" + domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 문자열을 지정된 길이로 자르고 말줄임표 추가
|
||||||
|
*
|
||||||
|
* @param str 원본 문자열
|
||||||
|
* @param maxLength 최대 길이
|
||||||
|
* @return 잘린 문자열
|
||||||
|
*/
|
||||||
|
public static String truncate(String str, int maxLength) {
|
||||||
|
if (isBlank(str) || str.length() <= maxLength) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
return str.substring(0, maxLength) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* null인 경우 기본값 반환
|
||||||
|
*
|
||||||
|
* @param str 원본 문자열
|
||||||
|
* @param defaultValue 기본값
|
||||||
|
* @return 원본 또는 기본값
|
||||||
|
*/
|
||||||
|
public static String defaultIfBlank(String str, String defaultValue) {
|
||||||
|
return StringUtils.defaultIfBlank(str, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 문자열에서 공백 제거
|
||||||
|
*
|
||||||
|
* @param str 원본 문자열
|
||||||
|
* @return 공백이 제거된 문자열
|
||||||
|
*/
|
||||||
|
public static String removeWhitespace(String str) {
|
||||||
|
if (isBlank(str)) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
return str.replaceAll("\\s+", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전화번호 포맷 검증
|
||||||
|
*
|
||||||
|
* @param phoneNumber 전화번호
|
||||||
|
* @return 유효한 전화번호 형식 여부
|
||||||
|
*/
|
||||||
|
public static boolean isValidPhoneNumber(String phoneNumber) {
|
||||||
|
if (isBlank(phoneNumber)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String cleaned = phoneNumber.replaceAll("[^0-9]", "");
|
||||||
|
return cleaned.length() >= 10 && cleaned.length() <= 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이메일 포맷 검증
|
||||||
|
*
|
||||||
|
* @param email 이메일
|
||||||
|
* @return 유효한 이메일 형식 여부
|
||||||
|
*/
|
||||||
|
public static boolean isValidEmail(String email) {
|
||||||
|
if (isBlank(email)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}$";
|
||||||
|
return email.matches(emailRegex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사업자번호 포맷 검증
|
||||||
|
*
|
||||||
|
* @param businessNumber 사업자번호
|
||||||
|
* @return 유효한 사업자번호 형식 여부
|
||||||
|
*/
|
||||||
|
public static boolean isValidBusinessNumber(String businessNumber) {
|
||||||
|
if (isBlank(businessNumber)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String cleaned = businessNumber.replaceAll("[^0-9]", "");
|
||||||
|
return cleaned.length() == 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,173 @@
|
|||||||
|
package com.kt.event.common.util;
|
||||||
|
|
||||||
|
import com.kt.event.common.exception.BusinessException;
|
||||||
|
import com.kt.event.common.exception.ErrorCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 유효성 검증 유틸리티
|
||||||
|
* 비즈니스 로직에서 사용하는 공통 유효성 검증 기능 제공
|
||||||
|
*/
|
||||||
|
public class ValidationUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* null 체크 및 예외 발생
|
||||||
|
*
|
||||||
|
* @param object 검증할 객체
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @throws BusinessException 객체가 null인 경우
|
||||||
|
*/
|
||||||
|
public static void requireNonNull(Object object, ErrorCode errorCode) {
|
||||||
|
if (object == null) {
|
||||||
|
throw new BusinessException(errorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* null 체크 및 예외 발생 (커스텀 메시지)
|
||||||
|
*
|
||||||
|
* @param object 검증할 객체
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @param message 커스텀 메시지
|
||||||
|
* @throws BusinessException 객체가 null인 경우
|
||||||
|
*/
|
||||||
|
public static void requireNonNull(Object object, ErrorCode errorCode, String message) {
|
||||||
|
if (object == null) {
|
||||||
|
throw new BusinessException(errorCode, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 문자열 공백 체크 및 예외 발생
|
||||||
|
*
|
||||||
|
* @param str 검증할 문자열
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @throws BusinessException 문자열이 null이거나 공백인 경우
|
||||||
|
*/
|
||||||
|
public static void requireNotBlank(String str, ErrorCode errorCode) {
|
||||||
|
if (StringUtil.isBlank(str)) {
|
||||||
|
throw new BusinessException(errorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 문자열 공백 체크 및 예외 발생 (커스텀 메시지)
|
||||||
|
*
|
||||||
|
* @param str 검증할 문자열
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @param message 커스텀 메시지
|
||||||
|
* @throws BusinessException 문자열이 null이거나 공백인 경우
|
||||||
|
*/
|
||||||
|
public static void requireNotBlank(String str, ErrorCode errorCode, String message) {
|
||||||
|
if (StringUtil.isBlank(str)) {
|
||||||
|
throw new BusinessException(errorCode, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 조건 검증 및 예외 발생
|
||||||
|
*
|
||||||
|
* @param condition 검증할 조건
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @throws BusinessException 조건이 false인 경우
|
||||||
|
*/
|
||||||
|
public static void require(boolean condition, ErrorCode errorCode) {
|
||||||
|
if (!condition) {
|
||||||
|
throw new BusinessException(errorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 조건 검증 및 예외 발생 (커스텀 메시지)
|
||||||
|
*
|
||||||
|
* @param condition 검증할 조건
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @param message 커스텀 메시지
|
||||||
|
* @throws BusinessException 조건이 false인 경우
|
||||||
|
*/
|
||||||
|
public static void require(boolean condition, ErrorCode errorCode, String message) {
|
||||||
|
if (!condition) {
|
||||||
|
throw new BusinessException(errorCode, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전화번호 유효성 검증
|
||||||
|
*
|
||||||
|
* @param phoneNumber 전화번호
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @throws BusinessException 유효하지 않은 전화번호인 경우
|
||||||
|
*/
|
||||||
|
public static void requireValidPhoneNumber(String phoneNumber, ErrorCode errorCode) {
|
||||||
|
if (!StringUtil.isValidPhoneNumber(phoneNumber)) {
|
||||||
|
throw new BusinessException(errorCode, "유효하지 않은 전화번호입니다: " + phoneNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이메일 유효성 검증
|
||||||
|
*
|
||||||
|
* @param email 이메일
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @throws BusinessException 유효하지 않은 이메일인 경우
|
||||||
|
*/
|
||||||
|
public static void requireValidEmail(String email, ErrorCode errorCode) {
|
||||||
|
if (!StringUtil.isValidEmail(email)) {
|
||||||
|
throw new BusinessException(errorCode, "유효하지 않은 이메일입니다: " + email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사업자번호 유효성 검증
|
||||||
|
*
|
||||||
|
* @param businessNumber 사업자번호
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @throws BusinessException 유효하지 않은 사업자번호인 경우
|
||||||
|
*/
|
||||||
|
public static void requireValidBusinessNumber(String businessNumber, ErrorCode errorCode) {
|
||||||
|
if (!StringUtil.isValidBusinessNumber(businessNumber)) {
|
||||||
|
throw new BusinessException(errorCode, "유효하지 않은 사업자번호입니다: " + businessNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 양수 검증
|
||||||
|
*
|
||||||
|
* @param value 검증할 값
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @throws BusinessException 값이 0보다 작거나 같은 경우
|
||||||
|
*/
|
||||||
|
public static void requirePositive(long value, ErrorCode errorCode) {
|
||||||
|
if (value <= 0) {
|
||||||
|
throw new BusinessException(errorCode, "값은 양수여야 합니다: " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 음수 아닌 값 검증
|
||||||
|
*
|
||||||
|
* @param value 검증할 값
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @throws BusinessException 값이 0보다 작은 경우
|
||||||
|
*/
|
||||||
|
public static void requireNonNegative(long value, ErrorCode errorCode) {
|
||||||
|
if (value < 0) {
|
||||||
|
throw new BusinessException(errorCode, "값은 음수가 아니어야 합니다: " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 범위 검증
|
||||||
|
*
|
||||||
|
* @param value 검증할 값
|
||||||
|
* @param min 최소값
|
||||||
|
* @param max 최대값
|
||||||
|
* @param errorCode 에러 코드
|
||||||
|
* @throws BusinessException 값이 범위를 벗어난 경우
|
||||||
|
*/
|
||||||
|
public static void requireInRange(long value, long min, long max, ErrorCode errorCode) {
|
||||||
|
if (value < min || value > max) {
|
||||||
|
throw new BusinessException(errorCode,
|
||||||
|
String.format("값은 %d ~ %d 범위여야 합니다: %d", min, max, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
content-service/build.gradle
Normal file
20
content-service/build.gradle
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
dependencies {
|
||||||
|
// Kafka Consumer
|
||||||
|
implementation 'org.springframework.kafka:spring-kafka'
|
||||||
|
|
||||||
|
// Redis for AI data reading and image URL caching
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||||
|
|
||||||
|
// OpenFeign for Stable Diffusion/DALL-E API
|
||||||
|
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
|
||||||
|
|
||||||
|
// Azure Blob Storage for CDN
|
||||||
|
implementation "com.azure:azure-storage-blob:${azureStorageVersion}"
|
||||||
|
|
||||||
|
// Resilience4j for Circuit Breaker
|
||||||
|
implementation "io.github.resilience4j:resilience4j-spring-boot3:${resilience4jVersion}"
|
||||||
|
implementation "io.github.resilience4j:resilience4j-circuitbreaker:${resilience4jVersion}"
|
||||||
|
|
||||||
|
// Jackson for JSON
|
||||||
|
implementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||||
|
}
|
||||||
610
develop/dev/package-structure.md
Normal file
610
develop/dev/package-structure.md
Normal file
@ -0,0 +1,610 @@
|
|||||||
|
# KT Event Marketing - Clean Architecture Package Structure
|
||||||
|
|
||||||
|
## 프로젝트 구조 개요
|
||||||
|
|
||||||
|
```
|
||||||
|
kt-event-marketing/
|
||||||
|
├── common/ # 공통 모듈
|
||||||
|
├── user-service/ # 사용자 서비스
|
||||||
|
├── event-service/ # 이벤트 서비스
|
||||||
|
├── ai-service/ # AI 서비스
|
||||||
|
├── content-service/ # 콘텐츠 서비스
|
||||||
|
├── distribution-service/ # 배포 서비스
|
||||||
|
├── participation-service/ # 참여 서비스
|
||||||
|
├── analytics-service/ # 분석 서비스
|
||||||
|
├── settings.gradle
|
||||||
|
└── build.gradle
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common 모듈 패키지 구조
|
||||||
|
|
||||||
|
```
|
||||||
|
common/
|
||||||
|
└── src/main/java/com/kt/event/common/
|
||||||
|
├── dto/
|
||||||
|
│ ├── ApiResponse.java # 공통 API 응답 래퍼
|
||||||
|
│ ├── PageResponse.java # 페이지네이션 응답
|
||||||
|
│ └── ErrorResponse.java # 에러 응답
|
||||||
|
├── exception/
|
||||||
|
│ ├── ErrorCode.java # 에러 코드 enum
|
||||||
|
│ ├── BusinessException.java # 비즈니스 예외
|
||||||
|
│ ├── InfraException.java # 인프라 예외
|
||||||
|
│ └── GlobalExceptionHandler.java # 전역 예외 핸들러
|
||||||
|
├── util/
|
||||||
|
│ ├── DateTimeUtil.java # 날짜/시간 유틸
|
||||||
|
│ ├── StringUtil.java # 문자열 유틸
|
||||||
|
│ ├── ValidationUtil.java # 유효성 검증 유틸
|
||||||
|
│ └── EncryptionUtil.java # 암호화 유틸
|
||||||
|
├── security/
|
||||||
|
│ ├── UserPrincipal.java # 인증된 사용자 정보
|
||||||
|
│ ├── JwtTokenProvider.java # JWT 토큰 제공자
|
||||||
|
│ └── JwtAuthenticationFilter.java # JWT 인증 필터
|
||||||
|
└── entity/
|
||||||
|
└── BaseTimeEntity.java # 생성/수정 시간 base entity
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Service 패키지 구조 (Clean Architecture)
|
||||||
|
|
||||||
|
```
|
||||||
|
user-service/
|
||||||
|
└── src/main/java/com/kt/event/user/
|
||||||
|
├── biz/ # Business Layer
|
||||||
|
│ ├── domain/ # 도메인 모델
|
||||||
|
│ │ ├── User.java
|
||||||
|
│ │ ├── Store.java
|
||||||
|
│ │ └── BusinessVerification.java
|
||||||
|
│ ├── usecase/ # Use Case 인터페이스
|
||||||
|
│ │ ├── in/ # Inbound Port (비즈니스 로직 진입점)
|
||||||
|
│ │ │ ├── RegisterUserUseCase.java
|
||||||
|
│ │ │ ├── LoginUserUseCase.java
|
||||||
|
│ │ │ ├── LogoutUserUseCase.java
|
||||||
|
│ │ │ ├── GetUserProfileUseCase.java
|
||||||
|
│ │ │ ├── UpdateUserProfileUseCase.java
|
||||||
|
│ │ │ ├── ChangePasswordUseCase.java
|
||||||
|
│ │ │ └── GetStoreInfoUseCase.java
|
||||||
|
│ │ └── out/ # Outbound Port (외부 의존성 인터페이스)
|
||||||
|
│ │ ├── UserReader.java
|
||||||
|
│ │ ├── UserWriter.java
|
||||||
|
│ │ ├── StoreReader.java
|
||||||
|
│ │ ├── StoreWriter.java
|
||||||
|
│ │ ├── RedisSessionWriter.java
|
||||||
|
│ │ └── BusinessVerifier.java
|
||||||
|
│ ├── service/ # Use Case 구현체
|
||||||
|
│ │ ├── UserService.java
|
||||||
|
│ │ ├── AuthenticationService.java
|
||||||
|
│ │ └── ProfileService.java
|
||||||
|
│ └── dto/ # 비즈니스 DTO
|
||||||
|
│ ├── UserCommand.java
|
||||||
|
│ ├── UserInfo.java
|
||||||
|
│ └── StoreInfo.java
|
||||||
|
└── infra/ # Infrastructure Layer
|
||||||
|
├── UserApplication.java # Spring Boot Main
|
||||||
|
├── controller/ # REST API Controller (Inbound Adapter)
|
||||||
|
│ ├── UserController.java
|
||||||
|
│ └── AuthController.java
|
||||||
|
├── dto/ # API Request/Response DTO
|
||||||
|
│ ├── request/
|
||||||
|
│ │ ├── UserRegisterRequest.java
|
||||||
|
│ │ ├── UserLoginRequest.java
|
||||||
|
│ │ ├── UserProfileUpdateRequest.java
|
||||||
|
│ │ └── ChangePasswordRequest.java
|
||||||
|
│ └── response/
|
||||||
|
│ ├── UserProfileResponse.java
|
||||||
|
│ ├── StoreInfoResponse.java
|
||||||
|
│ └── LoginResponse.java
|
||||||
|
├── gateway/ # Outbound Adapter
|
||||||
|
│ ├── entity/ # JPA Entity
|
||||||
|
│ │ ├── UserEntity.java
|
||||||
|
│ │ └── StoreEntity.java
|
||||||
|
│ ├── repository/ # JPA Repository
|
||||||
|
│ │ ├── UserJpaRepository.java
|
||||||
|
│ │ └── StoreJpaRepository.java
|
||||||
|
│ ├── UserGateway.java # User Reader/Writer 구현
|
||||||
|
│ ├── StoreGateway.java # Store Reader/Writer 구현
|
||||||
|
│ ├── RedisSessionGateway.java # Redis Session 구현
|
||||||
|
│ └── BusinessVerifierGateway.java # 사업자번호 검증 구현
|
||||||
|
└── config/ # 설정
|
||||||
|
├── SecurityConfig.java
|
||||||
|
├── RedisConfig.java
|
||||||
|
├── SwaggerConfig.java
|
||||||
|
└── DataLoader.java
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Event Service 패키지 구조 (Clean Architecture)
|
||||||
|
|
||||||
|
```
|
||||||
|
event-service/
|
||||||
|
└── src/main/java/com/kt/event/event/
|
||||||
|
├── biz/
|
||||||
|
│ ├── domain/
|
||||||
|
│ │ ├── Event.java
|
||||||
|
│ │ ├── EventObjective.java
|
||||||
|
│ │ ├── EventStatus.java
|
||||||
|
│ │ ├── AIRecommendation.java
|
||||||
|
│ │ ├── Image.java
|
||||||
|
│ │ ├── DistributionChannel.java
|
||||||
|
│ │ └── Job.java
|
||||||
|
│ ├── usecase/
|
||||||
|
│ │ ├── in/
|
||||||
|
│ │ │ ├── CreateEventObjectiveUseCase.java
|
||||||
|
│ │ │ ├── RequestAIRecommendationUseCase.java
|
||||||
|
│ │ │ ├── SelectAIRecommendationUseCase.java
|
||||||
|
│ │ │ ├── RequestImageGenerationUseCase.java
|
||||||
|
│ │ │ ├── SelectImageUseCase.java
|
||||||
|
│ │ │ ├── EditImageUseCase.java
|
||||||
|
│ │ │ ├── SelectDistributionChannelsUseCase.java
|
||||||
|
│ │ │ ├── PublishEventUseCase.java
|
||||||
|
│ │ │ ├── GetEventListUseCase.java
|
||||||
|
│ │ │ ├── GetEventDetailUseCase.java
|
||||||
|
│ │ │ ├── UpdateEventUseCase.java
|
||||||
|
│ │ │ ├── DeleteEventUseCase.java
|
||||||
|
│ │ │ ├── EndEventUseCase.java
|
||||||
|
│ │ │ └── GetJobStatusUseCase.java
|
||||||
|
│ │ └── out/
|
||||||
|
│ │ ├── EventReader.java
|
||||||
|
│ │ ├── EventWriter.java
|
||||||
|
│ │ ├── JobReader.java
|
||||||
|
│ │ ├── JobWriter.java
|
||||||
|
│ │ ├── KafkaJobPublisher.java
|
||||||
|
│ │ ├── KafkaEventPublisher.java
|
||||||
|
│ │ ├── RedisAIDataReader.java
|
||||||
|
│ │ ├── RedisImageDataReader.java
|
||||||
|
│ │ └── DistributionServiceCaller.java
|
||||||
|
│ ├── service/
|
||||||
|
│ │ ├── EventCreationService.java
|
||||||
|
│ │ ├── EventManagementService.java
|
||||||
|
│ │ ├── AIRecommendationService.java
|
||||||
|
│ │ ├── ImageGenerationService.java
|
||||||
|
│ │ ├── DistributionService.java
|
||||||
|
│ │ └── JobStatusService.java
|
||||||
|
│ └── dto/
|
||||||
|
│ ├── EventCommand.java
|
||||||
|
│ ├── EventInfo.java
|
||||||
|
│ ├── AIRecommendationInfo.java
|
||||||
|
│ ├── ImageInfo.java
|
||||||
|
│ ├── ChannelInfo.java
|
||||||
|
│ └── JobInfo.java
|
||||||
|
└── infra/
|
||||||
|
├── EventApplication.java
|
||||||
|
├── controller/
|
||||||
|
│ ├── EventController.java
|
||||||
|
│ ├── EventCreationController.java
|
||||||
|
│ └── JobController.java
|
||||||
|
├── dto/
|
||||||
|
│ ├── request/
|
||||||
|
│ │ ├── EventObjectiveRequest.java
|
||||||
|
│ │ ├── AIRecommendationSelectionRequest.java
|
||||||
|
│ │ ├── ImageSelectionRequest.java
|
||||||
|
│ │ ├── ImageEditRequest.java
|
||||||
|
│ │ ├── ChannelSelectionRequest.java
|
||||||
|
│ │ └── EventUpdateRequest.java
|
||||||
|
│ └── response/
|
||||||
|
│ ├── EventResponse.java
|
||||||
|
│ ├── EventListResponse.java
|
||||||
|
│ ├── JobStatusResponse.java
|
||||||
|
│ └── PublishResponse.java
|
||||||
|
├── gateway/
|
||||||
|
│ ├── entity/
|
||||||
|
│ │ ├── EventEntity.java
|
||||||
|
│ │ ├── AIRecommendationEntity.java
|
||||||
|
│ │ ├── ImageEntity.java
|
||||||
|
│ │ ├── DistributionChannelEntity.java
|
||||||
|
│ │ └── JobEntity.java
|
||||||
|
│ ├── repository/
|
||||||
|
│ │ ├── EventJpaRepository.java
|
||||||
|
│ │ ├── AIRecommendationJpaRepository.java
|
||||||
|
│ │ ├── ImageJpaRepository.java
|
||||||
|
│ │ ├── DistributionChannelJpaRepository.java
|
||||||
|
│ │ └── JobJpaRepository.java
|
||||||
|
│ ├── EventGateway.java
|
||||||
|
│ ├── JobGateway.java
|
||||||
|
│ ├── KafkaProducerGateway.java
|
||||||
|
│ ├── RedisGateway.java
|
||||||
|
│ └── DistributionServiceGateway.java
|
||||||
|
└── config/
|
||||||
|
├── SecurityConfig.java
|
||||||
|
├── KafkaProducerConfig.java
|
||||||
|
├── RedisConfig.java
|
||||||
|
├── SwaggerConfig.java
|
||||||
|
└── FeignConfig.java
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI Service 패키지 구조 (Clean Architecture)
|
||||||
|
|
||||||
|
```
|
||||||
|
ai-service/
|
||||||
|
└── src/main/java/com/kt/event/ai/
|
||||||
|
├── biz/
|
||||||
|
│ ├── domain/
|
||||||
|
│ │ ├── AIRecommendation.java
|
||||||
|
│ │ ├── TrendAnalysis.java
|
||||||
|
│ │ ├── EventRecommendation.java
|
||||||
|
│ │ └── Job.java
|
||||||
|
│ ├── usecase/
|
||||||
|
│ │ ├── in/
|
||||||
|
│ │ │ ├── GenerateAIRecommendationUseCase.java
|
||||||
|
│ │ │ ├── GetJobStatusUseCase.java
|
||||||
|
│ │ │ └── GetRecommendationResultUseCase.java
|
||||||
|
│ │ └── out/
|
||||||
|
│ │ ├── JobReader.java
|
||||||
|
│ │ ├── JobWriter.java
|
||||||
|
│ │ ├── RedisResultWriter.java
|
||||||
|
│ │ ├── RedisResultReader.java
|
||||||
|
│ │ ├── TrendAnalyzer.java
|
||||||
|
│ │ └── AIModelCaller.java
|
||||||
|
│ ├── service/
|
||||||
|
│ │ ├── AIRecommendationService.java
|
||||||
|
│ │ ├── TrendAnalysisService.java
|
||||||
|
│ │ └── JobManagementService.java
|
||||||
|
│ └── dto/
|
||||||
|
│ ├── AICommand.java
|
||||||
|
│ ├── AIResult.java
|
||||||
|
│ ├── TrendInfo.java
|
||||||
|
│ └── JobInfo.java
|
||||||
|
└── infra/
|
||||||
|
├── AIApplication.java
|
||||||
|
├── controller/
|
||||||
|
│ └── InternalAIController.java
|
||||||
|
├── dto/
|
||||||
|
│ ├── request/
|
||||||
|
│ │ └── AIRequestDto.java
|
||||||
|
│ └── response/
|
||||||
|
│ ├── AIRecommendationResponse.java
|
||||||
|
│ ├── JobStatusResponse.java
|
||||||
|
│ └── TrendAnalysisResponse.java
|
||||||
|
├── gateway/
|
||||||
|
│ ├── entity/
|
||||||
|
│ │ └── JobEntity.java
|
||||||
|
│ ├── repository/
|
||||||
|
│ │ └── JobJpaRepository.java
|
||||||
|
│ ├── JobGateway.java
|
||||||
|
│ ├── RedisGateway.java
|
||||||
|
│ ├── TrendAnalyzerGateway.java
|
||||||
|
│ └── ClaudeAPIGateway.java
|
||||||
|
├── consumer/
|
||||||
|
│ └── AIJobConsumer.java
|
||||||
|
└── config/
|
||||||
|
├── SecurityConfig.java
|
||||||
|
├── KafkaConsumerConfig.java
|
||||||
|
├── RedisConfig.java
|
||||||
|
├── SwaggerConfig.java
|
||||||
|
└── ClaudeAPIConfig.java
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Content Service 패키지 구조 (Clean Architecture)
|
||||||
|
|
||||||
|
```
|
||||||
|
content-service/
|
||||||
|
└── src/main/java/com/kt/event/content/
|
||||||
|
├── biz/
|
||||||
|
│ ├── domain/
|
||||||
|
│ │ ├── Content.java
|
||||||
|
│ │ ├── GeneratedImage.java
|
||||||
|
│ │ ├── ImageStyle.java
|
||||||
|
│ │ ├── Platform.java
|
||||||
|
│ │ └── Job.java
|
||||||
|
│ ├── usecase/
|
||||||
|
│ │ ├── in/
|
||||||
|
│ │ │ ├── GenerateImagesUseCase.java
|
||||||
|
│ │ │ ├── GetJobStatusUseCase.java
|
||||||
|
│ │ │ ├── GetEventContentUseCase.java
|
||||||
|
│ │ │ ├── GetImageListUseCase.java
|
||||||
|
│ │ │ ├── GetImageDetailUseCase.java
|
||||||
|
│ │ │ └── RegenerateImageUseCase.java
|
||||||
|
│ │ └── out/
|
||||||
|
│ │ ├── ContentReader.java
|
||||||
|
│ │ ├── ContentWriter.java
|
||||||
|
│ │ ├── JobReader.java
|
||||||
|
│ │ ├── JobWriter.java
|
||||||
|
│ │ ├── RedisAIDataReader.java
|
||||||
|
│ │ ├── RedisImageWriter.java
|
||||||
|
│ │ ├── ImageGeneratorCaller.java
|
||||||
|
│ │ └── CDNUploader.java
|
||||||
|
│ ├── service/
|
||||||
|
│ │ ├── ImageGenerationService.java
|
||||||
|
│ │ ├── ContentManagementService.java
|
||||||
|
│ │ └── JobManagementService.java
|
||||||
|
│ └── dto/
|
||||||
|
│ ├── ContentCommand.java
|
||||||
|
│ ├── ContentInfo.java
|
||||||
|
│ ├── ImageInfo.java
|
||||||
|
│ └── JobInfo.java
|
||||||
|
└── infra/
|
||||||
|
├── ContentApplication.java
|
||||||
|
├── controller/
|
||||||
|
│ └── ContentController.java
|
||||||
|
├── dto/
|
||||||
|
│ ├── request/
|
||||||
|
│ │ ├── ImageGenerationRequest.java
|
||||||
|
│ │ └── RegenerateRequest.java
|
||||||
|
│ └── response/
|
||||||
|
│ ├── ContentResponse.java
|
||||||
|
│ ├── ImageResponse.java
|
||||||
|
│ └── JobStatusResponse.java
|
||||||
|
├── gateway/
|
||||||
|
│ ├── entity/
|
||||||
|
│ │ ├── ContentEntity.java
|
||||||
|
│ │ ├── GeneratedImageEntity.java
|
||||||
|
│ │ └── JobEntity.java
|
||||||
|
│ ├── repository/
|
||||||
|
│ │ ├── ContentJpaRepository.java
|
||||||
|
│ │ ├── GeneratedImageJpaRepository.java
|
||||||
|
│ │ └── JobJpaRepository.java
|
||||||
|
│ ├── ContentGateway.java
|
||||||
|
│ ├── JobGateway.java
|
||||||
|
│ ├── RedisGateway.java
|
||||||
|
│ ├── StableDiffusionGateway.java
|
||||||
|
│ └── AzureBlobStorageGateway.java
|
||||||
|
├── consumer/
|
||||||
|
│ └── ImageGenerationJobConsumer.java
|
||||||
|
└── config/
|
||||||
|
├── SecurityConfig.java
|
||||||
|
├── KafkaConsumerConfig.java
|
||||||
|
├── RedisConfig.java
|
||||||
|
├── SwaggerConfig.java
|
||||||
|
└── AzureStorageConfig.java
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Distribution Service 패키지 구조 (Clean Architecture)
|
||||||
|
|
||||||
|
```
|
||||||
|
distribution-service/
|
||||||
|
└── src/main/java/com/kt/event/distribution/
|
||||||
|
├── biz/
|
||||||
|
│ ├── domain/
|
||||||
|
│ │ ├── Distribution.java
|
||||||
|
│ │ ├── DistributionChannel.java
|
||||||
|
│ │ ├── ChannelResult.java
|
||||||
|
│ │ └── DistributionStatus.java
|
||||||
|
│ ├── usecase/
|
||||||
|
│ │ ├── in/
|
||||||
|
│ │ │ ├── DistributeToChannelsUseCase.java
|
||||||
|
│ │ │ └── GetDistributionStatusUseCase.java
|
||||||
|
│ │ └── out/
|
||||||
|
│ │ ├── DistributionReader.java
|
||||||
|
│ │ ├── DistributionWriter.java
|
||||||
|
│ │ ├── ChannelDistributor.java
|
||||||
|
│ │ └── KafkaEventPublisher.java
|
||||||
|
│ ├── service/
|
||||||
|
│ │ ├── DistributionService.java
|
||||||
|
│ │ └── ChannelService.java
|
||||||
|
│ └── dto/
|
||||||
|
│ ├── DistributionCommand.java
|
||||||
|
│ ├── DistributionInfo.java
|
||||||
|
│ └── ChannelInfo.java
|
||||||
|
└── infra/
|
||||||
|
├── DistributionApplication.java
|
||||||
|
├── controller/
|
||||||
|
│ └── DistributionController.java
|
||||||
|
├── dto/
|
||||||
|
│ ├── request/
|
||||||
|
│ │ └── DistributionRequest.java
|
||||||
|
│ └── response/
|
||||||
|
│ ├── DistributionResponse.java
|
||||||
|
│ └── DistributionStatusResponse.java
|
||||||
|
├── gateway/
|
||||||
|
│ ├── entity/
|
||||||
|
│ │ ├── DistributionEntity.java
|
||||||
|
│ │ └── ChannelResultEntity.java
|
||||||
|
│ ├── repository/
|
||||||
|
│ │ ├── DistributionJpaRepository.java
|
||||||
|
│ │ └── ChannelResultJpaRepository.java
|
||||||
|
│ ├── DistributionGateway.java
|
||||||
|
│ ├── KafkaProducerGateway.java
|
||||||
|
│ └── channel/
|
||||||
|
│ ├── UriDongneTVGateway.java
|
||||||
|
│ ├── RingoBizGateway.java
|
||||||
|
│ ├── GenieTVGateway.java
|
||||||
|
│ ├── InstagramGateway.java
|
||||||
|
│ ├── NaverBlogGateway.java
|
||||||
|
│ └── KakaoChannelGateway.java
|
||||||
|
└── config/
|
||||||
|
├── SecurityConfig.java
|
||||||
|
├── KafkaProducerConfig.java
|
||||||
|
├── SwaggerConfig.java
|
||||||
|
├── ResilienceConfig.java
|
||||||
|
└── FeignConfig.java
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Participation Service 패키지 구조 (Clean Architecture)
|
||||||
|
|
||||||
|
```
|
||||||
|
participation-service/
|
||||||
|
└── src/main/java/com/kt/event/participation/
|
||||||
|
├── biz/
|
||||||
|
│ ├── domain/
|
||||||
|
│ │ ├── Participant.java
|
||||||
|
│ │ ├── Winner.java
|
||||||
|
│ │ ├── DrawResult.java
|
||||||
|
│ │ └── Consent.java
|
||||||
|
│ ├── usecase/
|
||||||
|
│ │ ├── in/
|
||||||
|
│ │ │ ├── ParticipateEventUseCase.java
|
||||||
|
│ │ │ ├── GetParticipantListUseCase.java
|
||||||
|
│ │ │ ├── GetParticipantDetailUseCase.java
|
||||||
|
│ │ │ ├── DrawWinnersUseCase.java
|
||||||
|
│ │ │ └── GetWinnerListUseCase.java
|
||||||
|
│ │ └── out/
|
||||||
|
│ │ ├── ParticipantReader.java
|
||||||
|
│ │ ├── ParticipantWriter.java
|
||||||
|
│ │ ├── WinnerReader.java
|
||||||
|
│ │ ├── WinnerWriter.java
|
||||||
|
│ │ ├── DrawExecutor.java
|
||||||
|
│ │ └── KafkaEventPublisher.java
|
||||||
|
│ ├── service/
|
||||||
|
│ │ ├── ParticipationService.java
|
||||||
|
│ │ └── WinnerDrawService.java
|
||||||
|
│ └── dto/
|
||||||
|
│ ├── ParticipationCommand.java
|
||||||
|
│ ├── ParticipantInfo.java
|
||||||
|
│ ├── WinnerInfo.java
|
||||||
|
│ └── DrawCommand.java
|
||||||
|
└── infra/
|
||||||
|
├── ParticipationApplication.java
|
||||||
|
├── controller/
|
||||||
|
│ └── ParticipationController.java
|
||||||
|
├── dto/
|
||||||
|
│ ├── request/
|
||||||
|
│ │ ├── ParticipationRequest.java
|
||||||
|
│ │ └── WinnerDrawRequest.java
|
||||||
|
│ └── response/
|
||||||
|
│ ├── ParticipationResponse.java
|
||||||
|
│ ├── ParticipantListResponse.java
|
||||||
|
│ └── WinnerResponse.java
|
||||||
|
├── gateway/
|
||||||
|
│ ├── entity/
|
||||||
|
│ │ ├── ParticipantEntity.java
|
||||||
|
│ │ └── WinnerEntity.java
|
||||||
|
│ ├── repository/
|
||||||
|
│ │ ├── ParticipantJpaRepository.java
|
||||||
|
│ │ └── WinnerJpaRepository.java
|
||||||
|
│ ├── ParticipantGateway.java
|
||||||
|
│ ├── WinnerGateway.java
|
||||||
|
│ ├── DrawExecutorGateway.java
|
||||||
|
│ └── KafkaProducerGateway.java
|
||||||
|
└── config/
|
||||||
|
├── SecurityConfig.java
|
||||||
|
├── KafkaProducerConfig.java
|
||||||
|
└── SwaggerConfig.java
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Analytics Service 패키지 구조 (Clean Architecture)
|
||||||
|
|
||||||
|
```
|
||||||
|
analytics-service/
|
||||||
|
└── src/main/java/com/kt/event/analytics/
|
||||||
|
├── biz/
|
||||||
|
│ ├── domain/
|
||||||
|
│ │ ├── EventAnalytics.java
|
||||||
|
│ │ ├── ChannelPerformance.java
|
||||||
|
│ │ ├── TimelineData.java
|
||||||
|
│ │ ├── RoiDetail.java
|
||||||
|
│ │ └── ParticipantProfile.java
|
||||||
|
│ ├── usecase/
|
||||||
|
│ │ ├── in/
|
||||||
|
│ │ │ ├── GetAnalyticsDashboardUseCase.java
|
||||||
|
│ │ │ ├── GetChannelPerformanceUseCase.java
|
||||||
|
│ │ │ ├── GetTimelineDataUseCase.java
|
||||||
|
│ │ │ └── GetRoiDetailUseCase.java
|
||||||
|
│ │ └── out/
|
||||||
|
│ │ ├── AnalyticsReader.java
|
||||||
|
│ │ ├── AnalyticsWriter.java
|
||||||
|
│ │ ├── ExternalAPIDataCollector.java
|
||||||
|
│ │ └── RedisAnalyticsCache.java
|
||||||
|
│ ├── service/
|
||||||
|
│ │ ├── AnalyticsDashboardService.java
|
||||||
|
│ │ ├── PerformanceAnalysisService.java
|
||||||
|
│ │ └── RoiCalculationService.java
|
||||||
|
│ └── dto/
|
||||||
|
│ ├── AnalyticsCommand.java
|
||||||
|
│ ├── AnalyticsInfo.java
|
||||||
|
│ ├── PerformanceInfo.java
|
||||||
|
│ └── RoiInfo.java
|
||||||
|
└── infra/
|
||||||
|
├── AnalyticsApplication.java
|
||||||
|
├── controller/
|
||||||
|
│ └── AnalyticsController.java
|
||||||
|
├── dto/
|
||||||
|
│ └── response/
|
||||||
|
│ ├── AnalyticsDashboardResponse.java
|
||||||
|
│ ├── ChannelPerformanceResponse.java
|
||||||
|
│ ├── TimelineDataResponse.java
|
||||||
|
│ └── RoiDetailResponse.java
|
||||||
|
├── gateway/
|
||||||
|
│ ├── entity/
|
||||||
|
│ │ ├── EventAnalyticsEntity.java
|
||||||
|
│ │ ├── ChannelPerformanceEntity.java
|
||||||
|
│ │ └── TimelineDataEntity.java
|
||||||
|
│ ├── repository/
|
||||||
|
│ │ ├── EventAnalyticsJpaRepository.java
|
||||||
|
│ │ ├── ChannelPerformanceJpaRepository.java
|
||||||
|
│ │ └── TimelineDataJpaRepository.java
|
||||||
|
│ ├── AnalyticsGateway.java
|
||||||
|
│ ├── RedisGateway.java
|
||||||
|
│ └── external/
|
||||||
|
│ ├── UriDongneTVAPIGateway.java
|
||||||
|
│ ├── GenieTVAPIGateway.java
|
||||||
|
│ ├── InstagramAPIGateway.java
|
||||||
|
│ ├── NaverAPIGateway.java
|
||||||
|
│ └── KakaoAPIGateway.java
|
||||||
|
├── consumer/
|
||||||
|
│ └── AnalyticsEventConsumer.java
|
||||||
|
└── config/
|
||||||
|
├── SecurityConfig.java
|
||||||
|
├── KafkaConsumerConfig.java
|
||||||
|
├── RedisConfig.java
|
||||||
|
├── SwaggerConfig.java
|
||||||
|
├── ResilienceConfig.java
|
||||||
|
└── FeignConfig.java
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 아키텍처 설명
|
||||||
|
|
||||||
|
### Clean Architecture 레이어 구조
|
||||||
|
|
||||||
|
1. **biz (Business Layer)** - 비즈니스 로직
|
||||||
|
- `domain/`: 핵심 도메인 모델 (순수 Java 객체, 외부 의존성 없음)
|
||||||
|
- `usecase/in/`: Inbound Port (비즈니스 로직 진입점 인터페이스)
|
||||||
|
- `usecase/out/`: Outbound Port (외부 의존성 인터페이스)
|
||||||
|
- `service/`: Use Case 구현체 (비즈니스 로직 실행)
|
||||||
|
- `dto/`: 비즈니스 레이어 내부 DTO
|
||||||
|
|
||||||
|
2. **infra (Infrastructure Layer)** - 외부 세계와의 통신
|
||||||
|
- `controller/`: REST API Controller (Inbound Adapter)
|
||||||
|
- `dto/request/`, `dto/response/`: API 계약 DTO
|
||||||
|
- `gateway/`: Outbound Adapter (DB, 외부 API, Kafka, Redis 등)
|
||||||
|
- `gateway/entity/`: JPA Entity
|
||||||
|
- `gateway/repository/`: JPA Repository
|
||||||
|
- `consumer/`: Kafka Consumer
|
||||||
|
- `config/`: 설정 클래스
|
||||||
|
|
||||||
|
### 의존성 규칙
|
||||||
|
|
||||||
|
- **biz → infra**: ❌ 불가능 (비즈니스 로직은 인프라에 의존하지 않음)
|
||||||
|
- **infra → biz**: ✅ 가능 (인프라는 비즈니스 인터페이스를 구현)
|
||||||
|
- **domain → 외부**: ❌ 불가능 (순수 비즈니스 로직만 포함)
|
||||||
|
|
||||||
|
### 네이밍 규칙
|
||||||
|
|
||||||
|
- **UseCase 인터페이스**: `{동작}{대상}UseCase` (예: `RegisterUserUseCase`)
|
||||||
|
- **Service 구현체**: `{대상}Service` (예: `UserService`)
|
||||||
|
- **Gateway 구현체**: `{대상}Gateway` (예: `UserGateway`)
|
||||||
|
- **Port 인터페이스**: `{대상}{동작}` (예: `UserReader`, `UserWriter`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 기술 스택 정리
|
||||||
|
|
||||||
|
| 레이어 | 기술 |
|
||||||
|
|--------|------|
|
||||||
|
| Framework | Spring Boot 3.3.0, Java 21 |
|
||||||
|
| Database | PostgreSQL (각 서비스별 독립 DB) |
|
||||||
|
| Cache | Redis (공통) |
|
||||||
|
| Message Queue | Kafka (비동기 Job 처리, Event 발행) |
|
||||||
|
| Security | JWT, Spring Security |
|
||||||
|
| API Documentation | Swagger UI (SpringDoc OpenAPI) |
|
||||||
|
| Persistence | Spring Data JPA |
|
||||||
|
| Build Tool | Gradle 8.x |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**작성일**: 2025-10-23
|
||||||
|
**작성자**: Backend Developer (리액트킹)
|
||||||
16
distribution-service/build.gradle
Normal file
16
distribution-service/build.gradle
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
dependencies {
|
||||||
|
// Kafka for event publishing
|
||||||
|
implementation 'org.springframework.kafka:spring-kafka'
|
||||||
|
|
||||||
|
// OpenFeign for external channel APIs
|
||||||
|
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
|
||||||
|
|
||||||
|
// Resilience4j for Circuit Breaker, Retry, Bulkhead
|
||||||
|
implementation "io.github.resilience4j:resilience4j-spring-boot3:${resilience4jVersion}"
|
||||||
|
implementation "io.github.resilience4j:resilience4j-circuitbreaker:${resilience4jVersion}"
|
||||||
|
implementation "io.github.resilience4j:resilience4j-retry:${resilience4jVersion}"
|
||||||
|
implementation "io.github.resilience4j:resilience4j-bulkhead:${resilience4jVersion}"
|
||||||
|
|
||||||
|
// Jackson for JSON
|
||||||
|
implementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||||
|
}
|
||||||
13
event-service/build.gradle
Normal file
13
event-service/build.gradle
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
dependencies {
|
||||||
|
// Kafka for job publishing
|
||||||
|
implementation 'org.springframework.kafka:spring-kafka'
|
||||||
|
|
||||||
|
// Redis for AI/Image data caching
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||||
|
|
||||||
|
// OpenFeign for Distribution Service call
|
||||||
|
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
|
||||||
|
|
||||||
|
// Jackson for JSON
|
||||||
|
implementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||||
|
}
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
248
gradlew
vendored
Executable file
248
gradlew
vendored
Executable file
@ -0,0 +1,248 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
93
gradlew.bat
vendored
Normal file
93
gradlew.bat
vendored
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
7
participation-service/build.gradle
Normal file
7
participation-service/build.gradle
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
dependencies {
|
||||||
|
// Kafka for event publishing
|
||||||
|
implementation 'org.springframework.kafka:spring-kafka'
|
||||||
|
|
||||||
|
// Jackson for JSON
|
||||||
|
implementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||||
|
}
|
||||||
13
settings.gradle
Normal file
13
settings.gradle
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
rootProject.name = 'kt-event-marketing'
|
||||||
|
|
||||||
|
// Common module
|
||||||
|
include 'common'
|
||||||
|
|
||||||
|
// Microservices
|
||||||
|
include 'user-service'
|
||||||
|
include 'event-service'
|
||||||
|
include 'ai-service'
|
||||||
|
include 'content-service'
|
||||||
|
include 'distribution-service'
|
||||||
|
include 'participation-service'
|
||||||
|
include 'analytics-service'
|
||||||
10
user-service/build.gradle
Normal file
10
user-service/build.gradle
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
dependencies {
|
||||||
|
// BCrypt for password hashing
|
||||||
|
implementation 'org.springframework.security:spring-security-crypto'
|
||||||
|
|
||||||
|
// Redis for session management
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||||
|
|
||||||
|
// OpenFeign for external API calls (사업자번호 검증)
|
||||||
|
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user