From ea82ff474850015d27c823ee0491c6f1205c10fd Mon Sep 17 00:00:00 2001 From: cherry2250 Date: Thu, 23 Oct 2025 17:54:28 +0900 Subject: [PATCH] add common module --- .gradle/8.10/checksums/checksums.lock | Bin 0 -> 17 bytes .gradle/8.10/checksums/md5-checksums.bin | Bin 0 -> 73965 bytes .gradle/8.10/checksums/sha1-checksums.bin | Bin 0 -> 153107 bytes .../8.10/dependencies-accessors/gc.properties | 0 .../executionHistory/executionHistory.bin | Bin 0 -> 85985 bytes .../executionHistory/executionHistory.lock | Bin 0 -> 17 bytes .gradle/8.10/fileChanges/last-build.bin | Bin 0 -> 1 bytes .gradle/8.10/fileHashes/fileHashes.bin | Bin 0 -> 20297 bytes .gradle/8.10/fileHashes/fileHashes.lock | Bin 0 -> 17 bytes .../8.10/fileHashes/resourceHashesCache.bin | Bin 0 -> 19075 bytes .gradle/8.10/gc.properties | 0 .gradle/9.1.0/checksums/checksums.lock | Bin 0 -> 17 bytes .../executionHistory/executionHistory.bin | Bin 0 -> 19693 bytes .../executionHistory/executionHistory.lock | Bin 0 -> 17 bytes .gradle/9.1.0/fileChanges/last-build.bin | Bin 0 -> 1 bytes .gradle/9.1.0/fileHashes/fileHashes.bin | Bin 0 -> 18697 bytes .gradle/9.1.0/fileHashes/fileHashes.lock | Bin 0 -> 17 bytes .gradle/9.1.0/gc.properties | 0 .../buildOutputCleanup.lock | Bin 0 -> 17 bytes .gradle/buildOutputCleanup/cache.properties | 2 + .gradle/buildOutputCleanup/outputFiles.bin | Bin 0 -> 18965 bytes .gradle/file-system.probe | Bin 0 -> 8 bytes .gradle/vcs-1/gc.properties | 0 ai-service/build.gradle | 17 + analytics-service/build.gradle | 17 + build.gradle | 124 ++++ claude/dev-backend.md | 662 ++++++++++++++++++ claude/standard_comment.md | 518 ++++++++++++++ claude/standard_package_structure.md | 173 +++++ claude/standard_testcode.md | 214 ++++++ common/build.gradle | 35 + .../com/kt/event/common/dto/ApiResponse.java | 78 +++ .../kt/event/common/dto/ErrorResponse.java | 122 ++++ .../com/kt/event/common/dto/PageResponse.java | 75 ++ .../event/common/entity/BaseTimeEntity.java | 35 + .../common/exception/BusinessException.java | 84 +++ .../kt/event/common/exception/ErrorCode.java | 108 +++ .../exception/GlobalExceptionHandler.java | 198 ++++++ .../common/exception/InfraException.java | 84 +++ .../security/JwtAuthenticationFilter.java | 130 ++++ .../common/security/JwtTokenProvider.java | 215 ++++++ .../event/common/security/UserPrincipal.java | 122 ++++ .../kt/event/common/util/DateTimeUtil.java | 148 ++++ .../kt/event/common/util/EncryptionUtil.java | 157 +++++ .../com/kt/event/common/util/StringUtil.java | 178 +++++ .../kt/event/common/util/ValidationUtil.java | 173 +++++ content-service/build.gradle | 20 + develop/dev/package-structure.md | 610 ++++++++++++++++ distribution-service/build.gradle | 16 + event-service/build.gradle | 13 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45457 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 248 +++++++ gradlew.bat | 93 +++ participation-service/build.gradle | 7 + settings.gradle | 13 + user-service/build.gradle | 10 + 57 files changed, 4706 insertions(+) create mode 100644 .gradle/8.10/checksums/checksums.lock create mode 100644 .gradle/8.10/checksums/md5-checksums.bin create mode 100644 .gradle/8.10/checksums/sha1-checksums.bin create mode 100644 .gradle/8.10/dependencies-accessors/gc.properties create mode 100644 .gradle/8.10/executionHistory/executionHistory.bin create mode 100644 .gradle/8.10/executionHistory/executionHistory.lock create mode 100644 .gradle/8.10/fileChanges/last-build.bin create mode 100644 .gradle/8.10/fileHashes/fileHashes.bin create mode 100644 .gradle/8.10/fileHashes/fileHashes.lock create mode 100644 .gradle/8.10/fileHashes/resourceHashesCache.bin create mode 100644 .gradle/8.10/gc.properties create mode 100644 .gradle/9.1.0/checksums/checksums.lock create mode 100644 .gradle/9.1.0/executionHistory/executionHistory.bin create mode 100644 .gradle/9.1.0/executionHistory/executionHistory.lock create mode 100644 .gradle/9.1.0/fileChanges/last-build.bin create mode 100644 .gradle/9.1.0/fileHashes/fileHashes.bin create mode 100644 .gradle/9.1.0/fileHashes/fileHashes.lock create mode 100644 .gradle/9.1.0/gc.properties create mode 100644 .gradle/buildOutputCleanup/buildOutputCleanup.lock create mode 100644 .gradle/buildOutputCleanup/cache.properties create mode 100644 .gradle/buildOutputCleanup/outputFiles.bin create mode 100644 .gradle/file-system.probe create mode 100644 .gradle/vcs-1/gc.properties create mode 100644 ai-service/build.gradle create mode 100644 analytics-service/build.gradle create mode 100644 build.gradle create mode 100644 claude/dev-backend.md create mode 100644 claude/standard_comment.md create mode 100644 claude/standard_package_structure.md create mode 100644 claude/standard_testcode.md create mode 100644 common/build.gradle create mode 100644 common/src/main/java/com/kt/event/common/dto/ApiResponse.java create mode 100644 common/src/main/java/com/kt/event/common/dto/ErrorResponse.java create mode 100644 common/src/main/java/com/kt/event/common/dto/PageResponse.java create mode 100644 common/src/main/java/com/kt/event/common/entity/BaseTimeEntity.java create mode 100644 common/src/main/java/com/kt/event/common/exception/BusinessException.java create mode 100644 common/src/main/java/com/kt/event/common/exception/ErrorCode.java create mode 100644 common/src/main/java/com/kt/event/common/exception/GlobalExceptionHandler.java create mode 100644 common/src/main/java/com/kt/event/common/exception/InfraException.java create mode 100644 common/src/main/java/com/kt/event/common/security/JwtAuthenticationFilter.java create mode 100644 common/src/main/java/com/kt/event/common/security/JwtTokenProvider.java create mode 100644 common/src/main/java/com/kt/event/common/security/UserPrincipal.java create mode 100644 common/src/main/java/com/kt/event/common/util/DateTimeUtil.java create mode 100644 common/src/main/java/com/kt/event/common/util/EncryptionUtil.java create mode 100644 common/src/main/java/com/kt/event/common/util/StringUtil.java create mode 100644 common/src/main/java/com/kt/event/common/util/ValidationUtil.java create mode 100644 content-service/build.gradle create mode 100644 develop/dev/package-structure.md create mode 100644 distribution-service/build.gradle create mode 100644 event-service/build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 participation-service/build.gradle create mode 100644 settings.gradle create mode 100644 user-service/build.gradle diff --git a/.gradle/8.10/checksums/checksums.lock b/.gradle/8.10/checksums/checksums.lock new file mode 100644 index 0000000000000000000000000000000000000000..837e5b9337bcfdbcf1aebd76ad9c0a8bfa0490ec GIT binary patch literal 17 VcmZQJJh3L%MKEUr0~l~D0RSxo1Tz2t literal 0 HcmV?d00001 diff --git a/.gradle/8.10/checksums/md5-checksums.bin b/.gradle/8.10/checksums/md5-checksums.bin new file mode 100644 index 0000000000000000000000000000000000000000..04c6d0050987548d4ed9b0f3828b171e49d75fb8 GIT binary patch literal 73965 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�zx1Zk6V^_B}PGWwj68Gr#d$~r( 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θ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 C=mk{( literal 0 HcmV?d00001 diff --git a/.gradle/8.10/executionHistory/executionHistory.lock b/.gradle/8.10/executionHistory/executionHistory.lock new file mode 100644 index 0000000000000000000000000000000000000000..0ce4c9646ca85d214092028f7db63bee6e79e803 GIT binary patch literal 17 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 literal 0 HcmV?d00001 diff --git a/.gradle/8.10/gc.properties b/.gradle/8.10/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.gradle/9.1.0/checksums/checksums.lock b/.gradle/9.1.0/checksums/checksums.lock new file mode 100644 index 0000000000000000000000000000000000000000..3d9ab526b250d28282a22b45eab88b7f8bff0acb GIT binary patch literal 17 TcmZP;Fbjdc$cuwqsw*?6eIW)DTQG zR-hL(qM|V>Aw*)LCLZMIK{yyWav|a11tS`7lIZpoO7uV?CdPc5?CxZCcmCh}X1A9* z2`M-K$ZceKD_d@&1px>^00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P-n{BH-31Qj(oa z`GKNzV^e~VnvHtHP~+Wxwy5;U{p{ryNB;kV{8{hcm%WA~1Rwwb2tWV=5P$##AOHaf zKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY>&fizHx0$dHL0c2 zN5MTY$)l0AsI_9i-xR!ASkM)qb@m2VEKF8B*4d-7;#aGhv$G(6`mz4T<0pp-OSnbI zI2n*q)eb9)#zdTZnU-H$y+no~51=X8MYkTZ)z(-&JYwtR>Oi*dG!vR`WW18THW*N*ER%ZmlkjIJ*_Joa!~niLa#*3m!=+n`!F>Uo|wHi z`DVU$-OEgn)O?X?P!&=A(~+EMnLr=^dZ6Wk#5`x&O`2cd-CVJwt+urNR@sY8uI~oZ rbuF_bnMiNfEK-7)M;&#pnck|IbN%~2d#G_}AyaYesL7!JJdOSY%9HD| literal 0 HcmV?d00001 diff --git a/.gradle/9.1.0/executionHistory/executionHistory.lock b/.gradle/9.1.0/executionHistory/executionHistory.lock new file mode 100644 index 0000000000000000000000000000000000000000..4cc7cd5cb479d832171fcd0db53f72124c463412 GIT binary patch literal 17 UcmZQhi#U39CF7$_3=qH!06235od5s; literal 0 HcmV?d00001 diff --git a/.gradle/9.1.0/fileChanges/last-build.bin b/.gradle/9.1.0/fileChanges/last-build.bin new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/.gradle/9.1.0/fileHashes/fileHashes.bin b/.gradle/9.1.0/fileHashes/fileHashes.bin new file mode 100644 index 0000000000000000000000000000000000000000..5c96b1a591f309e961c35d4eadb3b3bd15c7d1a4 GIT binary patch literal 18697 zcmeI)&ntvM9LMqT^I*wiOZI?TksnFck3V3exY(YOqp}WG;vhl@2baCA6UpYZ(Wcmg zgM_r?MIJjXS^O%`7?a@u|sd?txcltEV>pJxe3*oaIJw##D7fLDw5I_I{ z1Q0*~0R#|0009ILKmY**5I_I{1Q7Ty0u%BgRq8Un?ozj?T@|9YS{CK$jpdLtci>gq zqf6%f;Oiv+7J0FsbU$eN`AO%qDRgEm*smhu5o0xa!Eb5uf2~l!r4Xtge6FjzDdwwUjGl}>6%<;l`E}p)A z4pivGph~3niW83wZDQo=sIIwjEKpJFGXR&}v%drI*Xnqy-&9V!gogC~Lh?S)#Hq2X&~3SVWhC4mOLr zG;m-_2aU+GI!LPodq9LbET}LLhYQ3*hYlqJrJnKpen>alt@u9h?D_M3e$T`HpX;{I zFv`n*$zd!UYZjKQ5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0;re-Sw9Y-EMIneweA zg+*!HFltvhi*=nXd(CsVLYv*&W=?-UxSyGyx4pig^ds7Pk_X#kuMS7tZe7&=^6CDa zBe!RBGC!={9-NrnajLyp<}0+n+gOpUYBc9${;~EC=VReLT~~Ufr?h|a_20}ic(QJ< zU#fk)Vf1bMPTh#xtsB~>lD*y+SF@L-2ejvU6Vd+WyzO=~qEE^21lz_=^i5StPiwE;-rD3_8-DI~ z^R)KhXM5MCZNZrIjP_7tb$+xvqW%xHn=O?CTdq&Mk@vsXUcY*G+q6INNZ!+-J-p#t z-@Uuz&tyKS{n%jV*#oh$k23#5dsD#va`JdpUgjg(&ph#)57w8-_q3wgFU+JfLtndB z%DhLrUJaw-kH!u3B-`f{u AmjD0& literal 0 HcmV?d00001 diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe new file mode 100644 index 0000000000000000000000000000000000000000..ac4beb46220d110a11f9e5f196fa452a079e920d GIT binary patch literal 8 PcmZQzV4Nl3y6qtV25bU| literal 0 HcmV?d00001 diff --git a/.gradle/vcs-1/gc.properties b/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/ai-service/build.gradle b/ai-service/build.gradle new file mode 100644 index 0000000..a39127e --- /dev/null +++ b/ai-service/build.gradle @@ -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' +} diff --git a/analytics-service/build.gradle b/analytics-service/build.gradle new file mode 100644 index 0000000..a72c1bc --- /dev/null +++ b/analytics-service/build.gradle @@ -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' +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..3f32137 --- /dev/null +++ b/build.gradle @@ -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' +} diff --git a/claude/dev-backend.md b/claude/dev-backend.md new file mode 100644 index 0000000..81ece9d --- /dev/null +++ b/claude/dev-backend.md @@ -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 작성 + - 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 클래스 작성: '' 참조 + - JWT 인증 처리 클래스 작성: '' 참조 + - Swagger Config 클래스 작성: '' 참조 + - 테스트 코드 작성은 하지 않음 + + + +- **중앙 버전 관리**: 루트 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 등의 연결 정보는 반드시 환경변수로 변환해야 함: '' 참조 +- 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' +} +``` + + +``` +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} + +``` + + +``` +/** + * 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; + } +} +``` + + + +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; + } +} +``` + + +``` +/** + * 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"); + } +} +``` diff --git a/claude/standard_comment.md b/claude/standard_comment.md new file mode 100644 index 0000000..5200015 --- /dev/null +++ b/claude/standard_comment.md @@ -0,0 +1,518 @@ +# 개발주석표준 가이드 + +## 📋 개요 + +이 문서는 CMS 프로젝트의 JavaDoc 주석 작성 표준을 정의합니다. 일관된 주석 스타일을 통해 코드의 가독성과 유지보수성을 향상시키는 것을 목표로 합니다. + +## 🎯 주석 작성 원칙 + +### 1. **기본 원칙** +- **명확성**: 코드의 의도와 동작을 명확하게 설명 +- **일관성**: 프로젝트 전체에서 동일한 스타일 적용 +- **완전성**: 모든 public 메서드와 클래스에 주석 작성 +- **최신성**: 코드 변경 시 주석도 함께 업데이트 + +### 2. **주석 대상** +- **필수**: public 클래스, 인터페이스, 메서드 +- **권장**: protected 메서드, 중요한 필드 +- **선택**: private 메서드 (복잡한 로직인 경우) + +## 📝 JavaDoc 기본 문법 + +### 1. **기본 구조** +```java +/** + * 클래스나 메서드의 간단한 설명 (첫 번째 문장) + * + *

상세한 설명이 필요한 경우 여기에 작성합니다.

+ * + * @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 +/** + *

단락을 구분할 때 사용합니다.

+ * 중요한 내용을 강조할 때 사용합니다. + * 이탤릭체로 표시할 때 사용합니다. + * method()와 같은 코드를 표시할 때 사용합니다. + */ +``` + +#### **목록 작성** +```java +/** + *

주요 기능:

+ *
    + *
  • 첫 번째 기능
  • + *
  • 두 번째 기능
  • + *
  • 세 번째 기능
  • + *
+ * + *

처리 과정:

+ *
    + *
  1. 첫 번째 단계
  2. + *
  3. 두 번째 단계
  4. + *
  5. 세 번째 단계
  6. + *
+ */ +``` + +#### **코드 블록** +```java +/** + *

사용 예시:

+ *
+ * AuthController controller = new AuthController();
+ * LoginRequest request = new LoginRequest("user", "password");
+ * ResponseEntity<LoginResponse> response = controller.login(request);
+ * 
+ */ +``` + +#### **테이블** +```java +/** + *

HTTP 상태 코드:

+ * + * + * + * + * + *
상태 코드설명
200성공
400잘못된 요청
401인증 실패
+ */ +``` + +### 3. **HTML 태그 사용 규칙** + +- **<와 >**: 제네릭 타입 표현 시 `<T>` 사용 +- **줄바꿈**: `
` 태그 사용 (가급적 `

` 태그 권장) +- **링크**: `{@link ClassName#methodName}` 사용 +- **인라인 코드**: `{@code variableName}` 또는 `` 사용 + +## 📋 클래스 주석 표준 + +### 1. **클래스 주석 템플릿** +```java +/** + * 클래스의 간단한 설명 + * + *

클래스의 상세한 설명과 목적을 여기에 작성합니다.

+ * + *

주요 기능:

+ *
    + *
  • 기능 1
  • + *
  • 기능 2
  • + *
  • 기능 3
  • + *
+ * + *

사용 예시:

+ *
+ * ClassName instance = new ClassName();
+ * instance.someMethod();
+ * 
+ * + *

주의사항:

+ *
    + *
  • 주의사항 1
  • + *
  • 주의사항 2
  • + *
+ * + * @author 작성자명 + * @version 1.0 + * @since 2024-01-01 + * + * @see 관련클래스1 + * @see 관련클래스2 + */ +public class ClassName { + // ... +} +``` + +### 2. **Controller 클래스 주석 예시** +```java +/** + * 사용자 관리 API 컨트롤러 + * + *

사용자 등록, 조회, 수정, 삭제 기능을 제공하는 REST API 컨트롤러입니다.

+ * + *

주요 기능:

+ *
    + *
  • 사용자 등록 및 인증
  • + *
  • 사용자 정보 조회 및 수정
  • + *
  • 사용자 권한 관리
  • + *
+ * + *

API 엔드포인트:

+ *
    + *
  • POST /api/users - 사용자 등록
  • + *
  • GET /api/users/{id} - 사용자 조회
  • + *
  • PUT /api/users/{id} - 사용자 수정
  • + *
  • DELETE /api/users/{id} - 사용자 삭제
  • + *
+ * + *

보안 고려사항:

+ *
    + *
  • 모든 엔드포인트는 인증이 필요합니다
  • + *
  • 개인정보 처리 시 데이터 마스킹 적용
  • + *
  • 입력값 검증 및 XSS 방지
  • + *
+ * + * @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 +/** + * 메서드의 간단한 설명 + * + *

메서드의 상세한 설명과 동작을 여기에 작성합니다.

+ * + *

처리 과정:

+ *
    + *
  1. 첫 번째 단계
  2. + *
  3. 두 번째 단계
  4. + *
  5. 세 번째 단계
  6. + *
+ * + *

주의사항:

+ *
    + *
  • 주의사항 1
  • + *
  • 주의사항 2
  • + *
+ * + * @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 +/** + * 사용자 로그인 처리 + * + *

사용자 ID와 비밀번호를 검증하여 JWT 토큰을 생성합니다.

+ * + *

처리 과정:

+ *
    + *
  1. 입력값 검증 (@Valid 어노테이션)
  2. + *
  3. 사용자 인증 정보 확인
  4. + *
  5. JWT 토큰 생성
  6. + *
  7. 사용자 세션 시작
  8. + *
  9. 로그인 메트릭 업데이트
  10. + *
+ * + *

보안 고려사항:

+ *
    + *
  • 비밀번호는 BCrypt로 암호화된 값과 비교
  • + *
  • 로그인 실패 시 상세 정보 노출 방지
  • + *
  • 로그인 시도 로그 기록
  • + *
+ * + * @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 login(@Valid @RequestBody LoginRequest request) { + // ... +} +``` + +## 📋 필드 주석 표준 + +### 1. **필드 주석 템플릿** +```java +/** + * 필드의 간단한 설명 + * + *

필드의 상세한 설명과 용도를 여기에 작성합니다.

+ * + *

주의사항:

+ *
    + *
  • 주의사항 1
  • + *
  • 주의사항 2
  • + *
+ * + * @since 1.0 + */ +private final ServiceType serviceName; +``` + +### 2. **의존성 주입 필드 예시** +```java +/** + * 인증 서비스 + * + *

사용자 로그인/로그아웃 처리 및 JWT 토큰 관리를 담당합니다.

+ * + *

주요 기능:

+ *
    + *
  • 사용자 인증 정보 검증
  • + *
  • JWT 토큰 생성 및 검증
  • + *
  • 로그인/로그아웃 처리
  • + *
+ * + * @see AuthService + * @since 1.0 + */ +private final AuthService authService; +``` + +## 📋 예외 클래스 주석 표준 + +```java +/** + * 사용자 인증 실패 예외 + * + *

로그인 시 사용자 ID 또는 비밀번호가 올바르지 않을 때 발생하는 예외입니다.

+ * + *

발생 상황:

+ *
    + *
  • 존재하지 않는 사용자 ID
  • + *
  • 잘못된 비밀번호
  • + *
  • 계정 잠금 상태
  • + *
+ * + *

처리 방법:

+ *
    + *
  • 사용자에게 일반적인 오류 메시지 표시
  • + *
  • 보안 로그에 상세 정보 기록
  • + *
  • 브루트 포스 공격 방지 로직 실행
  • + *
+ * + * @author cms-team + * @version 1.0 + * @since 2024-01-01 + * + * @see AuthService + * @see SecurityException + */ +public class InvalidCredentialsException extends RuntimeException { + // ... +} +``` + +## 📋 인터페이스 주석 표준 + +```java +/** + * 사용자 인증 서비스 인터페이스 + * + *

사용자 로그인, 로그아웃, 토큰 관리 등 인증 관련 기능을 정의합니다.

+ * + *

구현 클래스:

+ *
    + *
  • {@link AuthServiceImpl} - 기본 구현체
  • + *
  • {@link LdapAuthService} - LDAP 연동 구현체
  • + *
+ * + *

주요 기능:

+ *
    + *
  • 사용자 인증 및 토큰 생성
  • + *
  • 로그아웃 및 토큰 무효화
  • + *
  • 토큰 유효성 검증
  • + *
+ * + * @author cms-team + * @version 1.0 + * @since 2024-01-01 + * + * @see AuthServiceImpl + * @see TokenProvider + */ +public interface AuthService { + // ... +} +``` + +## 📋 Enum 주석 표준 + +```java +/** + * 사용자 역할 열거형 + * + *

시스템 사용자의 권한 수준을 정의합니다.

+ * + *

권한 계층:

+ *
    + *
  1. {@link #ADMIN} - 최고 관리자 권한
  2. + *
  3. {@link #MANAGER} - 관리자 권한
  4. + *
  5. {@link #USER} - 일반 사용자 권한
  6. + *
+ * + * @author cms-team + * @version 1.0 + * @since 2024-01-01 + */ +public enum Role { + + /** + * 시스템 관리자 + * + *

모든 시스템 기능에 대한 접근 권한을 가집니다.

+ * + *

주요 권한:

+ *
    + *
  • 사용자 관리
  • + *
  • 시스템 설정
  • + *
  • 모든 데이터 접근
  • + *
+ */ + ADMIN, + + /** + * 관리자 + * + *

제한된 관리 기능에 대한 접근 권한을 가집니다.

+ */ + MANAGER, + + /** + * 일반 사용자 + * + *

기본적인 시스템 기능에 대한 접근 권한을 가집니다.

+ */ + USER +} +``` + +## 📋 주석 작성 체크리스트 + +### ✅ **클래스 주석 체크리스트** +- [ ] 클래스의 목적과 역할 명시 +- [ ] 주요 기능 목록 작성 +- [ ] 사용 예시 코드 포함 +- [ ] 주의사항 및 제약사항 명시 +- [ ] @author, @version, @since 태그 작성 +- [ ] 관련 클래스 @see 태그 추가 + +### ✅ **메서드 주석 체크리스트** +- [ ] 메서드의 목적과 동작 설명 +- [ ] 처리 과정 단계별 설명 +- [ ] 모든 @param 태그 작성 +- [ ] @return 태그 작성 (void 메서드 제외) +- [ ] 가능한 예외 @throws 태그 작성 +- [ ] 보안 관련 주의사항 명시 +- [ ] 관련 메서드 @see 태그 추가 + +### ✅ **HTML 태그 체크리스트** +- [ ] 목록은 `
    `, `
      `, `
    1. ` 태그 사용 +- [ ] 강조는 `` 태그 사용 +- [ ] 단락 구분은 `

      ` 태그 사용 +- [ ] 코드는 `` 또는 `

      ` 태그 사용
      +- [ ] 제네릭 타입은 `<`, `>` 사용
      +
      +## 📋 도구 및 설정
      +
      +### 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)
      +
      +---
      +
      +> **💡 팁**: 이 가이드를 팀 내에서 공유하고, 코드 리뷰 시 주석 품질도 함께 검토하세요!
      \ No newline at end of file
      diff --git a/claude/standard_package_structure.md b/claude/standard_package_structure.md
      new file mode 100644
      index 0000000..81a4890
      --- /dev/null
      +++ b/claude/standard_package_structure.md
      @@ -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
      +
      +
      diff --git a/claude/standard_testcode.md b/claude/standard_testcode.md
      new file mode 100644
      index 0000000..1eec6d5
      --- /dev/null
      +++ b/claude/standard_testcode.md
      @@ -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()
      +```
      diff --git a/common/build.gradle b/common/build.gradle
      new file mode 100644
      index 0000000..f1d6d37
      --- /dev/null
      +++ b/common/build.gradle
      @@ -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'
      +}
      diff --git a/common/src/main/java/com/kt/event/common/dto/ApiResponse.java b/common/src/main/java/com/kt/event/common/dto/ApiResponse.java
      new file mode 100644
      index 0000000..e4605e1
      --- /dev/null
      +++ b/common/src/main/java/com/kt/event/common/dto/ApiResponse.java
      @@ -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  응답 데이터 타입
      + */
      +@Getter
      +@AllArgsConstructor(access = AccessLevel.PRIVATE)
      +@JsonInclude(JsonInclude.Include.NON_NULL)
      +public class ApiResponse {
      +
      +    /**
      +     * 성공 여부
      +     */
      +    private final boolean success;
      +
      +    /**
      +     * 응답 데이터
      +     */
      +    private final T data;
      +
      +    /**
      +     * 에러 코드 (실패 시)
      +     */
      +    private final String errorCode;
      +
      +    /**
      +     * 에러 메시지 (실패 시)
      +     */
      +    private final String message;
      +
      +    /**
      +     * 응답 시간
      +     */
      +    private final LocalDateTime timestamp;
      +
      +    /**
      +     * 성공 응답 생성 (데이터 포함)
      +     *
      +     * @param data 응답 데이터
      +     * @param   응답 데이터 타입
      +     * @return API 응답
      +     */
      +    public static  ApiResponse success(T data) {
      +        return new ApiResponse<>(true, data, null, null, LocalDateTime.now());
      +    }
      +
      +    /**
      +     * 성공 응답 생성 (데이터 없음)
      +     *
      +     * @param  응답 데이터 타입
      +     * @return API 응답
      +     */
      +    public static  ApiResponse success() {
      +        return new ApiResponse<>(true, null, null, null, LocalDateTime.now());
      +    }
      +
      +    /**
      +     * 실패 응답 생성
      +     *
      +     * @param errorCode 에러 코드
      +     * @param message   에러 메시지
      +     * @param        응답 데이터 타입
      +     * @return API 응답
      +     */
      +    public static  ApiResponse error(String errorCode, String message) {
      +        return new ApiResponse<>(false, null, errorCode, message, LocalDateTime.now());
      +    }
      +}
      diff --git a/common/src/main/java/com/kt/event/common/dto/ErrorResponse.java b/common/src/main/java/com/kt/event/common/dto/ErrorResponse.java
      new file mode 100644
      index 0000000..7e45d1a
      --- /dev/null
      +++ b/common/src/main/java/com/kt/event/common/dto/ErrorResponse.java
      @@ -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 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 fieldErrors) {
      +        return ErrorResponse.builder()
      +                .errorCode(errorCode)
      +                .message(message)
      +                .fieldErrors(fieldErrors)
      +                .build();
      +    }
      +}
      diff --git a/common/src/main/java/com/kt/event/common/dto/PageResponse.java b/common/src/main/java/com/kt/event/common/dto/PageResponse.java
      new file mode 100644
      index 0000000..f8e27dd
      --- /dev/null
      +++ b/common/src/main/java/com/kt/event/common/dto/PageResponse.java
      @@ -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  목록 아이템 타입
      + */
      +@Getter
      +@Builder
      +@NoArgsConstructor
      +@AllArgsConstructor
      +public class PageResponse {
      +
      +    /**
      +     * 목록 데이터
      +     */
      +    private List 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   목록 아이템 타입
      +     * @return PageResponse
      +     */
      +    public static  PageResponse of(org.springframework.data.domain.Page page) {
      +        return PageResponse.builder()
      +                .content(page.getContent())
      +                .page(page.getNumber())
      +                .size(page.getSize())
      +                .totalElements(page.getTotalElements())
      +                .totalPages(page.getTotalPages())
      +                .first(page.isFirst())
      +                .last(page.isLast())
      +                .build();
      +    }
      +}
      diff --git a/common/src/main/java/com/kt/event/common/entity/BaseTimeEntity.java b/common/src/main/java/com/kt/event/common/entity/BaseTimeEntity.java
      new file mode 100644
      index 0000000..d098181
      --- /dev/null
      +++ b/common/src/main/java/com/kt/event/common/entity/BaseTimeEntity.java
      @@ -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;
      +}
      diff --git a/common/src/main/java/com/kt/event/common/exception/BusinessException.java b/common/src/main/java/com/kt/event/common/exception/BusinessException.java
      new file mode 100644
      index 0000000..507543c
      --- /dev/null
      +++ b/common/src/main/java/com/kt/event/common/exception/BusinessException.java
      @@ -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;
      +    }
      +}
      diff --git a/common/src/main/java/com/kt/event/common/exception/ErrorCode.java b/common/src/main/java/com/kt/event/common/exception/ErrorCode.java
      new file mode 100644
      index 0000000..bd422c5
      --- /dev/null
      +++ b/common/src/main/java/com/kt/event/common/exception/ErrorCode.java
      @@ -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;
      +}
      diff --git a/common/src/main/java/com/kt/event/common/exception/GlobalExceptionHandler.java b/common/src/main/java/com/kt/event/common/exception/GlobalExceptionHandler.java
      new file mode 100644
      index 0000000..d382813
      --- /dev/null
      +++ b/common/src/main/java/com/kt/event/common/exception/GlobalExceptionHandler.java
      @@ -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 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 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 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 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 handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
      +        log.warn("Validation exception occurred: {}", ex.getMessage());
      +
      +        List 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 handleBindException(BindException ex) {
      +        log.warn("Bind exception occurred: {}", ex.getMessage());
      +
      +        List 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 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();
      +    }
      +}
      diff --git a/common/src/main/java/com/kt/event/common/exception/InfraException.java b/common/src/main/java/com/kt/event/common/exception/InfraException.java
      new file mode 100644
      index 0000000..674d732
      --- /dev/null
      +++ b/common/src/main/java/com/kt/event/common/exception/InfraException.java
      @@ -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;
      +    }
      +}
      diff --git a/common/src/main/java/com/kt/event/common/security/JwtAuthenticationFilter.java b/common/src/main/java/com/kt/event/common/security/JwtAuthenticationFilter.java
      new file mode 100644
      index 0000000..2f1f55b
      --- /dev/null
      +++ b/common/src/main/java/com/kt/event/common/security/JwtAuthenticationFilter.java
      @@ -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());
      +    }
      +}
      diff --git a/common/src/main/java/com/kt/event/common/security/JwtTokenProvider.java b/common/src/main/java/com/kt/event/common/security/JwtTokenProvider.java
      new file mode 100644
      index 0000000..d441f92
      --- /dev/null
      +++ b/common/src/main/java/com/kt/event/common/security/JwtTokenProvider.java
      @@ -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 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 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();
      +    }
      +}
      diff --git a/common/src/main/java/com/kt/event/common/security/UserPrincipal.java b/common/src/main/java/com/kt/event/common/security/UserPrincipal.java
      new file mode 100644
      index 0000000..695f7ea
      --- /dev/null
      +++ b/common/src/main/java/com/kt/event/common/security/UserPrincipal.java
      @@ -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 roles;
      +
      +    /**
      +     * Spring Security 권한 목록 반환
      +     *
      +     * @return 권한 목록
      +     */
      +    @Override
      +    public Collection 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);
      +    }
      +}
      diff --git a/common/src/main/java/com/kt/event/common/util/DateTimeUtil.java b/common/src/main/java/com/kt/event/common/util/DateTimeUtil.java
      new file mode 100644
      index 0000000..4c687b1
      --- /dev/null
      +++ b/common/src/main/java/com/kt/event/common/util/DateTimeUtil.java
      @@ -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());
      +    }
      +}
      diff --git a/common/src/main/java/com/kt/event/common/util/EncryptionUtil.java b/common/src/main/java/com/kt/event/common/util/EncryptionUtil.java
      new file mode 100644
      index 0000000..eeb9a78
      --- /dev/null
      +++ b/common/src/main/java/com/kt/event/common/util/EncryptionUtil.java
      @@ -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;
      +    }
      +}
      diff --git a/common/src/main/java/com/kt/event/common/util/StringUtil.java b/common/src/main/java/com/kt/event/common/util/StringUtil.java
      new file mode 100644
      index 0000000..ff34215
      --- /dev/null
      +++ b/common/src/main/java/com/kt/event/common/util/StringUtil.java
      @@ -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;
      +    }
      +}
      diff --git a/common/src/main/java/com/kt/event/common/util/ValidationUtil.java b/common/src/main/java/com/kt/event/common/util/ValidationUtil.java
      new file mode 100644
      index 0000000..090ae79
      --- /dev/null
      +++ b/common/src/main/java/com/kt/event/common/util/ValidationUtil.java
      @@ -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));
      +        }
      +    }
      +}
      diff --git a/content-service/build.gradle b/content-service/build.gradle
      new file mode 100644
      index 0000000..aa9be20
      --- /dev/null
      +++ b/content-service/build.gradle
      @@ -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'
      +}
      diff --git a/develop/dev/package-structure.md b/develop/dev/package-structure.md
      new file mode 100644
      index 0000000..2d5c56d
      --- /dev/null
      +++ b/develop/dev/package-structure.md
      @@ -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 (리액트킹)
      diff --git a/distribution-service/build.gradle b/distribution-service/build.gradle
      new file mode 100644
      index 0000000..f50172a
      --- /dev/null
      +++ b/distribution-service/build.gradle
      @@ -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'
      +}
      diff --git a/event-service/build.gradle b/event-service/build.gradle
      new file mode 100644
      index 0000000..0f2d88c
      --- /dev/null
      +++ b/event-service/build.gradle
      @@ -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'
      +}
      diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
      new file mode 100644
      index 0000000000000000000000000000000000000000..8bdaf60c75ab801e22807dde59e12a8735a34077
      GIT binary patch
      literal 45457
      zcma&NW0YlEwk;ePwr$(aux;D69T}N{9ky*d!_2U4+qUuIRNZ#Jck8}7U+vcB{`IjNZqX3eq5;s6ddAkU&5{L|^Ow`ym2B0m+K02+~Q)i807X3X94qi>j)C0e$=H
      zm31v`=T&y}ACuKx7G~yWSYncG=NFB>O2);i9EmJ(9jSamq?Crj$g~1l3m-4M7;BWn
      zau2S&sSA0b0Rhg>6YlVLQa;D#)1yw+eGs~36Q$}5?avIRne3TQZXb<^e}?T69w<9~
      zUmx1cG0uZ?Kd;Brd$$>r>&MrY*3$t^PWF1+J+G_xmpHW=>mly$<>~wHH+Bt3mzN7W
      zhR)g{_veH6>*KxLJ~~s{9HZm!UeC86d_>42NRqd$ev8zSMq4kt)q*>8kJ8p|^wuKx
      zq2Is_HJPoQ_apSoT?zJj7vXBp!xejBc^7F|zU0rhy%Ub*Dy#jJs!>1?CmJ-gulPVX
      zKit>RVmjL=G?>jytf^U@mfnC*1-7EVag@%ROu*#kA+)Rxq?MGK0v-dp^kM?nyMngb
      z_poL>GLThB7xAO*I7&?4^Nj`<@O@>&0M-QxIi
      zD@n}s%CYI4Be19C$lAb9Bbm6!R{&A;=yh=#fnFyb`s7S5W3?arZf?$khCwkGN!+GY~GT8-`!6pFr
      zbFBVEF`kAgtecfjJ`flN2Z!$$8}6hV>Tu;+rN%$X^t8fI>tXQnRn^$UhXO8Gu
      zt$~QON8`doV&{h}=2!}+xJKrNPcIQid?WuHUC-i%P^F(^z#XB`&&`xTK&L+i8a3a@
      zkV-Jy;AnyQ`N=&KONV_^-0WJA{b|c#_l=v!19U@hS~M-*ix16$r01GN3#naZ|DxY2
      z76nbjbOnFcx4bKbEoH~^=EikiZ)_*kOb>nW6>_vjf-UCf0uUy~QBb7~WfVO6qN@ns
      zz=XEG0s5Yp`mlmUad)8!(QDgIzY=OK%_hhPStbyYYd|~zDIc3J4
      zy9y%wZOW>}eG4&&;Z>vj&Mjg+>4gL!
      z(@oCTFf-I^54t=*4AhKRoE-0Ky=qg3XK2Mu!Bmw@z>y(|a#(6PcfbVTw-dUqyx4x4
      z3O#+hW1ANwSv-U+9otHE#U9T>(nWx>^7RO_aI>${jvfZQ{mUwiaxHau!H
      z0Nc}ucJu+bKux?l!dQ2QA(r@(5KZl(Or=U!=2K*8?D=ZT-IAcAX!5OI3w@`sF@$($
      zbDk0p&3X0P%B0aKdijO|s})70K&mk1DC|P##b=k@fcJ|lo@JNWRUc>KL?6dJpvtSUK
      zxR|w8Bo6K&y~Bd}gvuz*3z
      z@sPJr{(!?mi@okhudaM{t3gp9TJ!|@j4eO1C&=@h#|QLCUKLaKVL
      z!lls$%N&ZG7yO#jK?U>bJ+^F@K#A4d&Jz4boGmptagnK!Qu{Ob>%+60xRYK>iffd_
      z>6%0K)p!VwP$^@Apm%NrS6TpKJwj_Q=k~?4=_*NIe~eh_QtRaqX4t-rJAGYdB{pGq
      zSXX)-dR8mQ)X|;8@_=J6Dk7MfMp;x)^aZeCtScHs12t3vL+p-6!qhPkOM1OYQ
      z8YXW5tWp)Th(+$m7SnV_hNGKAP`JF4URkkNc@YV9}FK$9k
      zR&qgi$Cj#4bC1VK%#U)f%(+oQJ+EqvV{uAq1YG0riLvGxW@)m;*ayU-BSW61COFy0
      z(-l>GJqYl;*x1PnRZ(p3Lm}*
      zlkpWyCoYtg9pAZ5RU^%w=vN{3Y<6WImxj(*SCcJsFj?o6CZ~>cWW^foliM#qN#We{
      zwsL!u1$rzC1#4~bILZm*a!T{^kCci$XOJADm)P;y^%x5)#G#_!2uNp^S;cE`*ASCn;}H7pP^RRA
      z6lfXK(r4dy<_}R|(7%Lyo>QFP#s31E8zsYA${gSUykUV@?lyDNF=KhTeF^*lu7C*{
      zBCIjy;bIE;9inJ$IT8_jL%)Q{7itmncYlkf2`lHl(gTwD%LmEPo^gskydVxMd~Do`
      zO8EzF!yn!r|BEgPjhW#>g(unY#n}=#4J;3FD2ThN5LpO0tI2~pqICaFAGT%%;3Xx$
      z>~Ng(64xH-RV^Rj4=A_q1Ee8kcF}8HN{5kjYX0ADh}jq{q18x(pV!23pVsK5S}{M#p8|+LvfKx|_3;9{+6cu7%5o-+R@z>TlTft#kcJ`s2-j
      zUe4dgpInZU!<}aTGuwgdWJZ#8TPiV9QW<-o!ibBn&)?!ZDomECehvT7GSCRyF#VN2&5GShch9*}4p;8TX~cW*<#(
      zv-HmU7&+YUWO__NN3UbTFJ&^#3vxW4U9q5=&ORa+2M$4rskA4xV$rFSEYBGy55b{z
      z!)$_fYXiY?-GWDhGZXgTw}#ilrw=BiN(DGO*W7Vw(}
      zjUexksYLt_Nq?pl_nVa@c1W#edQKbT>VSN1NK?DulHkFpI-LXl7{;dl@z0#v?x%U&
      z8k8M1X6%TwR4BQ_eEWJASvMTy?@fQubBU__A_US567I-~;_VcX^NJ-E(ZPR^NASj1
      zVP!LIf8QKtcdeH#w6ak50At)e={eF_Ns6J2Iko6dn8Qwa6!NQHZMGsD
      zhzWeSFK<{hJV*!cIHxjgR+e#lkUHCss-j)$g
      zF}DyS531TUXKPPIoePo{yH%qEr-dLMOhv^sC&@9YI~uvl?rBp^A-57{aH_wLg0&a|UxKLlYZQ24fpb24Qjil`4OCyt0<1eu>5i1Acv
      zaZtQRF)Q;?Aw3idg;8Yg9Cb#)03?pQ@O*bCloG
      zC^|TnJl`GXN*8iI;Ql&_QIY0ik}rqB;cNZ-qagp=qmci9eScHsRXG$zRNdf4SleJ}
      z7||<#PCW~0>3u8PP=-DjNhD(^(B0AFF+(oKOiQyO5#v4nI|v_D5@c2;zE`}DK!%;H
      zUn|IZ6P;rl*5`E(srr6@-hpae!jW=-G
      zC<*R?RLwL;#+hxN4fJ!oP4fX`vC3&)o!#l4y@MrmbmL{t;VP%7tMA-&vju_L
      zhtHbOL4`O;h*5^e3F{b9(mDwY6JwL8w`oi28xOyj`pVo!75hngQDNg7^D$h4t&1p2
      ziWD_!ap3GM(S)?@UwWk=Szym^eDxSx3NaR}+l1~(@0car6tfP#sZRTb~w!WAS{+|SgUN3Tv`J4OMf
      z9ta_f>-`!`I@KA=CXj_J>CE7T`yGmej0}61sE(%nZa1WC_tV6odiysHA5gzfWN-`uXF46mhJGLpvNTBmx$!i
      zF67bAz~E|P{L6t1B+K|Cutp&h$fDjyq9JFy$7c_tB(Q$sR)#iMQH3{Og1AyD^lyQwX6#B|*ecl{-_;*B>~WSFInaRE_q6
      zpK#uCprrCb`MU^AGddA#SS{P7-OS9h%+1`~9v-s^{s8faWNpt*Pmk_ECjt(wrpr{C_xdAqR(@!ERTSs@F%^DkE@No}wqol~pS^e7>ksF_NhL0?6R4g`P-
      zk8lMrVir~b(KY+hk5LQngwm`ZQT5t1^7AzHB2My6o)_ejR0{VxU<*r-Gld`l6tfA`
      zKoj%x9=>Ce|1R|1*aC}|F0R32^KMLAHN}MA<8NNaZ^j?HKxSwxz`N2hK8lEb{jE0&
      zg4G_6F@#NyDN?=i@=)eidKhlg!nQoA{`PgaH{;t|M#5z}a`u?^gy{5L~I2smLR
      z*4RmNxHqf9>D>sXSemHK!h4uPwMRb+W`6F>Q6j@isZ>-F=)B2*sTCD9A^jjUy)hjAw71B&$u}R(^R;
      zY9H3k8$|ounk>)EOi_;JAKV8U8ICSD@NrqB!&=)Ah_5hzp?L9Sw@c>>#f_kUhhm=p
      z1jRz8X7)~|VwO(MF3PS(|CL++1n|KT3*dhGjg!t_vR|8Yg($
      z+$S$K=J`K6eG#^(J54=4&X#+7Car=_aeAuC>dHE+%v9HFu>r%ry|rwkrO-XPhR_#K
      zS{2Unv!_CvS7}Mb6IIT$D4Gq5v$Pvi5nbYB+1Yc&RY;3;XDihlvhhIG6AhAHsBYsm
      zK@MgSzs~y|+f|j-lsXKT0(%E2SkEb)p+|EkV5w8=F^!r1&0#0^tGhf9yPZ)iLJ^
      zIXOg)HW_Vt{|r0W(`NmMLF$?3ZQpq+^OtjR-DaVLHpz%1+GZ7QGFA?(BIqBlVQ;)k
      zu)oO|KG&++gD9oL7aK4Zwjwi~5jqk6+w%{T$1`2>3Znh=OFg|kZ
      z>1cn>CZ>P|iQO%-Pic8wE9c*e%=3qNYKJ+z1{2=QHHFe=u3rqCWNhV_N*qzneN8A5
      zj`1Ir7-5`33rjDmyIGvTx4K3qsks(I(;Kgmn%p#p3K
      zn8r9H8kQu+n@D$<#RZtmp$*T4B&QvT{K&qx(?>t@mX%3Lh}sr?gI#vNi=vV5d(D<=Cp5-y!a{~&y|Uz*PU{qe
      zI7g}mt!txT)U(q<+Xg_sSY%1wVHy;Dv3uze
      zJ>BIdSB2a|aK+?o63lR8QZhhP)KyQvV`J3)5q^j1-G}fq=E4&){*&hiam>ssYm!ya
      z#PsY0F}vT#twY1mXkGYmdd%_Uh12x0*6lN-HS-&5XWbJ^%su)-vffvKZ%rvLHVA<;
      zJP=h13;x?$v30`T)M)htph`=if#r#O5iC^ZHeXc6J8gewn
      zL!49!)>3I-q6XOZRG0=zjyQc`tl|RFCR}f-sNtc)I^~?Vv2t7tZZHvgU2Mfc9$LqG
      z!(iz&xb=q#4otDBO4p)KtEq}8NaIVcL3&pbvm@0Kk-~C@y3I{K61VDF_=}c`VN)3P
      z+{nBy^;=1N`A=xH$01dPesY_na*zrcnssA}Ix60C=sWg9EY=2>-yH&iqhhm28qq9Z
      z;}znS4ktr40Lf~G@6D5QxW&?q^R|=1+h!1%G4LhQs54c2Wo~4%
      zCA||d==lv2bP=9%hd0Dw_a$cz9kk)(Vo}NpSPx!vnV*0Bh9$CYP~ia#lEoLRJ8D#5
      zSJS?}ABn1LX>8(Mfg&eefX*c0I5bf4<`gCy6VC{e>$&BbwFSJ0CgVa;0-U7=F81R+
      zUmzz&c;H|%G&mSQ0K16Vosh?sjJW(Gp+1Yw+Yf4qOi|BFVbMrdO6~-U8Hr|L@LHeZ
      z0ALmXHsVm137&xnt#yYF$H%&AU!lf{W436Wq87nC16b%)p?r
      z70Wua59%7Quak50G7m3lOjtvcS>5}YL_~?Pti_pfAfQ!OxkX$arHRg|VrNx>R_Xyi
      z`N|Y7KV`z3(ZB2wT9{Dl8mtl
      zg^UOBv~k>Z(E)O>Z;~Z)W&4FhzwiPjUHE9&T#nlM)@hvAZL>cha-<
      zQ8_RL#P1?&2Qhk#c9fK9+xM#AneqzE-g(>chLp_Q2Xh$=MAsW
      z2ScEKr+YOD*R~mzy{bOJjs;X2y1}DVFZi7d_df^~((5a2%p%^4cf>vM_4Sn@@ssVJ
      z9ChGhs
      zbanJ+h74)3tWOviXI|v!=HU2mE%3Th$Mpx&lEeGFEBWRy8ogJY`BCXj@7s~bjrOY!
      z4nIU5S>_NrpN}|waZBC)$6ST8x91U2n?FGV8lS{&LFhHbuHU?SVU{p7yFSP_f#Eyh
      zJhI@o9lAeEwbZYC=~<(FZ$sJx^6j@gtl{yTOAz`Gj!Ab^y})eG&`Qt2cXdog2^~oOH^K@oHcE(L;wu2QiMv
      zJuGdhNd+H{t#Tjd<$PknMSfbI>L1YIdZ+uFf*Z=BEM)UPG3oDFe@8roB0h(*XAqRc
      zoxw`wQD@^nxGFxQXN9@GpkLqd?9@(_ZRS@EFRCO8J5{iuNAQO=!Lo5cCsPtt4=1qZN8z`EA2{ge@SjTyhiJE%ttk{~`SEl%5>s=9E~dUW0uws>&~3PwXJ!f>ShhP~U9dLvE8ElNt3g(6-d
      zdgtD;rgd^>1URef?*=8BkE&+HmzXD-4w61(p6o~Oxm`XexcHmnR*B~5a|u-Qz$2lf
      zXc$p91T~E4psJxhf^rdR!b_XmNv*?}!PK9@-asDTaen;p{Rxsa=1E}4kZ*}yQPoT0
      zvM}t!CpJvk<`m~^$^1C^o1yM(BzY-Wz2q7C^+wfg-?}1bF?5Hk?S{^#U%wX4&lv0j
      zkNb)byI+nql(&65xV?_L<0tj!KMHX8Hmh2(udEG>@OPQ}KPtdwEuEb$?acp~yT1&r
      z|7YU<(v!0as6Xff5^XbKQIR&MpjSE)pmub+ECMZzn7c!|hnm_Rl&H_oXWU2!h7hhf
      zo&-@cLkZr#eNgUN9>b=QLE1V^b`($EX3RQIyg#45A^=G!jMY`qJ
      z8qjZ$*-V|?y0=zIM>!2q!Gi*t4J5Otr^OT3XzQ_GjATc(*eM
      zqllux#QtHhc>YtnswBNiS^t(dTDn|RYSI%i%-|sv1wh&|9jfeyx|IHowW)6uZWR<%n8I}6NidBm
      zJ>P7#5m`gnXLu;?7jQZ!PwA80d|AS*+mtrU6z+lzms6^vc4)6Zf+$l+Lk3AsEK7`_
      zQ9LsS!2o#-pK+V`g#3hC$6*Z~PD%cwtOT8;7K3O=gHdC=WLK-i_DjPO#WN__#YLX|Akw3LnqUJUw8&7pUR;K
      zqJ98?rKMXE(tnmT`#080w%l1bGno7wXHQbl?QFU=GoK@d!Ov=IgsdHd-iIs4ahcgSj(L@F96=LKZ
      zeb5cJOVlcKBudawbz~AYk@!^p+E=dT^UhPE`96Q5J~cT-8^tp`J43nLbFD*Nf!w;6
      zs>V!5#;?bwYflf0HtFvX_6_jh4GEpa0_s8UUe02@%$w^ym&%wI5_APD?9S4r9O@4m
      zq^Z5Br8#K)y@z*fo08@XCs;wKBydn+60ks4Z>_+PFD+PVTGNPFPg-V-|``!0l|XrTyUYA@mY?#bJYvD>jX&$o9VAbo?>?#Z^c+Y4Dl
      zXU9k`s74Sb$OYh7^B|SAVVz*jEW&GWG^cP<_!hW+#Qp|4791Od=HJcesFo?$#0eWD
      z8!Ib_>H1WQE}shsQiUNk!uWOyAzX>r(-N7;+(O333_ES7*^6z4{`p&O*q8xk{0xy@
      zB&9LkW_B}_Y&?pXP-OYNJfqEWUVAPBk)pTP^;f+75Wa(W>^UO_*J05f1k{
      zd-}j!4m@q#CaC6mLsQHD1&7{tJ*}LtE{g9LB>sIT7)l^ucm8&+L0=g1E_6#KHfS>A_Z?;pFP96*nX=1&ejZ+XvZ=ML`@oVu>s^WIjn^SY}n
      zboeP%`O9|dhzvnw%?wAsCw*lvVcv%bmO5M4cas>b%FHd;A6Z%Ej%;jgPuvL$nk=VQ=$-OTwslYg
      zJQtDS)|qkIs%)K$+r*_NTke8%Rv&w^v;|Ajh5QXaVh}ugccP}3E^(oGC5VO*4`&Q0
      z&)z$6i_aKI*CqVBglCxo#9>eOkDD!voCJRFkNolvA2N&SAp^4<8{Y;#Kr5740
      za|G`dYGE!9NGU3Ge6C)YByb6Wy#}EN`Ao#R!$LQ&SM#hifEvZp>1PAX{CSLqD4IuO
      z4#N4AjMj5t2|!yTMrl5r)`_{V6DlqVeTwo|tq4MHLZdZc5;=v9*ibc;IGYh+G|~PB
      zx2}BAv6p$}?7YpvhqHu7L;~)~Oe^Y)O(G(PJQB<&2AhwMw!(2#AHhjSsBYUd8MDeM
      z+UXXyV@@cQ`w}mJ2PGs>=jHE{%i44QsPPh(=yorg>jHic+K+S*q3{th6Ik^j=@%xo
      zXfa9L_<|xTL@UZ?4H`$vt9MOF`|*z&)!mECiuenMW`Eo2VE#|2>2ET7th6+VAmU(o
      zq$Fz^TUB*@a<}kr6I>r;6`l%8NWtVtkE?}Q<<$BIm*6Z(1EhDtA29O%5d1$0q#C&f
      zFhFrrss{hOsISjYGDOP*)j&zZUf9`xvR8G)gwxE$HtmKsezo`{Ta~V5u+J&Tg+{bh
      zhLlNbdzJNF6m$wZNblWNbP6>dTWhngsu=J{);9D|PPJ96aqM4Lc?&6H-J1W15uIpQ
      ziO{&pEc2}-cqw+)w$`p(k(_yRpmbp-Xcd`*;Y$X=o(v2K+ISW)B1(ZnkV`g4rHQ=s
      z+J?F9&(||&86pi}snC07Lxi1ja>6kvnut;|Ql3fD)%k+ASe^S|lN69+Ek3UwsSx=2EH)t}K>~
      z`Mz-SSVH29@DWyl`ChuGAkG>J;>8ZmLhm>uEmUvLqar~vK3lS;4s<{+ehMsFXM(l-
      zRt=HT>h9G)JS*&(dbXrM&z;)66C=o{=+^}ciyt8|@e$Y}IREAyd_!2|CqTg=eu}yG
      z@sI9T;Tjix*%v)c{4G84|0j@8wX^Iig_JsPU|T%(J&KtJ>V
      zsAR+dcmyT5k&&G{!)VXN`oRS{n;3qd`BgAE9r?%AHy_Gf8>$&X$=>YD7M911?<{qX
      zkJ;IOfY$nHdy@kKk_+X%g3`T(v|jS;>`pz`?>fqMZ>Fvbx1W=8nvtuve&y`JBfvU~
      zr+5pF!`$`TUVsx3^<)48&+XT92U0DS|^X6FwSa-8yviRkZ*@Wu|c*lX!m?8&$0~4T!DB0@)n}ey+ew}T1U>|fH3=W5I!=nfoNs~OkzTY7^x^G&h>M7ewZqmZ=EL0}3#ikWg+(wuoA{7hm|7eJz
      zNz78l-K81tP16rai+fvXtspOhN-%*RY3IzMX6~8k9oFlXWgICx9dp;`)?Toz`fxV@&m8<
      z{lzWJG_Y(N1nOox>yG^uDr}kDX_f`lMbtxfP`VD@l$HR*B(sDeE(+T831V-3d3$+%
      zDKzKnK_W(gLwAK{Saa2}zaV?1QmcuhDu$)#;*4gU(l&rgNXB^WcMuuTki*rt>|M)D
      zoI;l$FTWIUp}euuZjDidpVw6AS-3dal2TJJaVMGj#CROWr|;^?q>PAo2k^u-27t~v
      zCv10IL~E)o*|QgdM!GJTaT&|A?oW)m9qk2{=y*7qb@BIAlYgDIe)k(qVH@)#xx6%7
      z@)l%aJwz5Joc84Q2jRp71d;=a@NkjSdMyN%L6OevML^(L0_msbef>ewImS=+DgrTk
      z4ON%Y$mYgcZ^44O*;ctP>_7=}=pslsu>~<-bw=C(jeQ-X`kUo^BS&JDHy%#L32Cj_
      zXRzDCfCXKXxGSW9yOGMMOYqPKnU
      zTF6gDj47!7PoL%z?*{1eyc2IVF*RXX?mj1RS}++hZg_%b@6&PdO)VzvmkXxJ*O7H}
      z6I7XmJqwX3<>z%M@W|GD%(X|VOZ7A+=@~MxMt8zhDw`yz?V>H%C0&VY+ZZ>9AoDVZeO1c~z$r~!H
      zA`N_9p`X?z>jm!-leBjW1R13_i2(0&aEY2$l_+-n#powuRO;n2Fr#%jp{+3@`h$c<
      zcFMr;18Z`UN#spXv+3Ks_V_tSZ1!FY7H(tdAk!v}SkoL9RPYSD3O5w>A3%>7J+C-R
      zZfDmu=9<1w1CV8rCMEm{qyErCUaA3Q
      zRYYw_z!W7UDEK)8DF}la9`}8z*?N32-6c-Bwx^Jf#Muwc67sVW24
      zJ4nab%>_EM8wPhL=MAN)xx1tozAl
      zmhXN;*-X%)s>(L=Q@vm$qmuScku>PV(W_x-6E?SFRjSk)A1xVqnml_92fbj0m};UC
      zcV}lRW-r*wY106|sshV`n#RN{)D9=!>XVH0vMh>od=9!1(U+sWF%#B|eeaKI9RpaW
      z8Ol_wAJX%j0h5fkvF)WMZ1}?#R(n-OT0CtwsL)|qk;*(!a)5a5ku2nCR9=E*iOZ`9
      zy4>LHKt-BgHL@R9CBSG!v4wK
      zvjF8DORRva)@>nshE~VM@i2c$PKw?3nz(6-iVde;-S~~7R<5r2t$0U8k2_<5C0!$j
      zQg#lsRYtI#Q1YRs(-%(;F-K7oY~!m&zhuU4LL}>jbLC>B`tk8onRRcmIm{{0cpkD|o@Ixu#x9Wm5J)3oFkbfi62BX8IX1}VTe#{C(d@H|#gy5#Sa#t>sH@8v1h8XFgNGs?)tyF_S^ueJX_-1%+LR`1X@C
      zS3Oc)o)!8Z9!u9d!35YD^!aXtH;IMNzPp`NS|EcdaQw~<;z`lmkg
      zE|tQRF7!S!UCsbag%XlQZXmzAOSs=
      zIUjgY2jcN9`xA6mzG{m|Zw=3kZC4@XY=Bj%k8%D&iadvne$pYNfZI$^2BAB|-MnZW
      zU4U?*qE3`ZDx-bH})>wz~)a
      z_SWM!E=-BS#wdrfh;EfPNOS*9!;*+wp-zDthj<>P0a2n?$xfe;YmX~5a;(mNV5nKx
      zYR86%WtAPsOMIg&*o9uUfD!v&4(mpS6P`bFohPP<&^fZzfA|SvVzPQgbtwwM>IO>Z
      z75ejU$1_SB1tn!Y-9tajZ~F=Fa~{cnj%Y|$;%z6fJV1XC0080f)Pj|87j142q6`i>#)BCIi+x&jAH9|H#iMvS~?w;&E`y
      zoarJ)+5HWmZ{&OqlzbdQU=SE3GKmnQq
      zI{h6f$C@}Mbqf#JDsJyi&7M0O2ORXtEB`#cZ;#AcB
      zkao0`&|iH8XKvZ_RH|VaK@tAGKMq9x{sdd%p-o`!cJzmd&hb86N!KKxp($2G?#(#BJn5%hF0(^`=
      z2qRg5?82({w-HyjbffI>eqUXavp&|D8(I6zMOfM}0;h%*D_Dr@+%TaWpIEQX3*$vQ
      z8_)wkNMDi{rW`L+`yN^J*Gt(l7PExu3_hrntgbW0s}7m~1K=(mFymoU87#{|t*fJ?w8&>Uh
      zcS$Ny$HNRbT!UCFldTSp2*;%EoW+yhJD8<3FUt8@XSBeJM2dSEz+5}BWmBvdYK(OA
      zlm`nDDsjKED{$v*jl(&)H7-+*#jWI)W|_X)!em1qpjS_CBbAiyMt;tx*+0P%*m&v<
      zxV9rlslu8#cS!of#^1O$(ds8aviMFiT`6W+FzMHW{YS+SieJ^?TQb%NT&pasw^kbc
      znd`=%(bebvrNx3#7vq@vAX-G`4|>cY0svIXopH02{v;GZ{wJM#psz4!m8(IZu<)9D
      zqR~U7@cz-6H{724_*}-DWwE8Sk+dYBb*O-=c
      z+wdchFcm6$$^Z0_qGnv0P`)h1=D$_eg8!2-|7Y;o*c)4ax!Me0*EVcioh{wI#!qcb
      z1&xhOotXMrlo7P6{+C8m;E#4*=8(2y!r0d<6
      zKi$d2X;O*zS(&Xiz_?|`ympxITf|&M%^WHp=694g6W@k+BL_T1JtSYX0OZ}o%?Pzu
      zJ{%P8A$uq?4F!NWGtq>_GLK3*c6dIcGH)??L`9Av&0k$A*14ED9!e9z_SZd3OH6ER
      zg%5^)3^gw;4DFw(RC;~r`bPJOR}H}?2n60=g4ESUTud$bkBLPyI#4#Ye{5x3@Yw<*
      z;P5Up>Yn(QdP#momCf=kOzZYzg9E330=67WOPbCMm2-T1%8{=or9L8+HGL{%83lri
      zODB;Y|LS`@mn#Wmez7t6-x`a2{}U9hE|xY7|BVcFCqoAZQzsEi=dYHB
      z(bqG3J5?teVSBqTj{aiqe<9}}CEc$HdsJSMp#I;4(EXRy_k|Y8X#5hwkqAaIGKARF
      zX?$|UO{>3-FU;IlFi80O^t+WMNw4So2nsg}^T1`-Ox&C%Gn_AZ-49Nir=2oYX6
      z`uVke@L5PVh)YsvAgFMZfKi{DuSgWnlAaag{RN6t6oLm6{4)H~4xg#Xfcq-e@ALk&
      z@UP4;uCe(Yjg4jaJZ4pu*+*?4#+XCi%sTrqaT*jNY7|WQ!oR;S8nt)cI27W$Sz!94
      z01zoTW`C*P3E?1@6thPe(QpIue$A54gp#C7pmfwRj}GxIw$!!qQetn`nvuwIvMBQ;
      zfF8K-D~O4aJKmLbNRN1?AZsWY&rp?iy`LP^3KT0UcGNy=Z@7qVM(#5u#Du#w>a&Bs
      z@f#zU{wk&5n!YF%D11S9*CyaI8%^oX=vq$Ei9cL1&kvv9|8vZD;Mhs1&slm`$A%ED
      zvz6SQ8aty~`IYp2Xd~G$z%Jf4zwVPKkCtqObrnc2gHKj^jg&-NH|xdNK_;+2d4ZXw
      zN9j)`jcp7y65&6P@}LsD_OLSi(#GW#hC*qF5KpmeXuQDNS%ZYpuW<;JI<>P6ln!p@
      z>KPAM>8^cX|2!n@tV=P)f2Euv?!}UM`^RJ~nTT@W>KC2{{}xXS{}WH{|3najkiEUj
      z7l;fUWDPCtzQ$?(f)6RvzW~Tqan$bXibe%dv}**BqY!d4J?`1iX`-iy8nPo$s4^mQ
      z5+@=3xuZAl#KoDF*%>bJ4UrEB2EE8m7sQn!r7Z-ggig`?yy`p~3;&NFukc$`_>?}a
      z?LMo2LV^n>m!fv^HKKRrDn|2|zk?~S6i|xOHt%K(*TGWkq3{~|9+(G3M-L=;U-YRa
      zp{kIXZ8P!koE;BN2A;nBx!={yg4v=-xGOMC#~MA07zfR)yZtSF_2W^pDLcXg->*WD
      zY7Sz5%<_k+lbS^`y)=vX|KaN!gEMQob|(`%nP6huwr$%^?%0^vwr$(CZQD*Jc5?E(
      zb-q9E`OfoWSJ$rUs$ILfSFg3Mb*-!Ozgaz^%7ZkX@=3km0G;?+e?FQT_l5A9vKr<>
      z_CoemDo@6YIyl57l*gnJ^7+8xLW5oEGzjLv2P8vj*Q%O1^KOfrsC6eHvk{+$BMLGu
      z%goP8UY?J7Lj=@jcI$4{m2Sw?1E%_0C7M$lj}w{E#hM4%3QX|;tH6>RJf-TI_1A0w
      z@KcTEFx(@uitbo?UMMqUaSgt=n`Bu*;$4@cbg9JIS})3#2T;B7S

      Z?HZkSa`=MM?n)?|XcM)@e1qmzJ$_4K^?-``~Oi&38`2}sjmP?kK z$yT)K(UU3fJID@~3R;)fU%k%9*4f>oq`y>#t90$(y*sZTzWcW$H=Xv|%^u^?2*n)Csx;35O0v7Nab-REgxDZNf5`cI69k$` zx(&pP6zVxlK5Apn5hAhui}b)(IwZD}D?&)_{_yTL7QgTxL|_X!o@A`)P#!%t9al+# zLD(Rr+?HHJEOl545~m1)cwawqY>cf~9hu-L`crI^5p~-9Mgp9{U5V&dJSwolnl_CM zwAMM1Tl$D@>v?LN2PLe0IZrQL1M zcA%i@Lc)URretFJhtw7IaZXYC6#8slg|*HfUF2Z5{3R_tw)YQ94=dprT`SFAvHB+7 z)-Hd1yE8LB1S+4H7iy$5XruPxq6pc_V)+VO{seA8^`o5{T5s<8bJ`>I3&m%R4cm1S z`hoNk%_=KU2;+#$Y!x7L%|;!Nxbu~TKw?zSP(?H0_b8Qqj4EPrb@~IE`~^#~C%D9k zvJ=ERh`xLgUwvusQbo6S=I5T+?lITYsVyeCCwT9R>DwQa&$e(PxF<}RpLD9Vm2vV# zI#M%ksVNFG1U?;QR{Kx2sf>@y$7sop6SOnBC4sv8S0-`gEt0eHJ{`QSW(_06Uwg*~ zIw}1dZ9c=K$a$N?;j`s3>)AqC$`ld?bOs^^stmYmsWA$XEVhUtGlx&OyziN1~2 z)s5fD(d@gq7htIGX!GCxKT=8aAOHW&DAP=$MpZ)SpeEZhk83}K) z0(Uv)+&pE?|4)D2PX4r6gOGHDY}$8FSg$3eDb*nEVmkFQ#lFpcH~IPeatiH3nPTkP z*xDN7l}r2GM9jwSsl=*!547nRPCS0pb;uE#myTqV+=se>bU=#e)f2}wCp%f-cIrh`FHA$2`monVy?qvJ~o2B6I7IE28bCY4=c#^){*essLG zXUH50W&SWmi{RIG9G^p;PohSPtC}djjXSoC)kyA8`o+L}SjE{i?%;Vh=h;QC{s`T7 zLmmHCr8F}#^O8_~lR)^clv$mMe`e*{MW#Sxd`rDckCnFBo9sC*vw2)dA9Q3lUi*Fy zgDsLt`xt|7G=O6+ms=`_FpD4}37uvelFLc^?snyNUNxbdSj2+Mpv<67NR{(mdtSDNJ3gSD@>gX_7S5 zCD)JP5Hnv!llc-9fwG=4@?=%qu~(4j>YXtgz%gZ#+A9i^H!_R!MxWlFsH(ClP3dU} za&`m(cM0xebj&S170&KLU%39I+XVWOJ_1XpF^ip}3|y()Fn5P@$pP5rvtiEK6w&+w z7uqIxZUj$#qN|<_LFhE@@SAdBy8)xTu>>`xC>VYU@d}E)^sb9k0}YKr=B8-5M?3}d z7&LqQWQ`a&=ihhANxe3^YT>yj&72x#X4NXRTc#+sk;K z=VUp#I(YIRO`g7#;5))p=y=MQ54JWeS(A^$qt>Y#unGRT$0BG=rI(tr>YqSxNm+-x z6n;-y8B>#FnhZX#mhVOT30baJ{47E^j-I6EOp;am;FvTlYRR2_?CjCWY+ypoUD-2S zqnFH6FS+q$H$^7>>(nd^WE+?Zn#@HU3#t|&=JnEDgIU+;CgS+krs+Y8vMo6U zHVkPoReZ-Di3z!xdBu#aW1f{8sC)etjN90`2|Y@{2=Os`(XLL9+ z1$_PE$GgTQrVx`^sx=Y(_y-SvquMF5<`9C=vM52+e+-r=g?D z+E|97MyoaK5M^n1(mnWeBpgtMs8fXOu4Q$89C5q4@YY0H{N47VANA1}M2e zspor6LdndC=kEvxs3YrPGbc;`q}|zeg`f;t3-8na)dGdZ9&d(n{|%mNaHaKJOA~@8 zgP?nkzV-=ULb)L3r`p)vj4<702a5h~Y%byo4)lh?rtu1YXYOY+qyTwzs!59I zL}XLe=q$e<+Wm7tvB$n88#a9LzBkgHhfT<&i#%e*y|}@I z!N~_)vodngB7%CI2pJT*{GX|cI5y>ZBN)}mezK~fFv@$*L`84rb0)V=PvQ2KN}3lTpT@$>a=CP?kcC0S_^PZ#Vd9#CF4 zP&`6{Y!hd^qmL!zr#F~FB0yag-V;qrmW9Jnq~-l>Sg$b%%TpO}{Q+*Pd-@n2suVh_ zSYP->P@# z&gQ^f{?}m(u5B9xqo63pUvDsJDQJi5B~ak+J{tX8$oL!_{Dh zL@=XFzWb+83H3wPbTic+osVp&~UoW3SqK0#P6+BKbOzK65tz)-@AW#g}Ew+pE3@ zVbdJkJ}EM@-Ghxp_4a)|asEk* z5)mMI&EK~BI^aaTMRl)oPJRH^Ld{;1FC&#pS`gh;l3Y;DF*`pR%OSz8U@B@zJxPNX zwyP_&8GsQ7^eYyUO3FEE|9~I~X8;{WTN=DJW0$2OH=3-!KZG=X6TH?>URr(A0l@+d zj^B9G-ACel;yYGZc}G`w9sR$Mo{tzE7&%XKuW$|u7DM<6_z}L>I{o`(=!*1 z{5?1p3F^aBONr6Ws!6@G?XRxJxXt_6b}2%Bp=0Iv5ngnpU^P+?(?O0hKwAK z*|wAisG&8&Td1XY+6qI~-5&+4DE2p|Dj8@do;!40o)F)QuoeUY;*I&QZ0*4?u)$s`VTkNl1WG`}g@J_i zjjmv4L%g&>@U9_|l>8^CN}`@4<D2aMN&?XXD-HNnsVM`irjv$ z^YVNUx3r1{-o6waQfDp=OG^P+vd;qEvd{UUYc;gF0UwaeacXkw32He^qyoYHjZeFS zo(#C9#&NEdFRcFrj7Q{CJgbmDejNS!H%aF6?;|KJQn_*Ps3pkq9yE~G{0wIS*mo0XIEYH zzIiJ>rbmD;sGXt#jlx7AXSGGcjty)5z5lTGp|M#5DCl0q0|~pNQ%1dP!-1>_7^BA~ zwu+uumJmTCcd)r|Hc)uWm7S!+Dw4;E|5+bwPb4i17Ued>NklnnsG+A{T-&}0=sLM- zY;sA9v@YH>b9#c$Vg{j@+>UULBX=jtu~N^%Y#BB5)pB|$?0Mf7msMD<7eACoP1(XY zPO^h5Brvhn$%(0JSo3KFwEPV&dz8(P41o=mo7G~A*P6wLJ@-#|_A z7>k~4&lbqyP1!la!qmhFBfIfT?nIHQ0j2WlohXk^sZ`?8-vwEwV0~uu{RDE^0yfl$ znua{^`VTZ)-h#ch_6^e2{VPaE@o&55|3dx$z_b6gbqduXJ(Lz(zq&ZbJ6qA4Ac4RT zhJO4KBLN!t;h(eW(?cZJw^swf8lP@tWMZ8GD)zg)siA3!2EJYI(j>WI$=pK!mo!Ry z?q&YkTIbTTr<>=}+N8C_EAR0XQL2&O{nNAXb?33iwo8{M``rUHJgnk z8KgZzZLFf|(O6oeugsm<;5m~4N$2Jm5#dph*@TgXC2_k&d%TG0LPY=Fw)=gf(hy9QmY*D6jCAiq44 zo-k2C+?3*+Wu7xm1w*LEAl`Vsq(sYPUMw|MiXrW)92>rVOAse5Pmx^OSi{y%EwPAE zx|csvE{U3c{vA>@;>xcjdCW15pE31F3aoIBsz@OQRvi%_MMfgar2j3Ob`9e@gLQk# zlzznEHgr|Ols%f*a+B-0klD`czi@RWGPPpR1tE@GB|nwe`td1OwG#OjGlTH zfT#^r?%3Ocp^U0F8Kekck6-Vg2gWs|sD_DTJ%2TR<5H3a$}B4ZYpP=p)oAoHxr8I! z1SYJ~v-iP&mNm{ra7!KP^KVpkER>-HFvq*>eG4J#kz1|eu;=~u2|>}TE_5nv2=d!0 z3P~?@blSo^uumuEt{lBsGcx{_IXPO8s01+7DP^yt&>k;<5(NRrF|To2h7hTWBFQ_A z+;?Q$o5L|LlIB>PH(4j)j3`JIb1xA_C@HRFnPnlg{zGO|-RO7Xn}!*2U=Z2V?{5Al z9+iL+n^_T~6Uu{law`R&fFadSVi}da8G>|>D<{(#vi{OU;}1ZnfXy8=etC7)Ae<2S zAlI`&=HkNiHhT0|tQztSLNsRR6v8bmf&$6CI|7b8V4kyJ{=pG#h{1sVeC28&Ho%Fh zwo_FIS}ST-2OF6jNQ$(pjrq)P)@sie#tigN1zSclxJLb-O9V|trp^G8<1rpsj8@+$ z2y27iiM>H8kfd%AMlK|9C>Lkvfs9iSk>k2}tCFlqF~Z_>-uWVQDd$5{3sM%2$du9; z*ukNSo}~@w@DPF)_vS^VaZ)7Mk&8ijX2hNhKom$#PM%bzSA-s$ z0O!broj`!Nuk)Qcp3(>dL|5om#XMx2RUSDMDY9#1|+~fxwP}1I4iYy4j$CGx3jD&eKhf%z`Jn z7mD!y6`nVq%&Q#5yqG`|+e~1$Zkgu!O(~~pWSDTw2^va3u!DOMVRQ8ycq)sk&H%vb z;$a`3gp74~I@swI!ILOkzVK3G&SdTcVe~RzN<+z`u(BY=yuwez{#T3a_83)8>2!X?`^02zVjqx-fN+tW`zCqH^XG>#Ies$qxa!n4*FF0m zxgJlPPYl*q4ylX;DVu3G*I6T&JyWvs`A(*u0+62=+ylt2!u)6LJ=Qe1rA$OWcNCmH zLu7PwMDY#rYQA1!!ONNcz~I^uMvi6N&Lo4dD&HF?1Su5}COTZ-jwR)-zLq=6@bN}X zSP(-MY`TOJ@1O`bLPphMMSWm+YL{Ger>cA$KT~)DuTl+H)!2Lf`c+lZ0ipxd>KfKn zIv;;eEmz(_(nwW24a+>v{K}$)A?=tp+?>zAmfL{}@0r|1>iFQfJ5C*6dKdijK=j16 zQpl4gl93ttF5@d<9e2LoZ~cqkH)aFMgt(el_)#OG4R4Hnqm(@D*Uj>2ZuUCy)o-yy z_J|&S-@o5#2IMcL(}qWF3EL<4n(`cygenA)G%Ssi7k4w)LafelpV5FvS9uJES+(Ml z?rzZ={vYrB#mB-Hd#ID{KS5dKl-|Wh_~v+Lvq3|<@w^MD-RA{q!$gkUUNIvAaex5y z)jIGW{#U=#UWyku7FIAB=TES8>L%Y9*h2N`#Gghie+a?>$CRNth?ORq)!Tde24f5K zKh>cz5oLC;ry*tHIEQEL>8L=zsjG7+(~LUN5K1pT`_Z-4Z}k^m%&H%g3*^e(FDCC{ zBh~eqx%bY?qqu_2qa+9A+oS&yFw^3nLRsN#?FcZvt?*dZhRC_a%Jd{qou(p5AG_Q6 ziOJMu8D~kJ7xEkG(69$Dl3t1J592=Olom%;13uZvYDda08YwzqFlND-;YodmA!SL) z!AOSI=(uCnG#Yo&BgrH(muUemmhQW7?}IHfxI~T`44wuLGFOMdKreQO!a=Z-LkH{T z@h;`A_l2Pp>Xg#`Vo@-?WJn-0((RR4uKM6P2*^-qprHgQhMzSd32@ho>%fFMbp9Y$ zx-#!r8gEu;VZN(fDbP7he+Nu7^o3<+pT!<<>m;m z=FC$N)wx)asxb_KLs}Z^;x*hQM}wQGr((&=%+=#jW^j|Gjn$(qqXwt-o-|>kL!?=T zh0*?m<^>S*F}kPiq@)Cp+^fnKi2)%<-Tw4K3oHwmI-}h}Kc^+%1P!D8aWp!hB@-ZT zybHrRdeYlYulEj>Bk zEIi|PU0eGg&~kWQ{q)gw%~bFT0`Q%k5S|tt!JIZXVXX=>er!7R^w>zeQ%M-(C|eOQG>5i|}i3}X#?aqAg~b1t{-fqwKd(&CyA zmyy)et*E}+q_lEqgbClewiJ=u@bFX}LKe)5o26K9fS;R`!er~a?lUCKf60`4Zq7{2q$L?k?IrAdcDu+ z4A0QJBUiGx&$TBASI2ASM_Wj{?fjv=CORO3GZz;1X*AYY`anM zI`M6C%8OUFSc$tKjiFJ|V74Yj-lK&Epi7F^Gp*rLeDTokfW#o6sl33W^~4V|edbS1 zhx%1PTdnI!C96iYqSA=qu6;p&Dd%)Skjjw0fyl>3k@O?I@x5|>2_7G#_Yc2*1>=^# z|H43bJDx$SS2!vkaMG!;VRGMbY{eJhT%FR{(a+RXDbd4OT?DRoE(`NhiVI6MsUCsT z1gc^~Nv>i;cIm2~_SYOfFpkUvV)(iINXEep;i4>&8@N#|h+_;DgzLqh3I#lzhn>cN zjm;m6U{+JXR2Mi)=~WxM&t9~WShlyA$Pnu+VIW2#;0)4J*C!{1W|y1TP{Q;!tldR< zI7aoH&cMm*apW}~BabBT;`fQ1-9q|!?6nTzmhiIo6fGQlcP{pu)kJh- zUK&Ei9lArSO6ep_SN$Lt_01|Y#@Ksznl@f<+%ku1F|k#Gcwa`(^M<2%M3FAZVb99?Ez4d9O)rqM< zCbYsdZlSo{X#nKqiRA$}XG}1Tw@)D|jGKo1ITqmvE4;ovYH{NAk{h8*Ysh@=nZFiF zmDF`@4do#UDKKM*@wDbwoO@tPx4aExhPF_dvlR&dB5>)W=wG6Pil zq{eBzw%Ov!?D+%8&(uK`m7JV7pqNp-krMd>ECQypq&?p#_3wy){eW{(2q}ij{6bfmyE+-ZO z)G4OtI;ga9;EVyKF6v3kO1RdQV+!*>tV-ditH-=;`n|2T zu(vYR*BJSBsjzFl1Oy#DpL=|pfEY4NM;y5Yly__T*Eg^3Mb_()pHwn)mAsh!7Yz-Z zY`hBLDXS4F^{>x=oOphq|LMo;G!C(b2hS9A6lJqb+e$2af}7C>zW2p{m18@Bdd>iL zoEE$nFUnaz_6p${cMO|;(c1f9nm5G5R;p)m4dcC1?1YD=2Mi&20=4{nu>AV#R^d%A zsmm_RlT#`;g~an9mo#O1dYV)2{mgUWEqb*a@^Ok;ckj;uqy{%*YB^({d{^V)P9VvP zC^qbK&lq~}TWm^RF8d4zbo~bJuw zFV!!}b^4BlJ0>5S3Q>;u*BLC&G6Fa5V|~w&bRZ*-YU>df6%qAvK?%Qf+#=M-+JqLw&w*l4{v7XTstY4j z26z69U#SVzSbY9HBXyD;%P$#vVU7G*Yb-*fy)Qpx?;ed;-P24>-L6U+OAC9Jj63kg zlY`G2+5tg1szc#*9ga3%f9H9~!(^QjECetX-PlacTR+^g8L<#VRovPGvsT)ln3lr= zm5WO@!NDuw+d4MY;K4WJg3B|Sp|WdumpFJO>I2tz$72s4^uXljWseYSAd+vGfjutO z-x~Qlct+BnlI+Iun)fOklxPH?30i&j9R$6g5^f&(x7bIom|FLKq9CUE);w2G>}vye zxWvEaXhx8|~2j)({Rq>0J9}lzdE`yhQ(l$z! z;x%d%_u?^4vlES_>JaIjJBN|N8z5}@l1#PG_@{mh`oWXQOI41_kPG}R_pV+jd^PU) zEor^SHo`VMul*80-K$0mSk|FiI+tHdWt-hzt~S>6!2-!R&rdL_^gGGUzkPe zEZkUKU=EY(5Ex)zeTA4-{Bkbn!Gm?nuaI4jLE%X;zMZ7bwn4FXz(?az;9(Uv;38U6 zi)}rA3xAcD2&6BY<~Pj9Q1~4Dyjs&!$)hyHiiTI@%qXd~+>> zW}$_puSSJ^uWv$jtWakn}}@eX6_LGz|7M#$!3yjY ztS{>HmQ%-8u0@|ig{kzD&CNK~-dIK5e{;@uWOs8$r>J7^c2P~Pwx%QVX0e8~oXK0J zM4HCNK?%t6?v~#;eP#t@tM$@SXRt;(b&kU7uDzlzUuu;+LQ5g%=FqpJPGrX8HJ8CS zITK|(fjhs3@CR}H4@)EjL@J zV_HPexOQ!@k&kvsQG)n;7lZaUh>{87l4NS_=Y-O9Ul3CaKG8iy+xD=QXZSr57a-hb z7jz3Ts-NVsMI783OPEdlE|e&a2;l^h@e>oYMh5@=Lte-9A+20|?!9>Djl~{XkAo>0p9`n&nfWGdGAfT-mSYW z1cvG>GT9dRJdcm7M_AG9JX5AqTCdJ6MRqR3p?+FvMxp(oB-6MZ`lRzSAj%N(1#8@_ zDnIIo9Rtv12(Eo}k_#FILhaZQ`yRD^Vn5tm+IK@hZO>s=t5`@p1#k?Umz2y*R64CF zGM-v&*k}zZ%Xm<_?1=g~<*&3KAy;_^QfccIp~CS7NW24Tn|mSDxb%pvvi}S}(~`2# z3I|kD@||l@lAW06K2%*gHd4x9YKeXWpwU%!ozYcJ+KJeX!s6b94j!Qyy7>S!wb?{qaMa`rpbU1phn0EpF}L zsBdZc|Im#iRiQmJjZwb5#n;`_O{$Zu$I zMXqbfu0yVmt!!Y`Fzl}QV7HUSOPib#da4i@vM$0u2FEYytsvrbR#ui9lrMkZ(AVVJ zMVl^Wi_fSRsEXLA_#rdaG%r(@UCw#o7*yBN)%22b)VSNyng6Lxk|2;XK3Qb=C_<`F zN##8MLHz-s%&O6JE~@P1=iHpj8go@4sC7*AWe99tuf$f7?2~wC&RA^UjB*2`K!%$y zSDzMd7}!vvN|#wDuP%%nuGk8&>N)7eRxtqdMXHD1W%hP7tYW{W>^DJp`3WS>3}i+$ z_li?4AlEj`r=!SPiIc+NNUZ9NCrMv&G0BdQHBO&S7d48aB)LfGi@D%5CC1%)1hVcJ zB~=yNC}LBn(K?cHkPmAX$5^M7JSnNkcc!X!0kD&^F$cJmRP(SJ`9b7}b)o$rj=BZ- zC;BX3IG94%Qz&(V$)7O~v|!=jd-yU1(6wd1u;*$z4DDe6+BFLhz>+8?59?d2Ngxck zm92yR!jk@MP@>>9FtAY2L+Z|MaSp{MnL-;fm}W3~fg!9TRr3;S@ysLf@#<)keHDRO zsJI1tP`g3PNL`2(8hK3!4;r|E-ZQbU0e-9u{(@du`4wjGj|A!QB&9w~?OI1r}M? zw)6tvsknfPfmNijZ;3VZX&HM6=|&W zy6GIe3a?_(pRxdUc==do9?C&v7+6cgIoL4)Ka^bOG9`l;S|QmVzjv%)3^PDi@=-cp z=!R0bU<@_;#*D}e1m@0!%k=VPtyRAkWYW(VFl|eu0LteWH7eDB%P|uF7BQ-|D4`n; z)UpuY1)*s32UwW756>!OoAq#5GAtfrjo*^7YUv^(eiySE?!TQzKxzqXE@jM_bq3Zq zg#1orE*Zd5ZWEpDXW9$=NzuadNSO*NW)ZJ@IDuU`w}j_FRE4-QS*rD4mPVQPH(jGg z+-Ye?3%G%=DT5U1b+TnNHHv(nz-S?3!M4hXtEB@J4WK%%p zkv=Bb`1DHmgUdYo>3kwB(T>Ba#DKv%cLp2h4r8v}p=Np}wL!&PB5J-w4V4REM{kMD z${oSuAw9?*yo3?tNp~X5WF@B^P<6L0HtIW0H7^`R8~9zAXgREH`6H{ntGu$aQ;oNq zig;pB^@KMHNoJcEb0f1fz+!M6sy?hQjof-QoxJgBM`!k^T~cykcmi^s_@1B9 z)t1)Y-ZsV9iA&FDrVoF=L7U#4&inXk{3+Xm9A|R<=ErgxPW~Fq zqu-~x0dIBlR+5_}`IK^*5l3f5$&K@l?J{)_d_*459pvsF*e*#+2guls(cid4!N%DG zl3(2`az#5!^@HNRe3O4(_5nc+){q?ENQG2|uKW0U0$aJ5SQ6hg>G4OyN6os76y%u8qNNHi;}XnRNwpsfn^!6Qt(-4tE`uxaDZ`hQp#aFX373|F?vjEiSEkV>K)cTBG+UL#wDj0_ zM9$H&-86zP=9=5_Q7d3onkqKNr4PAlF<>U^^yYAAEso|Ak~p$3NNZ$~4&kE9Nj^As zQPoo!m*uZ;z1~;#g(?zFECJ$O2@EBy<;F)fnQxOKvH`MojG5T?7thbe%F@JyN^k1K zn3H*%Ymoim)ePf)xhl2%$T)vq3P=4ty%NK)@}po&7Q^~o3l))Zm4<75Y!fFihsXJc z9?vecovF^nYfJVg#W~R3T1*PK{+^YFgb*7}Up2U#)oNyzkfJ#$)PkFxrq_{Ai?0zk zWnjq_ixF~Hs7YS9Y6H&8&k0#2cAj~!Vv4{wCM zi2f1FjQf+F@=BOB)pD|T41a4AEz+8hnH<#_PT#H|Vwm7iQ0-Tw()WMN za0eI-{B2G{sZ7+L+^k@BA)G;mOFWE$O+2nS|DzPSGZ)ede(9%+8kqu4W^wTn!yZPN z7u!Qu0u}K5(0euRZ$7=kn9DZ+llruq5A_l) zOK~wof7_^8Yeh@Qd*=P!gM)lh`Z@7^M?k8Z?t$$vMAuBG>4p56Dt!R$p{)y>QG}it zGG;Ei```7ewXrbGo6Z=!AJNQ!GP8l13m7|FIQTFZTpIg#kpZkl1wj)s1eySXjAAWy zfl;;@{QQ;Qnb$@LY8_Z&7 z6+d98F?z2Zo)sS)z$YoL(zzF>Ey8u#S_%n7)XUX1Pu(>e8gEUU1S;J=EH(#`cWi1+ zoL$5TN+?#NM8=4E7HOk)bf5MXvEo%he5QcB%_5YQ$cu_j)Pd^@5hi}d%nG}x9xXtD-JMQxr;KkC=r_dS-t`lf zF&CS?Lk~>U^!)Y0LZqNVJq+*_#F7W~!UkvZfQhzvW`q;^X&iv~ zEDDGIQ&(S;#Hb(Ej4j+#D#sDS_uHehlY0kZsQpktc?;O z22W1b%wNcdfNza<1M2{*mAkM<{}@(w`VuQ<^lG|iYSuWBD#lYK9+jsdA+&#;Y@=zXLVr840Nq_t5))#7}2s9pK* zg42zd{EY|#sIVMDhg9>t6_Y#O>JoG<{GO&OzTa;iA9&&^6=5MT21f6$7o@nS=w;R) znkgu*7Y{UNPu7B9&B&~q+N@@+%&cO0N`TZ-qQ|@f@e0g2BI+9xO$}NzMOzEbSSJ@v z1uNp(S z-dioXc$5YyA6-My@gW~1GH($Q?;GCHfk{ej-{Q^{iTFs1^Sa67RNd5y{cjX1tG+$& zbGrUte{U1{^Z_qpzW$-V!pJz$dQZrL5i(1MKU`%^= z^)i;xua4w)evDBrFVm)Id5SbXMx2u7M5Df<2L4B`wy4-Y+Wec#b^QJO|J9xF{x#M8 zuLUer`%ZL^m3gy?U&dI+`kgNZ+?bl3H%8)&k84*-=aMfADh&@$xr&IS|4{3$v&K3q zZTn&f{N(#L6<-BZYNs4 zB*Kl*@_IhGXI^_8zfXT^XNmjJ@5E~H*wFf<&er?p7suz85)$-Hqz@C zGMFg1NKs;otNViu)r-u{SOLcqwqc7$poPvm(-^ag1m71}HL#cj5t4Hw(W?*fi4GSH z9962NZ>p^ECPqVc$N}phy>N8rQsWWm%%rc5B4XLATFEtffX&TM2%|8S2Lh_q; zCytXua84HBnSybW-}(j z3Zwv4CaK)jC!{oUvdsFRXK&Sx@t)yGm(h65$!WZ!-jL52no}NX6=E<=H!aZ74h_&> zZ+~c@k!@}Cs84l{u+)%kg4fq~pOeTK3S4)gX~FKJw4t9ba!Ai{_gkKQYQvafZIyKq zX|r4xgC(l%JgmW!tvR&yNt$6uME({M`uNIi7HFiPEQo_UMRkl~12&4c& z^se;dbZWKu7>dLMg`IZq%@b@ME?|@{&xEIZEU(omKNUY? z`JszxNghuO-VA;MrZKEC0|Gi0tz3c#M?aO?WGLy64LkG4T%|PBIt_?bl{C=L@9e;A zia!35TZI7<`R8hr06xF62*rNH5T3N0v^acg+;ENvrLYo|B4!c^eILcn#+lxDZR!%l zjL6!6h9zo)<5GrSPth7+R(rLAW?HF4uu$glo?w1U-y}CR@%v+wSAlsgIXn>e%bc{FE;j@R0AoNIWf#*@BSngZ)HmNqkB z)cs3yN%_PT4f*K+Y1wFl)be=1iq+bb1G-}b|72|gJ|lMt`tf~0Jk}zMbS0+M-Mq}R z>Bv}-W6J%}j#dIz`Z0}zD(DGKn`R;E8A`)$a6qDfr(c@iHKZcCVY_nJEDpcUddGH* z*ct2$&)RelhmV}@jGXY>3Y~vp;b*l9M+hO}&x`e~q*heO8GVkvvJTwyxFetJC8VnhjR`5*+qHEDUNp16g`~$TbdliLLd}AFf}U+Oda1JXwwseRFbj?DN96;VSX~z?JxJSuA^BF}262%Z0)nv<6teKK`F zfm9^HsblS~?Xrb1_~^=5=PD!QH$Y1hD_&qe1HTQnese8N#&C(|Q)CvtAu6{{0Q%ut8ESVdn&& z4y%nsCs!$(#9d{iVjXDR##3UyoMNeY@_W^%qyuZ^K3Oa4(^!tDXOUS?b2P)yRtJ8j zSX}@qGBj+gKf;|6Kb&rq`!}S*cSu-3&S>=pM$eEB{K>PP~I}N|uGE|`3U#{Q6v^kO4nIsaq zfPld}c|4tVPI4!=!ETCNW+LjcbmEoxm0RZ%ieV0`(nVlWKClZW5^>f&h79-~CF(%+ zv|KL(^xQ7$#a}&BSGr9zf{xJ(cCfq>UR*>^-Ou_pmknCt6Y--~!duL{k2D{yLMl__ z!KeMRRg&EsD2s|cmy?xgK&XcGIKeos`&UEVhBTw;mqy|8DlP1M7PYS2z{YmTJ;n!h znPe(Qu?c7+xZz!Tm1AnE8|;&tf7fW$2dArX7ck1Jd(S1+91YB8bjISRZ`UL*?vb{b zMp*!Xq7VaLc0Ogqj5qmop8NREQ{9_iC$;tviZlubGLy1jLlIFBxAymMr@SDLAcx+) z5YRkl$bW**X)W0JzWNcLx9>fTqJj00ipY6Ua?mUlsgQrVVgpmaheE;RgA5U_+WsPh z9+X|PU4zFyNxZ2?Q+V`Mo{xH~(m}OMRZa<&$nCl7o4x`^^|V4?aPz8#KwFm=8T6_} z8=P_4$_rD2a%7}}HT6VQ>ZGKW=QF7zI-2=6oBNZR$HVn|gq`>l$HZ`48lkM7%R$>MS& zghR`WZ9Xrd_6FaDedH6_aKVJhYev*2)UQ>!CRH3PQ_d9nXlO;c z9PeqiKD@aGz^|mvD-tV<{BjfA;)B+76!*+`$CZOJ=#)}>{?!9fAg(Xngbh||n=q*C zU0mGP`NxHn$uY#@)gN<0xr)%Ue80U{-`^FX1~Q@^>WbLraiB|c#4v$5HX)0z!oA#jOXPyWg! z8EC}SBmG7j3T&zCenPLYA{kN(3l62pu}91KOWZl? zg~>T4gQ%1y3AYa^J|>ba$7F5KlVx}_&*~me*q-SYLBCXZFU=U8mHQD4K!?;B61NoX z?VS41SS&jHyhmB~+bC=w0a06V``ZXCkC~}oM9pM{$hU~-s_elYPmT1L!%B`?*<+?( zFQ@TP%y+QL`_&Y0A3679pe5~iL=z)$b)k!oSbJRyw+K};SGAvvE=|<~*aiwJc?uE@2?7a1i9|3=^N%*9smt3ZIhjY>gIsr{Q2rX(NovZ7I1n^V{ z#~(1ze-%`C>fM`^hCV**9BA-04lNuu&3=reevNOMwmX(A{yh`^c8%0mjAKMj{Th05 zXrM(zILwyL-Pcdw^(=gj(ZLVMA95zlzmLa^skb8tQq%8SV&4vp?S>L3+P4^tp`$xA zr38jBw0ItR`VbO5vB1`<3d})}aorkIU1z3*ifYN&Lpp)}|}QJS60th_v-EEkAM zyOREuj!Ou|pVeZEWg;$Hf!x;xAmFu7gB^UR$=L0BuZ~thLC@#moJ(@@wejR|`t_K@ zuQ{XmpAWz%o&~2dk!SIGR$EmpZY)@+r^gvX26%)y>1u2bt~JUPTQzQu&_tB)|{19)&n$m5Fhw0A-8S1^%XpAD%`#a z_ModVxsM|x!m3N1vRt_XEL`O-+J3cMsM1l*dbjT&S0c@}Xxl3I&AeMNT97G3c6%3C zbrZS?2EAKcEq@@Pw?r%eh0YM6z0>&Qe#n+e9hEHK?fzig3v5S#O2IxVLu;a>~c~ZfHVbgLox%_tg)bsC8Rl35P=Jhl+Y=w6zb$ z;*uO%i^U z^mp_QggBILLF$AyjPD41Z0SFdbDj&z&xjq~X|OoM7bCuBfma1CEd!4RKGqPR)K)e}+7^JfFUI_fy63cMyq#&)Z*#w18{S zhC@f9U5k#2S2`d$-)cEoH-eAz{2Qh>YF1Xa)E$rWd52N-@{#lrw3lRqr)z?BGThgO z-Mn>X=RPHQ)#9h{3ciF)<>s{uf_&XdKb&kC!a373l2OCu&y8&n#P%$7YwAVJ_lD-G zX7tgMEV8}dY^mz`R6_0tQ5Eu@CdSOyaI63Vb*mR+rCzxgsjCXLSHOmzt0tA zGoA0Cp&l>rtO@^uQayrkoe#d2@}|?SlQl9W{fmcxY(0*y zHTZ6>FL;$8FEzbb;M(o%mBe-X?o<0+1dH?ZVjcf8)Kyqb07*a zLfP1blbt)=W)TN}4M#dUnt8Gdr4p$QRA<0W)JhWLK3-g82Q~2Drmx4J z;6m4re%igus136VL}MDI-V;WmSfs4guF_(7ifNl#M~Yx5HB!UF)>*-KDQl0U?u4UXV2I*qMhEfsxb%87fi+W;mW5{h?o8!52}VUs*Fpo#aSuXk(Ug z>r>xC#&2<9Uwmao@iJQ|{Vr__?eRT2NB$OcoXQ-jZ{t|?Uy{7q$nU-i|&-R6fHPWJDgHZ69iVbK#Ab@2@y zPD*Gj=hib?PWr8NGf;g$o5I!*n>94Z!IfqRm zLvM>Gx$Y*rEL3Z-+lS42=cnEfXR)h1z`h8a+I%E_ss%qXsrgIV%qv9d|KT>fV5=3e zw>P#ju>2naGc{=6!)9TeHq$S9Pk|>$UCEl}H}lE@;0(jbNT9TXUXyss>al>S4DuGi zVCy;Qt=a2`iu2;TvrIkh2NTvNV}0)qun~9y1yEQMdOf#V#3(e(C?+--8bCsJu={Q1z5qNJIk&yW>ZnVm;A=fL~29lvXQ*4j(SLau?P zi8LC7&**O!6B6=vfY%M;!p2L2tQ+w3Y!am{b?14E`h4kN$1L0XqT5=y=DW8GI_yi% zlIWsjmf0{l#|ei>)>&IM4>jXH)?>!fK?pfWIQn9gT9N(z&w3SvjlD|u*6T@oNQRF6 zU5Uo~SA}ml5f8mvxzX>BGL}c2#AT^6Lo-TM5XluWoqBRin$tiyRQK0wJ!Ro+7S!-K z=S95p-(#IDKOZsRd{l65N(Xae`wOa4Dg9?g|Jx97N-7OfHG(rN#k=yNGW0K$Tia5J zMMX1+!ulc1%8e*FNRV8jL|OSL-_9Nv6O=CH>Ty(W@sm`j=NFa1F3tT$?wM1}GZekB z6F_VLMCSd7(b9T%IqUMo$w9sM5wOA7l8xW<(1w0T=S}MB+9X5UT|+nemtm_;!|bxX z_bnOKN+F30ehJ$459k@=69yTz^_)-hNE4XMv$~_%vlH_y^`P1pLxYF6#_IZyteO`9wpuS> z#%Vyg5mMDt?}j!0}MoBX|9PS0#B zSVo6xLVjujMN57}IVc#A{VB*_yx;#mgM4~yT6wO;Qtm8MV6DX?u(JS~JFA~PvEl%9 z2XI}c>OzPoPn_IoyXa2v}BA(M+sWq=_~L0rZ_yR17I5c^m4;?2&KdCc)3lCs!M|0OzH@(PbG8T6w%N zKzR>%SLxL_C6~r3=xm9VG8<9yLHV6rJOjFHPaNdQHHflp><44l>&;)&7s)4lX%-er znWCv8eJJe1KAi_t1p%c4`bgxD2(1v)jm(gvQLp2K-=04oaIJu{F7SIu8&)gyw7x>+ zbzYF7KXg;T71w!-=C0DjcnF^JP$^o_N>*BAjtH!^HD6t1o?(O7IrmcodeQVDD<*+j zN)JdgB6v^iiJ1q`bZ(^WvN{v@sDqG$M9L`-UV!3q&sWZUnQ{&tAkpX(nZ_L#rMs}>p7l0fU5I5IzArncQi6TWjP#1B=QZ|Uqm-3{)YPn=XFqHW-~Fb z^!0CvIdelQbgcac9;By79%T`uvNhg9tS><pLzXePP=JZzcO@?5GRAdF4)sY*)YGP* zyioMa3=HRQz(v}+cqXc0%2*Q%CQi%e2~$a9r+X*u3J8w^Shg#%4I&?!$})y@ zzg8tQ6_-`|TBa_2v$D;Q(pFutj7@yos0W$&__9$|Yn3DFe*)k{g^|JIV4bqI@2%-4kpb_p? zQ4}qQcA>R6ihbxnVa{c;f7Y)VPV&mRY-*^qm~u3HB>8lf3P&&#GhQk8uIYYgwrugY zei>mp`YdC*R^Cxuv@d0V?$~d*=m-X?1Fqd9@*IM^wQ_^-nQEuc0!OqMr#TeT=8W`JbjjXc-Dh3NhnTj8e82yP;V_B<7LIejij+B{W1ViaJ_)+q?$BaLJpxt_4@&(?rWC3NC-_Z9Sg4JJWc( zX!Y34j67vCMHKB=JcJ1|#UI^D^mn(i=A5rf-iV7y4bR5HhC=I`rFPZv4F>q+h?l34 z4(?KYwZYHwkPG%kK7$A&M#=lpIn3Qo<>s6UFy|J$Zca-s(oM7??dkuKh?f5b2`m57 zJhs4BTcVVmwsswlX?#70uQb*k1Fi3q4+9`V+ikSk{L3K=-5HgN0JekQ=J~549Nd*+H%5+fi6aJuR=K zyD3xW{X$PL7&iR)=wumlTq2gY{LdrngAaPC;Qw_xLfVE0c0Z>y918TQpL!q@?`8{L!el18Qxiki3WZONF=eK$N3)p>36EW)I@Y z7QxbWW_9_7a*`VS&5~4-9!~&g8M+*U9{I2Bz`@TJ@E(YL$l+%<=?FyR#&e&v?Y@@G zqFF`J*v;l$&(A=s`na2>4ExKnxr`|OD+Xd-b4?6xl4mQ94xuk!-$l8*%+1zQU{)!= zTooUhjC0SNBh!&Ne}Q=1%`_r=Vu1c8RuE!|(g4BQGcd5AbpLbvKv_Z~Y`l!mr!sCc zDBupoc{W@U(6KWqW@xV_`;J0~+WDx|t^WeMri#=q0U5ZN7@@FAv<1!hP6!IYX z>UjbhaEv2Fk<6C0M^@J`lH#LgKJ(`?6z5=uH+ImggSQaZtvh52WTK+EBN~-op#EQKYW`$yBmq z4wgLTJPn3;mtbs0m0RO&+EG>?rb*ZECE0#eeSOFL!2YQ$w}cae>sun`<=}m!=go!v zO2jn<0tNh4E-4)ZA(ixh5nIUuXF-qYl>0I_1)K%EAw`D7~la$=gc@6g{iWF=>i_76?Mc zh#l9h7))<|EY=sK!E|54;c!b;Zp}HLd5*-w^6^whxB98v`*P>cj!Nfu1R%@bcp{cb zUZ24(fUXn3d&oc{6H%u(@4&_O?#HO(qd^YH=V`WJ=u*u6Zie8mE^r_Oz zDw`DaXeq4G#m@EK5+p40Xe!Lr!-jTQLCV3?R1|3#`%45h8#WSA!XoLDMS7=t!SluZ4H56;G z6C9D(B6>k^ur_DGfJ@Y-=3$5HkrI zO+3P>R@$6QZ#ATUI3$)xRBEL#5IKs}yhf&fK;ANA#Qj~G zdE|k|`puh$%dyE4R0$7dZd)M*#e7s%*PKPyrS;d%&S(d{_Ktq^!Hpi&bxZx`?9pEw z%sPjo&adHm95F7Z1{RdY#*a!&LcBZVRe{qhn8d{pOUJ{fOu`_kFg7ZVeRYZ(!ezNktT5{Ab z4BZI$vS0$vm3t9q`ECjDK;pmS{8ZTKs`Js~PYv2|=VkDv{Dtt)cLU@9%K6_KqtqfM zaE*e$f$Xm=;IAURNUXw8g%=?jzG2}10ZA5qXzAaJ@eh)yv5B=ETyVwC-a*CD;GgRJ z4J1~zMUey?4iVlS0zW|F-~0nenLiN3S0)l!T2}D%;<}Z9DzeVgcB+MSj;f$KY;uP%UR#f`0u*@6U@tk@jO3N?Fjq< z{cUUhjrr$rmo>qE?52zKe+>6iP5P_tcUfxsLSy{9*)shB(w`UUveNH`a`kr$VEF@} zKh&|lTD;4;m_H6C&)9#D`kRh;S(NTa=Ve^~xe_0~x$6h8Q@B_qu#ee=(lkI9@F6$0m=z@H=4&h%Q{htM>uHs(Sr@2ry`fgLA zKj8lVXdGPyy)2J%A${}Rm_a{){wHnlM?yGPQ7#KO{8*(_l0QZHuV};nO?c%h?qwSL z3wem|w*2tdxW5&PxC(Wd0QG_w|GPbw|0UFK`u$~U%!`QKcME;=Q@?*erh4_>FP~1n zAldwG9h$$u_$RFK6Uxo20GHqJzc}Rl-EwVz3h4n z;3~%DwD84i>)-8#&#y3k)3BG5cNaP3?t4q}F%yfv?*yEiC>sSo}$f>nh0QNZXH1N)-Q7kbk=2uL9OrF)nXrE@F1y%_8Yn c82=K%QXLKFx%@O{wJjEi6Y56o#$)Bpeg literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9355b41 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..adff685 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e509b2d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/participation-service/build.gradle b/participation-service/build.gradle new file mode 100644 index 0000000..c5507a9 --- /dev/null +++ b/participation-service/build.gradle @@ -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' +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..e3c178a --- /dev/null +++ b/settings.gradle @@ -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' diff --git a/user-service/build.gradle b/user-service/build.gradle new file mode 100644 index 0000000..63a1c78 --- /dev/null +++ b/user-service/build.gradle @@ -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' +}