mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 06:46:24 +00:00
develop
This commit is contained in:
parent
41d57e7399
commit
98ede67f62
15
ai/build.gradle
Normal file
15
ai/build.gradle
Normal file
@ -0,0 +1,15 @@
|
||||
bootJar {
|
||||
archiveFileName = 'ai.jar'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// OpenAI
|
||||
implementation "com.theokanning.openai-gpt3-java:service:${openaiVersion}"
|
||||
|
||||
// Azure AI Search
|
||||
implementation "com.azure:azure-search-documents:${azureAiSearchVersion}"
|
||||
|
||||
// Feign (for external API calls)
|
||||
implementation "io.github.openfeign:feign-jackson:${feignJacksonVersion}"
|
||||
implementation "io.github.openfeign:feign-okhttp:${feignJacksonVersion}"
|
||||
}
|
||||
111
ai/src/main/resources/application.yml
Normal file
111
ai/src/main/resources/application.yml
Normal file
@ -0,0 +1,111 @@
|
||||
spring:
|
||||
application:
|
||||
name: ai
|
||||
|
||||
# Database Configuration
|
||||
datasource:
|
||||
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:20.249.153.213}:${DB_PORT:5432}/${DB_NAME:aidb}
|
||||
username: ${DB_USERNAME:hgzerouser}
|
||||
password: ${DB_PASSWORD:}
|
||||
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 Configuration
|
||||
jpa:
|
||||
show-sql: ${SHOW_SQL:true}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
use_sql_comments: true
|
||||
hibernate:
|
||||
ddl-auto: ${DDL_AUTO:update}
|
||||
|
||||
# Redis Configuration
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:20.249.177.114}
|
||||
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:3}
|
||||
|
||||
# Server Configuration
|
||||
server:
|
||||
port: ${SERVER_PORT:8083}
|
||||
|
||||
# JWT Configuration
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600}
|
||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800}
|
||||
|
||||
# CORS Configuration
|
||||
cors:
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:*}
|
||||
|
||||
# OpenAI Configuration
|
||||
openai:
|
||||
api-key: ${OPENAI_API_KEY:}
|
||||
model: ${OPENAI_MODEL:gpt-4}
|
||||
max-tokens: ${OPENAI_MAX_TOKENS:2000}
|
||||
temperature: ${OPENAI_TEMPERATURE:0.7}
|
||||
|
||||
# Azure AI Search Configuration
|
||||
azure:
|
||||
aisearch:
|
||||
endpoint: ${AZURE_AISEARCH_ENDPOINT:}
|
||||
api-key: ${AZURE_AISEARCH_API_KEY:}
|
||||
index-name: ${AZURE_AISEARCH_INDEX_NAME:minutes-index}
|
||||
|
||||
# Actuator Configuration
|
||||
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 Configuration
|
||||
logging:
|
||||
level:
|
||||
com.unicorn.hgzero.ai: ${LOG_LEVEL_APP:DEBUG}
|
||||
org.springframework.web: ${LOG_LEVEL_WEB:INFO}
|
||||
org.springframework.security: ${LOG_LEVEL_SECURITY:DEBUG}
|
||||
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/ai.log}
|
||||
132
build.gradle
Normal file
132
build.gradle
Normal file
@ -0,0 +1,132 @@
|
||||
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.hgzero'
|
||||
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'
|
||||
azureSpeechVersion = '1.37.0'
|
||||
azureBlobVersion = '12.25.3'
|
||||
azureEventHubsVersion = '5.18.2'
|
||||
azureAiSearchVersion = '11.6.2'
|
||||
springAiVersion = '1.0.0-M1'
|
||||
}
|
||||
}
|
||||
|
||||
// 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-security'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
|
||||
// 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}"
|
||||
|
||||
// JWT
|
||||
implementation "io.jsonwebtoken:jjwt-api:${jjwtVersion}"
|
||||
runtimeOnly "io.jsonwebtoken:jjwt-impl:${jjwtVersion}"
|
||||
runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jjwtVersion}"
|
||||
|
||||
// Database
|
||||
runtimeOnly 'org.postgresql:postgresql'
|
||||
|
||||
// Redis
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
|
||||
// Utilities
|
||||
implementation "org.apache.commons:commons-lang3:${commonsLang3Version}"
|
||||
implementation "commons-io:commons-io:${commonsIoVersion}"
|
||||
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
|
||||
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
|
||||
|
||||
// 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'
|
||||
}
|
||||
662
claude/dev-backend.md
Normal file
662
claude/dev-backend.md
Normal file
@ -0,0 +1,662 @@
|
||||
# 백엔드 개발 가이드
|
||||
|
||||
[요청사항]
|
||||
- <개발원칙>을 준용하여 개발
|
||||
- <개발순서>에 따라 아래 3단계로 개발
|
||||
- '0. 준비'를 수행하고 완료 후 다음 단계 진행여부를 사용자에게 확인
|
||||
- '1. common 모듈 개발'을 수행하고 완료 후 다음 단계 진행여부를 사용자에게 확인
|
||||
- '2. 각 서비스별 구현'은 사용자와 함께 각 서비스를 개발
|
||||
|
||||
[가이드]
|
||||
<개발원칙>
|
||||
- '개발주석표준'에 맞게 주석 작성
|
||||
- API설계서와 일관성 있게 설계. Controller에 API를 누락하지 말고 모두 개발
|
||||
- '외부시퀀스설계서'와 '내부시퀀스설계서'와 일치되도록 개발
|
||||
- 각 서비스별 지정된 {설계 아키텍처 패턴}을 적용하여 개발
|
||||
- Layered 아키텍처 적용 시 Service레이어에 Interface 사용
|
||||
- Clean아키텍처 적용 시 Port/Adapter라는 용어 대신 Clean 아키텍처에 맞는 용어 사용
|
||||
- 백킹서비스 연동은 '데이터베이스설치결과서', '캐시설치결과서', 'MQ설치결과서'를 기반으로 개발
|
||||
- 빌드도구는 Gradle 사용
|
||||
- 설정 Manifest(src/main/resources/application*.yml) 작성 시 '[설정 Manifest 표준]' 준용
|
||||
<개발순서>
|
||||
- 0. 준비:
|
||||
- 참고자료 분석 및 이해
|
||||
- '패키지구조표준'의 예시를 참조하여 모든 클래스와 파일이 포함된 패키지 구조도를 작성
|
||||
- plantuml 스크립트가 아니라 트리구조 텍스트로 작성
|
||||
- 결과파일: develop/dev/package-structure.md
|
||||
- settings.gralde 파일 작성
|
||||
- build.gradle 작성
|
||||
- '<Build.gradle 구성 최적화>' 가이드대로 최상위와 각 서비스별 build.gradle 작성
|
||||
- '[루트 build.gradle 표준]'대로 최상위 build.gradle 작성
|
||||
- SpringBoot 3.3.0, Java 21 사용
|
||||
- common을 제외한 각 서비스에서 공통으로 사용되는 설정과 Dependency는 루트 build.gradle에 지정
|
||||
- 서비스별 build.gradle 작성
|
||||
- 최상위 build.gradle에 정의한 설정은 각 마이크로서비스의 build.gradle에 중복하여 정의하지 않도록 함
|
||||
- 각 서비스의 실행 jar 파일명은 서비스명과 동일하게 함
|
||||
- 각 서비스별 설정 파일 작성
|
||||
- 설정 Manifest(application.yml) 작성: '[설정 Manifest 표준]' 준용
|
||||
|
||||
- 1. common 모듈 개발
|
||||
- 각 서비스에서 공통으로 사용되는 클래스를 개발
|
||||
- 외부(웹브라우저, 데이터베이스, Message Queue, 외부시스템)와의 인터페이스를 위한 클래스는 포함하지 않음
|
||||
- 개발 완료 후 컴파일 및 에러 해결: {프로젝트 루트}/gradlew common:compileJava
|
||||
|
||||
- 2. 각 서비스별 개발
|
||||
- 사용자가 제공한 서비스의 유저스토리, 외부시퀀스설계서, 내부시퀀스설계서, API설계서 파악
|
||||
- 기존 개발 결과 파악
|
||||
- API 설계서의 각 API를 순차적으로 개발
|
||||
- Controller -> Service -> Data 레이어순으로 순차적으로 개발
|
||||
- 컴파일 및 에러 해결: {프로젝트 루트}/gradlew {service-name}:compileJava
|
||||
- 컴파일까지만 하고 서버 실행은 하지 않음
|
||||
- 모든 API개발 후 아래 수행
|
||||
- 컴파일 및 에러 해결: {프로젝트 루트}/gradlew {service-name}:compileJava
|
||||
- 빌드 및 에러 해결: {프로젝트 루트}/gradlew {service-name}:build
|
||||
- SecurityConfig 클래스 작성: '<SecurityConfig 예제>' 참조
|
||||
- JWT 인증 처리 클래스 작성: '<JWT 인증처리 예제>' 참조
|
||||
- Swagger Config 클래스 작성: '<SwaggerConfig 예제>' 참조
|
||||
- 테스트 코드 작성은 하지 않음
|
||||
|
||||
|
||||
<Build.gradle 구성 최적화>
|
||||
- **중앙 버전 관리**: 루트 build.gradle의 `ext` 블록에서 모든 외부 라이브러리 버전 통일 관리
|
||||
- **Spring Boot BOM 활용**: Spring Boot/Cloud에서 관리하는 라이브러리는 버전 명시 불필요 (자동 호환성 보장)
|
||||
- **Common 모듈 설정**: `java-library` + Spring Boot 플러그인 조합, `bootJar` 비활성화로 일반 jar 생성
|
||||
- **서비스별 최적화**: 공통 의존성(API 문서화, 테스트 등)은 루트에서 일괄 적용
|
||||
- **JWT 버전 통일**: 라이브러리 버전 변경시 API 호환성 확인 필수 (`parserBuilder()` → `parser()`)
|
||||
- **dependency-management 적용**: 모든 서브프로젝트에 Spring BOM 적용으로 버전 충돌 방지
|
||||
|
||||
[참고자료]
|
||||
- 유저스토리
|
||||
- API설계서
|
||||
- 외부시퀀스설계서
|
||||
- 내부시퀀스설계서
|
||||
- 데이터베이스설치결과서
|
||||
- 캐시설치결과서
|
||||
- MQ설치결과서
|
||||
- 테스트코드표준
|
||||
- 패키지구조표준
|
||||
|
||||
---
|
||||
|
||||
[설정 Manifest 표준]
|
||||
- common모듈은 작성하지 않음
|
||||
- application.yml에 작성
|
||||
- 하드코딩하지 않고 환경변수 사용
|
||||
특히, 데이터베이스, MQ 등의 연결 정보는 반드시 환경변수로 변환해야 함: '<DB/Redis 설정 예제>' 참조
|
||||
- spring.application.name은 서비스명과 동일하게 함
|
||||
- Redis Database는 각 서비스마다 다르게 설정
|
||||
- 민감한 정보의 디폴트값은 생략하거나 간략한 값으로 지정
|
||||
- JWT Secret Key는 모든 서비스가 동일해야 함
|
||||
- '[JWT,CORS,Actuaotr,OpenAPI Documentation,Loggings 표준]'을 준수하여 설정
|
||||
|
||||
[JWT, CORS, Actuaotr,OpenAPI Documentation,Loggings 표준]
|
||||
```
|
||||
# JWT
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:1800}
|
||||
refresh-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:86400}
|
||||
|
||||
# CORS Configuration
|
||||
cors:
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:*}
|
||||
|
||||
# Actuator
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
base-path: /actuator
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
show-components: always
|
||||
health:
|
||||
livenessState:
|
||||
enabled: true
|
||||
readinessState:
|
||||
enabled: true
|
||||
|
||||
# OpenAPI Documentation
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
tags-sorter: alpha
|
||||
operations-sorter: alpha
|
||||
show-actuator: false
|
||||
|
||||
# Logging
|
||||
logging:
|
||||
level:
|
||||
{서비스 패키지 경로}: ${LOG_LEVEL_APP:DEBUG}
|
||||
org.springframework.web: ${LOG_LEVEL_WEB:INFO}
|
||||
org.hibernate.SQL: ${LOG_LEVEL_SQL:DEBUG}
|
||||
org.hibernate.type: ${LOG_LEVEL_SQL_TYPE:TRACE}
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
|
||||
file:
|
||||
name: ${LOG_FILE_PATH:logs/{서비스명}.log}
|
||||
|
||||
```
|
||||
|
||||
[루트 build.gradle 표준]
|
||||
```
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.3.0' apply false
|
||||
id 'io.spring.dependency-management' version '1.1.6' apply false
|
||||
id 'io.freefair.lombok' version '8.10' apply false
|
||||
}
|
||||
|
||||
group = 'com.unicorn.{시스템명}'
|
||||
version = '1.0.0'
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'io.freefair.lombok'
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom annotationProcessor
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
// Common versions for all subprojects
|
||||
ext {
|
||||
jjwtVersion = '0.12.5'
|
||||
springdocVersion = '2.5.0'
|
||||
mapstructVersion = '1.5.5.Final'
|
||||
commonsLang3Version = '3.14.0'
|
||||
commonsIoVersion = '2.16.1'
|
||||
hypersistenceVersion = '3.7.3'
|
||||
openaiVersion = '0.18.2'
|
||||
feignJacksonVersion = '13.1'
|
||||
}
|
||||
}
|
||||
|
||||
// Configure all subprojects with Spring dependency management
|
||||
subprojects {
|
||||
apply plugin: 'io.spring.dependency-management'
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure only service modules (exclude common)
|
||||
configure(subprojects.findAll { it.name != 'common' }) {
|
||||
apply plugin: 'org.springframework.boot'
|
||||
|
||||
dependencies {
|
||||
// Common module dependency
|
||||
implementation project(':common')
|
||||
|
||||
// Actuator for health checks and monitoring
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
|
||||
// API Documentation (common across all services)
|
||||
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springdocVersion}"
|
||||
|
||||
// Testing
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'org.springframework.security:spring-security-test'
|
||||
testImplementation 'org.testcontainers:junit-jupiter'
|
||||
testImplementation 'org.mockito:mockito-junit-jupiter'
|
||||
|
||||
// Configuration Processor
|
||||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||
}
|
||||
}
|
||||
|
||||
// Java version consistency check for all modules
|
||||
tasks.register('checkJavaVersion') {
|
||||
doLast {
|
||||
println "Java Version: ${System.getProperty('java.version')}"
|
||||
println "Java Home: ${System.getProperty('java.home')}"
|
||||
}
|
||||
}
|
||||
|
||||
// Clean task for all subprojects
|
||||
tasks.register('cleanAll') {
|
||||
dependsOn subprojects.collect { it.tasks.named('clean') }
|
||||
description = 'Clean all subprojects'
|
||||
}
|
||||
|
||||
// Build task for all subprojects
|
||||
tasks.register('buildAll') {
|
||||
dependsOn subprojects.collect { it.tasks.named('build') }
|
||||
description = 'Build all subprojects'
|
||||
}
|
||||
```
|
||||
|
||||
<DB/Redis 설정 예제>
|
||||
```
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:phonebill_auth}
|
||||
username: ${DB_USERNAME:phonebill_user}
|
||||
password: ${DB_PASSWORD:phonebill_pass}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
leak-detection-threshold: 60000
|
||||
# JPA 설정
|
||||
jpa:
|
||||
show-sql: ${SHOW_SQL:true}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
use_sql_comments: true
|
||||
hibernate:
|
||||
ddl-auto: ${DDL_AUTO:update}
|
||||
|
||||
# Redis 설정
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
timeout: 2000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
max-wait: -1ms
|
||||
database: ${REDIS_DATABASE:0}
|
||||
|
||||
```
|
||||
|
||||
<SecurityConfig 예제>
|
||||
```
|
||||
/**
|
||||
* Spring Security 설정
|
||||
* JWT 기반 인증 및 API 보안 설정
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Value("${cors.allowed-origins:http://localhost:3000,http://localhost:8080,http://localhost:8081,http://localhost:8082,http://localhost:8083,http://localhost:8084}")
|
||||
private String allowedOrigins;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
return http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// Actuator endpoints
|
||||
.requestMatchers("/actuator/**").permitAll()
|
||||
// Swagger UI endpoints - context path와 상관없이 접근 가능하도록 설정
|
||||
.requestMatchers("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**").permitAll()
|
||||
// Health check
|
||||
.requestMatchers("/health").permitAll()
|
||||
// All other requests require authentication
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
|
||||
UsernamePasswordAuthenticationFilter.class)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
|
||||
// 환경변수에서 허용할 Origin 패턴 설정
|
||||
String[] origins = allowedOrigins.split(",");
|
||||
configuration.setAllowedOriginPatterns(Arrays.asList(origins));
|
||||
|
||||
// 허용할 HTTP 메소드
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
|
||||
|
||||
// 허용할 헤더
|
||||
configuration.setAllowedHeaders(Arrays.asList(
|
||||
"Authorization", "Content-Type", "X-Requested-With", "Accept",
|
||||
"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"
|
||||
));
|
||||
|
||||
// 자격 증명 허용
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
// Pre-flight 요청 캐시 시간
|
||||
configuration.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<JWT 인증처리 예제>
|
||||
|
||||
1) JwtAuthenticationFilter
|
||||
```
|
||||
/**
|
||||
* JWT 인증 필터
|
||||
* HTTP 요청에서 JWT 토큰을 추출하여 인증을 수행
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
String token = jwtTokenProvider.resolveToken(request);
|
||||
|
||||
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
|
||||
String userId = jwtTokenProvider.getUserId(token);
|
||||
String username = null;
|
||||
String authority = null;
|
||||
|
||||
try {
|
||||
username = jwtTokenProvider.getUsername(token);
|
||||
} catch (Exception e) {
|
||||
log.debug("JWT에 username 클레임이 없음: {}", e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
authority = jwtTokenProvider.getAuthority(token);
|
||||
} catch (Exception e) {
|
||||
log.debug("JWT에 authority 클레임이 없음: {}", e.getMessage());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(userId)) {
|
||||
// UserPrincipal 객체 생성 (username과 authority가 없어도 동작)
|
||||
UserPrincipal userPrincipal = UserPrincipal.builder()
|
||||
.userId(userId)
|
||||
.username(username != null ? username : "unknown")
|
||||
.authority(authority != null ? authority : "USER")
|
||||
.build();
|
||||
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(
|
||||
userPrincipal,
|
||||
null,
|
||||
Collections.singletonList(new SimpleGrantedAuthority(authority != null ? authority : "USER"))
|
||||
);
|
||||
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
log.debug("인증된 사용자: {} ({})", userPrincipal.getUsername(), userId);
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String path = request.getRequestURI();
|
||||
return path.startsWith("/actuator") ||
|
||||
path.startsWith("/swagger-ui") ||
|
||||
path.startsWith("/v3/api-docs") ||
|
||||
path.equals("/health");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1) JwtTokenProvider
|
||||
```
|
||||
/**
|
||||
* JWT 토큰 제공자
|
||||
* JWT 토큰의 생성, 검증, 파싱을 담당
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtTokenProvider {
|
||||
|
||||
private final SecretKey secretKey;
|
||||
private final long tokenValidityInMilliseconds;
|
||||
|
||||
public JwtTokenProvider(@Value("${jwt.secret}") String secret,
|
||||
@Value("${jwt.access-token-validity:3600}") long tokenValidityInSeconds) {
|
||||
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
||||
this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 요청에서 JWT 토큰 추출
|
||||
*/
|
||||
public String resolveToken(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader("Authorization");
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
||||
return bearerToken.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 토큰 유효성 검증
|
||||
*/
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
Jwts.parser()
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token);
|
||||
return true;
|
||||
} catch (SecurityException | MalformedJwtException e) {
|
||||
log.debug("Invalid JWT signature: {}", e.getMessage());
|
||||
} catch (ExpiredJwtException e) {
|
||||
log.debug("Expired JWT token: {}", e.getMessage());
|
||||
} catch (UnsupportedJwtException e) {
|
||||
log.debug("Unsupported JWT token: {}", e.getMessage());
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.debug("JWT token compact of handler are invalid: {}", e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 토큰에서 사용자 ID 추출
|
||||
*/
|
||||
public String getUserId(String token) {
|
||||
Claims claims = Jwts.parser()
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
|
||||
return claims.getSubject();
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 토큰에서 사용자명 추출
|
||||
*/
|
||||
public String getUsername(String token) {
|
||||
Claims claims = Jwts.parser()
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
|
||||
return claims.get("username", String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 토큰에서 권한 정보 추출
|
||||
*/
|
||||
public String getAuthority(String token) {
|
||||
Claims claims = Jwts.parser()
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
|
||||
return claims.get("authority", String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 만료 시간 확인
|
||||
*/
|
||||
public boolean isTokenExpired(String token) {
|
||||
try {
|
||||
Claims claims = Jwts.parser()
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
|
||||
return claims.getExpiration().before(new Date());
|
||||
} catch (Exception e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰에서 만료 시간 추출
|
||||
*/
|
||||
public Date getExpirationDate(String token) {
|
||||
Claims claims = Jwts.parser()
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
|
||||
return claims.getExpiration();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1) UserPrincipal
|
||||
```
|
||||
/**
|
||||
* 인증된 사용자 정보
|
||||
* JWT 토큰에서 추출된 사용자 정보를 담는 Principal 객체
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@RequiredArgsConstructor
|
||||
public class UserPrincipal {
|
||||
|
||||
/**
|
||||
* 사용자 고유 ID
|
||||
*/
|
||||
private final String userId;
|
||||
|
||||
/**
|
||||
* 사용자명
|
||||
*/
|
||||
private final String username;
|
||||
|
||||
/**
|
||||
* 사용자 권한
|
||||
*/
|
||||
private final String authority;
|
||||
|
||||
/**
|
||||
* 사용자 ID 반환 (별칭)
|
||||
*/
|
||||
public String getName() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 권한 여부 확인
|
||||
*/
|
||||
public boolean isAdmin() {
|
||||
return "ADMIN".equals(authority);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일반 사용자 권한 여부 확인
|
||||
*/
|
||||
public boolean isUser() {
|
||||
return "USER".equals(authority) || authority == null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<SwaggerConfig 예제>
|
||||
```
|
||||
/**
|
||||
* Swagger/OpenAPI 설정
|
||||
* AI Service API 문서화를 위한 설정
|
||||
*/
|
||||
@Configuration
|
||||
public class SwaggerConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI openAPI() {
|
||||
return new OpenAPI()
|
||||
.info(apiInfo())
|
||||
.addServersItem(new Server()
|
||||
.url("http://localhost:8084")
|
||||
.description("Local Development"))
|
||||
.addServersItem(new Server()
|
||||
.url("{protocol}://{host}:{port}")
|
||||
.description("Custom Server")
|
||||
.variables(new io.swagger.v3.oas.models.servers.ServerVariables()
|
||||
.addServerVariable("protocol", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||
._default("http")
|
||||
.description("Protocol (http or https)")
|
||||
.addEnumItem("http")
|
||||
.addEnumItem("https"))
|
||||
.addServerVariable("host", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||
._default("localhost")
|
||||
.description("Server host"))
|
||||
.addServerVariable("port", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||
._default("8084")
|
||||
.description("Server port"))))
|
||||
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
|
||||
.components(new Components()
|
||||
.addSecuritySchemes("Bearer Authentication", createAPIKeyScheme()));
|
||||
}
|
||||
|
||||
private Info apiInfo() {
|
||||
return new Info()
|
||||
.title("AI Service API")
|
||||
.description("AI 기반 시간별 상세 일정 생성 및 장소 추천 정보 API")
|
||||
.version("1.0.0")
|
||||
.contact(new Contact()
|
||||
.name("TripGen Development Team")
|
||||
.email("dev@tripgen.com"));
|
||||
}
|
||||
|
||||
private SecurityScheme createAPIKeyScheme() {
|
||||
return new SecurityScheme()
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.bearerFormat("JWT")
|
||||
.scheme("bearer");
|
||||
}
|
||||
}
|
||||
```
|
||||
518
claude/standard_comment.md
Normal file
518
claude/standard_comment.md
Normal file
@ -0,0 +1,518 @@
|
||||
# 개발주석표준 가이드
|
||||
|
||||
## 📋 개요
|
||||
|
||||
이 문서는 CMS 프로젝트의 JavaDoc 주석 작성 표준을 정의합니다. 일관된 주석 스타일을 통해 코드의 가독성과 유지보수성을 향상시키는 것을 목표로 합니다.
|
||||
|
||||
## 🎯 주석 작성 원칙
|
||||
|
||||
### 1. **기본 원칙**
|
||||
- **명확성**: 코드의 의도와 동작을 명확하게 설명
|
||||
- **일관성**: 프로젝트 전체에서 동일한 스타일 적용
|
||||
- **완전성**: 모든 public 메서드와 클래스에 주석 작성
|
||||
- **최신성**: 코드 변경 시 주석도 함께 업데이트
|
||||
|
||||
### 2. **주석 대상**
|
||||
- **필수**: public 클래스, 인터페이스, 메서드
|
||||
- **권장**: protected 메서드, 중요한 필드
|
||||
- **선택**: private 메서드 (복잡한 로직인 경우)
|
||||
|
||||
## 📝 JavaDoc 기본 문법
|
||||
|
||||
### 1. **기본 구조**
|
||||
```java
|
||||
/**
|
||||
* 클래스나 메서드의 간단한 설명 (첫 번째 문장)
|
||||
*
|
||||
* <p>상세한 설명이 필요한 경우 여기에 작성합니다.</p>
|
||||
*
|
||||
* @param paramName 파라미터 설명
|
||||
* @return 반환값 설명
|
||||
* @throws ExceptionType 예외 상황 설명
|
||||
* @since 1.0
|
||||
* @author 작성자명
|
||||
* @see 관련클래스#메서드
|
||||
*/
|
||||
```
|
||||
|
||||
### 2. **주요 JavaDoc 태그**
|
||||
|
||||
| 태그 | 설명 | 사용 위치 |
|
||||
|------|------|-----------|
|
||||
| `@param` | 메서드 파라미터 설명 | 메서드 |
|
||||
| `@return` | 반환값 설명 | 메서드 |
|
||||
| `@throws` | 예외 상황 설명 | 메서드 |
|
||||
| `@since` | 도입 버전 | 클래스, 메서드 |
|
||||
| `@author` | 작성자 | 클래스 |
|
||||
| `@version` | 버전 정보 | 클래스 |
|
||||
| `@see` | 관련 항목 참조 | 모든 곳 |
|
||||
| `@apiNote` | API 사용 시 주의사항 | 메서드 |
|
||||
| `@implNote` | 구현 관련 참고사항 | 메서드 |
|
||||
|
||||
## 🎨 HTML 태그 활용 가이드
|
||||
|
||||
### 1. **HTML 태그 사용 이유**
|
||||
|
||||
JavaDoc은 소스코드 주석을 파싱하여 **HTML 형태의 API 문서**를 자동 생성합니다. HTML 태그를 사용하면:
|
||||
|
||||
- **가독성 향상**: 구조화된 문서로 이해하기 쉬움
|
||||
- **자동 문서화**: JavaDoc 도구가 예쁜 HTML 문서 생성
|
||||
- **IDE 지원**: 개발 도구에서 리치 텍스트로 표시
|
||||
- **표준 준수**: Oracle JavaDoc 스타일 가이드 준수
|
||||
|
||||
### 2. **자주 사용되는 HTML 태그**
|
||||
|
||||
#### **텍스트 서식**
|
||||
```java
|
||||
/**
|
||||
* <p>단락을 구분할 때 사용합니다.</p>
|
||||
* <b>중요한 내용</b>을 강조할 때 사용합니다.
|
||||
* <i>이탤릭체</i>로 표시할 때 사용합니다.
|
||||
* <code>method()</code>와 같은 코드를 표시할 때 사용합니다.
|
||||
*/
|
||||
```
|
||||
|
||||
#### **목록 작성**
|
||||
```java
|
||||
/**
|
||||
* <p><b>주요 기능:</b></p>
|
||||
* <ul>
|
||||
* <li>첫 번째 기능</li>
|
||||
* <li>두 번째 기능</li>
|
||||
* <li>세 번째 기능</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>처리 과정:</b></p>
|
||||
* <ol>
|
||||
* <li>첫 번째 단계</li>
|
||||
* <li>두 번째 단계</li>
|
||||
* <li>세 번째 단계</li>
|
||||
* </ol>
|
||||
*/
|
||||
```
|
||||
|
||||
#### **코드 블록**
|
||||
```java
|
||||
/**
|
||||
* <p>사용 예시:</p>
|
||||
* <pre>
|
||||
* AuthController controller = new AuthController();
|
||||
* LoginRequest request = new LoginRequest("user", "password");
|
||||
* ResponseEntity<LoginResponse> response = controller.login(request);
|
||||
* </pre>
|
||||
*/
|
||||
```
|
||||
|
||||
#### **테이블**
|
||||
```java
|
||||
/**
|
||||
* <p><b>HTTP 상태 코드:</b></p>
|
||||
* <table>
|
||||
* <tr><th>상태 코드</th><th>설명</th></tr>
|
||||
* <tr><td>200</td><td>성공</td></tr>
|
||||
* <tr><td>400</td><td>잘못된 요청</td></tr>
|
||||
* <tr><td>401</td><td>인증 실패</td></tr>
|
||||
* </table>
|
||||
*/
|
||||
```
|
||||
|
||||
### 3. **HTML 태그 사용 규칙**
|
||||
|
||||
- **<와 >**: 제네릭 타입 표현 시 `<T>` 사용
|
||||
- **줄바꿈**: `<br>` 태그 사용 (가급적 `<p>` 태그 권장)
|
||||
- **링크**: `{@link ClassName#methodName}` 사용
|
||||
- **인라인 코드**: `{@code variableName}` 또는 `<code>` 사용
|
||||
|
||||
## 📋 클래스 주석 표준
|
||||
|
||||
### 1. **클래스 주석 템플릿**
|
||||
```java
|
||||
/**
|
||||
* 클래스의 간단한 설명
|
||||
*
|
||||
* <p>클래스의 상세한 설명과 목적을 여기에 작성합니다.</p>
|
||||
*
|
||||
* <p><b>주요 기능:</b></p>
|
||||
* <ul>
|
||||
* <li>기능 1</li>
|
||||
* <li>기능 2</li>
|
||||
* <li>기능 3</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>사용 예시:</b></p>
|
||||
* <pre>
|
||||
* ClassName instance = new ClassName();
|
||||
* instance.someMethod();
|
||||
* </pre>
|
||||
*
|
||||
* <p><b>주의사항:</b></p>
|
||||
* <ul>
|
||||
* <li>주의사항 1</li>
|
||||
* <li>주의사항 2</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author 작성자명
|
||||
* @version 1.0
|
||||
* @since 2024-01-01
|
||||
*
|
||||
* @see 관련클래스1
|
||||
* @see 관련클래스2
|
||||
*/
|
||||
public class ClassName {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **Controller 클래스 주석 예시**
|
||||
```java
|
||||
/**
|
||||
* 사용자 관리 API 컨트롤러
|
||||
*
|
||||
* <p>사용자 등록, 조회, 수정, 삭제 기능을 제공하는 REST API 컨트롤러입니다.</p>
|
||||
*
|
||||
* <p><b>주요 기능:</b></p>
|
||||
* <ul>
|
||||
* <li>사용자 등록 및 인증</li>
|
||||
* <li>사용자 정보 조회 및 수정</li>
|
||||
* <li>사용자 권한 관리</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>API 엔드포인트:</b></p>
|
||||
* <ul>
|
||||
* <li>POST /api/users - 사용자 등록</li>
|
||||
* <li>GET /api/users/{id} - 사용자 조회</li>
|
||||
* <li>PUT /api/users/{id} - 사용자 수정</li>
|
||||
* <li>DELETE /api/users/{id} - 사용자 삭제</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>보안 고려사항:</b></p>
|
||||
* <ul>
|
||||
* <li>모든 엔드포인트는 인증이 필요합니다</li>
|
||||
* <li>개인정보 처리 시 데이터 마스킹 적용</li>
|
||||
* <li>입력값 검증 및 XSS 방지</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author cms-team
|
||||
* @version 1.0
|
||||
* @since 2024-01-01
|
||||
*
|
||||
* @see UserService
|
||||
* @see UserRepository
|
||||
* @see UserDTO
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 메서드 주석 표준
|
||||
|
||||
### 1. **메서드 주석 템플릿**
|
||||
```java
|
||||
/**
|
||||
* 메서드의 간단한 설명
|
||||
*
|
||||
* <p>메서드의 상세한 설명과 동작을 여기에 작성합니다.</p>
|
||||
*
|
||||
* <p><b>처리 과정:</b></p>
|
||||
* <ol>
|
||||
* <li>첫 번째 단계</li>
|
||||
* <li>두 번째 단계</li>
|
||||
* <li>세 번째 단계</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p><b>주의사항:</b></p>
|
||||
* <ul>
|
||||
* <li>주의사항 1</li>
|
||||
* <li>주의사항 2</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param param1 첫 번째 파라미터 설명
|
||||
* - 추가 설명이 필요한 경우
|
||||
* @param param2 두 번째 파라미터 설명
|
||||
*
|
||||
* @return 반환값 설명
|
||||
* - 성공 시: 설명
|
||||
* - 실패 시: 설명
|
||||
*
|
||||
* @throws ExceptionType1 예외 상황 1 설명
|
||||
* @throws ExceptionType2 예외 상황 2 설명
|
||||
*
|
||||
* @apiNote API 사용 시 주의사항
|
||||
*
|
||||
* @see 관련메서드1
|
||||
* @see 관련메서드2
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public ReturnType methodName(Type param1, Type param2) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **API 메서드 주석 예시**
|
||||
```java
|
||||
/**
|
||||
* 사용자 로그인 처리
|
||||
*
|
||||
* <p>사용자 ID와 비밀번호를 검증하여 JWT 토큰을 생성합니다.</p>
|
||||
*
|
||||
* <p><b>처리 과정:</b></p>
|
||||
* <ol>
|
||||
* <li>입력값 검증 (@Valid 어노테이션)</li>
|
||||
* <li>사용자 인증 정보 확인</li>
|
||||
* <li>JWT 토큰 생성</li>
|
||||
* <li>사용자 세션 시작</li>
|
||||
* <li>로그인 메트릭 업데이트</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p><b>보안 고려사항:</b></p>
|
||||
* <ul>
|
||||
* <li>비밀번호는 BCrypt로 암호화된 값과 비교</li>
|
||||
* <li>로그인 실패 시 상세 정보 노출 방지</li>
|
||||
* <li>로그인 시도 로그 기록</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param request 로그인 요청 정보
|
||||
* - username: 사용자 ID (3-50자, 필수)
|
||||
* - password: 비밀번호 (6-100자, 필수)
|
||||
*
|
||||
* @return ResponseEntity<LoginResponse> 로그인 응답 정보
|
||||
* - 성공 시: 200 OK + JWT 토큰, 사용자 역할, 만료 시간
|
||||
* - 실패 시: 401 Unauthorized + 에러 메시지
|
||||
*
|
||||
* @throws InvalidCredentialsException 인증 정보가 올바르지 않은 경우
|
||||
* @throws RuntimeException 로그인 처리 중 시스템 오류 발생 시
|
||||
*
|
||||
* @apiNote 보안상 이유로 로그인 실패 시 구체적인 실패 사유를 반환하지 않습니다.
|
||||
*
|
||||
* @see AuthService#login(LoginRequest)
|
||||
* @see UserSessionService#startSession(String, String, java.time.Instant)
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 필드 주석 표준
|
||||
|
||||
### 1. **필드 주석 템플릿**
|
||||
```java
|
||||
/**
|
||||
* 필드의 간단한 설명
|
||||
*
|
||||
* <p>필드의 상세한 설명과 용도를 여기에 작성합니다.</p>
|
||||
*
|
||||
* <p><b>주의사항:</b></p>
|
||||
* <ul>
|
||||
* <li>주의사항 1</li>
|
||||
* <li>주의사항 2</li>
|
||||
* </ul>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
private final ServiceType serviceName;
|
||||
```
|
||||
|
||||
### 2. **의존성 주입 필드 예시**
|
||||
```java
|
||||
/**
|
||||
* 인증 서비스
|
||||
*
|
||||
* <p>사용자 로그인/로그아웃 처리 및 JWT 토큰 관리를 담당합니다.</p>
|
||||
*
|
||||
* <p><b>주요 기능:</b></p>
|
||||
* <ul>
|
||||
* <li>사용자 인증 정보 검증</li>
|
||||
* <li>JWT 토큰 생성 및 검증</li>
|
||||
* <li>로그인/로그아웃 처리</li>
|
||||
* </ul>
|
||||
*
|
||||
* @see AuthService
|
||||
* @since 1.0
|
||||
*/
|
||||
private final AuthService authService;
|
||||
```
|
||||
|
||||
## 📋 예외 클래스 주석 표준
|
||||
|
||||
```java
|
||||
/**
|
||||
* 사용자 인증 실패 예외
|
||||
*
|
||||
* <p>로그인 시 사용자 ID 또는 비밀번호가 올바르지 않을 때 발생하는 예외입니다.</p>
|
||||
*
|
||||
* <p><b>발생 상황:</b></p>
|
||||
* <ul>
|
||||
* <li>존재하지 않는 사용자 ID</li>
|
||||
* <li>잘못된 비밀번호</li>
|
||||
* <li>계정 잠금 상태</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>처리 방법:</b></p>
|
||||
* <ul>
|
||||
* <li>사용자에게 일반적인 오류 메시지 표시</li>
|
||||
* <li>보안 로그에 상세 정보 기록</li>
|
||||
* <li>브루트 포스 공격 방지 로직 실행</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author cms-team
|
||||
* @version 1.0
|
||||
* @since 2024-01-01
|
||||
*
|
||||
* @see AuthService
|
||||
* @see SecurityException
|
||||
*/
|
||||
public class InvalidCredentialsException extends RuntimeException {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 인터페이스 주석 표준
|
||||
|
||||
```java
|
||||
/**
|
||||
* 사용자 인증 서비스 인터페이스
|
||||
*
|
||||
* <p>사용자 로그인, 로그아웃, 토큰 관리 등 인증 관련 기능을 정의합니다.</p>
|
||||
*
|
||||
* <p><b>구현 클래스:</b></p>
|
||||
* <ul>
|
||||
* <li>{@link AuthServiceImpl} - 기본 구현체</li>
|
||||
* <li>{@link LdapAuthService} - LDAP 연동 구현체</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>주요 기능:</b></p>
|
||||
* <ul>
|
||||
* <li>사용자 인증 및 토큰 생성</li>
|
||||
* <li>로그아웃 및 토큰 무효화</li>
|
||||
* <li>토큰 유효성 검증</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author cms-team
|
||||
* @version 1.0
|
||||
* @since 2024-01-01
|
||||
*
|
||||
* @see AuthServiceImpl
|
||||
* @see TokenProvider
|
||||
*/
|
||||
public interface AuthService {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 Enum 주석 표준
|
||||
|
||||
```java
|
||||
/**
|
||||
* 사용자 역할 열거형
|
||||
*
|
||||
* <p>시스템 사용자의 권한 수준을 정의합니다.</p>
|
||||
*
|
||||
* <p><b>권한 계층:</b></p>
|
||||
* <ol>
|
||||
* <li>{@link #ADMIN} - 최고 관리자 권한</li>
|
||||
* <li>{@link #MANAGER} - 관리자 권한</li>
|
||||
* <li>{@link #USER} - 일반 사용자 권한</li>
|
||||
* </ol>
|
||||
*
|
||||
* @author cms-team
|
||||
* @version 1.0
|
||||
* @since 2024-01-01
|
||||
*/
|
||||
public enum Role {
|
||||
|
||||
/**
|
||||
* 시스템 관리자
|
||||
*
|
||||
* <p>모든 시스템 기능에 대한 접근 권한을 가집니다.</p>
|
||||
*
|
||||
* <p><b>주요 권한:</b></p>
|
||||
* <ul>
|
||||
* <li>사용자 관리</li>
|
||||
* <li>시스템 설정</li>
|
||||
* <li>모든 데이터 접근</li>
|
||||
* </ul>
|
||||
*/
|
||||
ADMIN,
|
||||
|
||||
/**
|
||||
* 관리자
|
||||
*
|
||||
* <p>제한된 관리 기능에 대한 접근 권한을 가집니다.</p>
|
||||
*/
|
||||
MANAGER,
|
||||
|
||||
/**
|
||||
* 일반 사용자
|
||||
*
|
||||
* <p>기본적인 시스템 기능에 대한 접근 권한을 가집니다.</p>
|
||||
*/
|
||||
USER
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 주석 작성 체크리스트
|
||||
|
||||
### ✅ **클래스 주석 체크리스트**
|
||||
- [ ] 클래스의 목적과 역할 명시
|
||||
- [ ] 주요 기능 목록 작성
|
||||
- [ ] 사용 예시 코드 포함
|
||||
- [ ] 주의사항 및 제약사항 명시
|
||||
- [ ] @author, @version, @since 태그 작성
|
||||
- [ ] 관련 클래스 @see 태그 추가
|
||||
|
||||
### ✅ **메서드 주석 체크리스트**
|
||||
- [ ] 메서드의 목적과 동작 설명
|
||||
- [ ] 처리 과정 단계별 설명
|
||||
- [ ] 모든 @param 태그 작성
|
||||
- [ ] @return 태그 작성 (void 메서드 제외)
|
||||
- [ ] 가능한 예외 @throws 태그 작성
|
||||
- [ ] 보안 관련 주의사항 명시
|
||||
- [ ] 관련 메서드 @see 태그 추가
|
||||
|
||||
### ✅ **HTML 태그 체크리스트**
|
||||
- [ ] 목록은 `<ul>`, `<ol>`, `<li>` 태그 사용
|
||||
- [ ] 강조는 `<b>` 태그 사용
|
||||
- [ ] 단락 구분은 `<p>` 태그 사용
|
||||
- [ ] 코드는 `<code>` 또는 `<pre>` 태그 사용
|
||||
- [ ] 제네릭 타입은 `<`, `>` 사용
|
||||
|
||||
## 📋 도구 및 설정
|
||||
|
||||
### 1. **JavaDoc 생성**
|
||||
```bash
|
||||
# Gradle 프로젝트
|
||||
./gradlew javadoc
|
||||
|
||||
# Maven 프로젝트
|
||||
mvn javadoc:javadoc
|
||||
|
||||
# 직접 실행
|
||||
javadoc -d docs -cp classpath src/**/*.java
|
||||
```
|
||||
|
||||
### 2. **IDE 설정**
|
||||
- **IntelliJ IDEA**: Settings > Editor > Code Style > Java > JavaDoc
|
||||
- **Eclipse**: Window > Preferences > Java > Code Style > Code Templates
|
||||
- **VS Code**: Java Extension Pack + JavaDoc 플러그인
|
||||
|
||||
### 3. **정적 분석 도구**
|
||||
- **Checkstyle**: JavaDoc 누락 검사
|
||||
- **SpotBugs**: 주석 품질 검사
|
||||
- **SonarQube**: 문서화 품질 메트릭
|
||||
|
||||
## 📋 참고 자료
|
||||
|
||||
- [Oracle JavaDoc 가이드](https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javadoc.html)
|
||||
- [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html)
|
||||
- [Spring Framework 주석 스타일](https://github.com/spring-projects/spring-framework/wiki/Code-Style)
|
||||
|
||||
---
|
||||
|
||||
> **💡 팁**: 이 가이드를 팀 내에서 공유하고, 코드 리뷰 시 주석 품질도 함께 검토하세요!
|
||||
173
claude/standard_package_structure.md
Normal file
173
claude/standard_package_structure.md
Normal file
@ -0,0 +1,173 @@
|
||||
패키지 구조 표준
|
||||
|
||||
레이어드 아키텍처 패키지 구조
|
||||
|
||||
├── {SERVICE}
|
||||
│ ├── domain
|
||||
│ ├── service
|
||||
│ ├── controller
|
||||
│ ├── dto
|
||||
│ ├── repository
|
||||
│ │ ├── jpa
|
||||
│ │ └── entity
|
||||
│ ├── config
|
||||
└── common
|
||||
├── dto
|
||||
├── util
|
||||
├── response
|
||||
└── exception
|
||||
|
||||
Package명:
|
||||
- com.{ORG}.{ROOT}.{SERVICE}
|
||||
예) com.unicorn.lifesub.mysub, com.unicorn.lifesub.common
|
||||
|
||||
변수:
|
||||
- ORG: 회사 또는 조직명
|
||||
- ROOT: Root Project 명
|
||||
- SERVICE: 서비스명으로 Root Project의 서브 프로젝트임
|
||||
|
||||
|
||||
예시
|
||||
|
||||
com.unicorn.lifesub.member
|
||||
├── MemberApplication.java
|
||||
├── controller
|
||||
│ └── MemberController.java
|
||||
├── dto
|
||||
│ ├── LoginRequest.java
|
||||
│ ├── LogoutRequest.java
|
||||
│ └── LogoutResponse.java
|
||||
├── service
|
||||
│ ├── MemberService.java
|
||||
│ └── MemberServiceImpl.java
|
||||
├── domain
|
||||
│ └── Member.java
|
||||
├── repository
|
||||
│ ├── entity
|
||||
│ │ └── MemberEntity.java
|
||||
│ └── jpa
|
||||
│ └── MemberRepository.java
|
||||
└── config
|
||||
├── SecurityConfig.java
|
||||
├── DataLoader.java
|
||||
├── SwaggerConfig.java
|
||||
└── jwt
|
||||
├── JwtAuthenticationFilter.java
|
||||
├── JwtTokenProvider.java
|
||||
└── CustomUserDetailsService.java
|
||||
|
||||
|
||||
클린 아키텍처 패키지 구조
|
||||
|
||||
├── biz
|
||||
│ ├── usecase
|
||||
│ │ ├── in
|
||||
│ │ ├── out
|
||||
│ ├── service
|
||||
│ └── domain
|
||||
│ └── dto
|
||||
├── infra
|
||||
│ ├── controller
|
||||
│ ├── dto
|
||||
│ ├── gateway
|
||||
│ │ ├── repository
|
||||
│ │ └── entity
|
||||
│ └── config
|
||||
|
||||
|
||||
Package명:
|
||||
- com.{ORG}.{ROOT}.{SERVICE}.biz
|
||||
- com.{ORG}.{ROOT}.{SERVICE}.infra
|
||||
예) com.unicorn.lifesub.mysub.biz, com.unicorn.lifesub.common
|
||||
|
||||
변수:
|
||||
- ORG: 회사 또는 조직명
|
||||
- ROOT: Root Project 명
|
||||
- SERVICE: 서비스명으로 Root Project의 서브 프로젝트임
|
||||
|
||||
예시
|
||||
|
||||
|
||||
com.unicorn.lifesub.mysub
|
||||
├── biz
|
||||
│ ├── dto
|
||||
│ │ ├── CategoryResponse.java
|
||||
│ │ ├── ServiceListResponse.java
|
||||
│ │ ├── MySubResponse.java
|
||||
│ │ ├── SubDetailResponse.java
|
||||
│ │ └── TotalFeeResponse.java
|
||||
│ ├── service
|
||||
│ │ ├── FeeLevel.java
|
||||
│ │ └── MySubscriptionService.java
|
||||
│ ├── usecase
|
||||
│ │ ├── in
|
||||
│ │ │ ├── CancelSubscriptionUseCase.java
|
||||
│ │ │ ├── CategoryUseCase.java
|
||||
│ │ │ ├── MySubscriptionsUseCase.java
|
||||
│ │ │ ├── SubscribeUseCase.java
|
||||
│ │ │ ├── SubscriptionDetailUseCase.java
|
||||
│ │ │ └── TotalFeeUseCase.java
|
||||
│ │ └── out
|
||||
│ │ ├── MySubscriptionReader.java
|
||||
│ │ ├── MySubscriptionWriter.java
|
||||
│ │ └── SubscriptionReader.java
|
||||
│ └── domain
|
||||
│ ├── Category.java
|
||||
│ ├── MySubscription.java
|
||||
│ └── Subscription.java
|
||||
└── infra
|
||||
├── MySubApplication.java
|
||||
├── controller
|
||||
│ ├── CategoryController.java
|
||||
│ ├── MySubController.java
|
||||
│ └── ServiceController.java
|
||||
├── config
|
||||
│ ├── DataLoader.java
|
||||
│ ├── SecurityConfig.java
|
||||
│ ├── SwaggerConfig.java
|
||||
│ └── jwt
|
||||
│ ├── JwtAuthenticationFilter.java
|
||||
│ └── JwtTokenProvider.java
|
||||
└── gateway
|
||||
├── entity
|
||||
│ ├── CategoryEntity.java
|
||||
│ ├── MySubscriptionEntity.java
|
||||
│ └── SubscriptionEntity.java
|
||||
├── repository
|
||||
│ ├── CategoryJpaRepository.java
|
||||
│ ├── MySubscriptionJpaRepository.java
|
||||
│ └── SubscriptionJpaRepository.java
|
||||
├── MySubscriptionGateway.java
|
||||
└── SubscriptionGateway.java
|
||||
|
||||
|
||||
---
|
||||
|
||||
common 모듈 패키지 구조
|
||||
|
||||
├── common
|
||||
├── dto
|
||||
├── entity
|
||||
├── config
|
||||
├── util
|
||||
└── exception
|
||||
|
||||
|
||||
com.unicorn.lifesub.common
|
||||
├── dto
|
||||
│ ├── ApiResponse.java
|
||||
│ ├── JwtTokenDTO.java
|
||||
│ ├── JwtTokenRefreshDTO.java
|
||||
│ └── JwtTokenVerifyDTO.java
|
||||
├── config
|
||||
│ └── JpaConfig.java
|
||||
├── entity
|
||||
│ └── BaseTimeEntity.java
|
||||
├── aop
|
||||
│ └── LoggingAspect.java
|
||||
└── exception
|
||||
├── ErrorCode.java
|
||||
├── InfraException.java
|
||||
└── BusinessException.java
|
||||
|
||||
|
||||
32
common/build.gradle
Normal file
32
common/build.gradle
Normal file
@ -0,0 +1,32 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'org.springframework.boot'
|
||||
}
|
||||
|
||||
bootJar {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Spring Boot
|
||||
api 'org.springframework.boot:spring-boot-starter-web'
|
||||
api 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
api 'org.springframework.boot:spring-boot-starter-security'
|
||||
api 'org.springframework.boot:spring-boot-starter-validation'
|
||||
api 'org.springframework.boot:spring-boot-starter-aop'
|
||||
|
||||
// JWT
|
||||
api "io.jsonwebtoken:jjwt-api:${jjwtVersion}"
|
||||
|
||||
// Utilities
|
||||
api "org.apache.commons:commons-lang3:${commonsLang3Version}"
|
||||
api "commons-io:commons-io:${commonsIoVersion}"
|
||||
|
||||
// Jackson
|
||||
api 'com.fasterxml.jackson.core:jackson-databind'
|
||||
api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
package com.unicorn.hgzero.common.aop;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 로깅 AOP
|
||||
* Controller, Service 메서드 실행 시 로그를 자동으로 기록
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
public class LoggingAspect {
|
||||
|
||||
/**
|
||||
* Controller 메서드 포인트컷
|
||||
*/
|
||||
@Pointcut("within(@org.springframework.web.bind.annotation.RestController *)")
|
||||
public void controllerPointcut() {
|
||||
// Pointcut for all methods in @RestController classes
|
||||
}
|
||||
|
||||
/**
|
||||
* Service 메서드 포인트컷
|
||||
*/
|
||||
@Pointcut("within(@org.springframework.stereotype.Service *)")
|
||||
public void servicePointcut() {
|
||||
// Pointcut for all methods in @Service classes
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller 메서드 실행 로깅
|
||||
*
|
||||
* @param joinPoint 조인 포인트
|
||||
* @return 메서드 실행 결과
|
||||
* @throws Throwable 예외 발생 시
|
||||
*/
|
||||
@Around("controllerPointcut()")
|
||||
public Object logController(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
String className = joinPoint.getSignature().getDeclaringTypeName();
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
Object[] args = joinPoint.getArgs();
|
||||
|
||||
log.info("[Controller] {}.{} 호출 - 파라미터: {}",
|
||||
className, methodName, Arrays.toString(args));
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
Object result = null;
|
||||
try {
|
||||
result = joinPoint.proceed();
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
log.info("[Controller] {}.{} 완료 - 실행시간: {}ms",
|
||||
className, methodName, executionTime);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
log.error("[Controller] {}.{} 실패 - 실행시간: {}ms, 에러: {}",
|
||||
className, methodName, executionTime, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Service 메서드 실행 로깅
|
||||
*
|
||||
* @param joinPoint 조인 포인트
|
||||
* @return 메서드 실행 결과
|
||||
* @throws Throwable 예외 발생 시
|
||||
*/
|
||||
@Around("servicePointcut()")
|
||||
public Object logService(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
String className = joinPoint.getSignature().getDeclaringTypeName();
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
|
||||
log.debug("[Service] {}.{} 시작", className, methodName);
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
Object result = null;
|
||||
try {
|
||||
result = joinPoint.proceed();
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
log.debug("[Service] {}.{} 완료 - 실행시간: {}ms",
|
||||
className, methodName, executionTime);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
log.error("[Service] {}.{} 실패 - 실행시간: {}ms, 에러: {}",
|
||||
className, methodName, executionTime, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package com.unicorn.hgzero.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
|
||||
/**
|
||||
* JPA 설정
|
||||
* JPA Auditing 기능을 활성화하여 엔티티의 생성일시, 수정일시를 자동으로 관리
|
||||
*/
|
||||
@Configuration
|
||||
@EnableJpaAuditing
|
||||
public class JpaConfig {
|
||||
// JPA Auditing 활성화
|
||||
// BaseTimeEntity의 @CreatedDate, @LastModifiedDate가 자동으로 동작
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
package com.unicorn.hgzero.common.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* API 응답 공통 포맷
|
||||
* 모든 API 응답에 사용되는 표준 응답 형식
|
||||
*
|
||||
* @param <T> 응답 데이터 타입
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ApiResponse<T> {
|
||||
|
||||
/**
|
||||
* 응답 상태 (success, error)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 응답 메시지
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 응답 데이터
|
||||
*/
|
||||
private T data;
|
||||
|
||||
/**
|
||||
* 에러 코드 (에러 발생 시)
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 에러 상세 정보 (에러 발생 시)
|
||||
*/
|
||||
private Object details;
|
||||
|
||||
/**
|
||||
* 응답 타임스탬프
|
||||
*/
|
||||
@Builder.Default
|
||||
private LocalDateTime timestamp = LocalDateTime.now();
|
||||
|
||||
/**
|
||||
* 성공 응답 생성
|
||||
*
|
||||
* @param <T> 응답 데이터 타입
|
||||
* @param data 응답 데이터
|
||||
* @return 성공 응답
|
||||
*/
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
return ApiResponse.<T>builder()
|
||||
.status("success")
|
||||
.data(data)
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 성공 응답 생성 (메시지 포함)
|
||||
*
|
||||
* @param <T> 응답 데이터 타입
|
||||
* @param message 성공 메시지
|
||||
* @param data 응답 데이터
|
||||
* @return 성공 응답
|
||||
*/
|
||||
public static <T> ApiResponse<T> success(String message, T data) {
|
||||
return ApiResponse.<T>builder()
|
||||
.status("success")
|
||||
.message(message)
|
||||
.data(data)
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 응답 생성
|
||||
*
|
||||
* @param code 에러 코드
|
||||
* @param message 에러 메시지
|
||||
* @return 에러 응답
|
||||
*/
|
||||
public static ApiResponse<Void> error(String code, String message) {
|
||||
return ApiResponse.<Void>builder()
|
||||
.status("error")
|
||||
.code(code)
|
||||
.message(message)
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 응답 생성 (상세 정보 포함)
|
||||
*
|
||||
* @param code 에러 코드
|
||||
* @param message 에러 메시지
|
||||
* @param details 에러 상세 정보
|
||||
* @return 에러 응답
|
||||
*/
|
||||
public static ApiResponse<Void> error(String code, String message, Object details) {
|
||||
return ApiResponse.<Void>builder()
|
||||
.status("error")
|
||||
.code(code)
|
||||
.message(message)
|
||||
.details(details)
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package com.unicorn.hgzero.common.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* JWT 토큰 DTO
|
||||
* Access Token과 Refresh Token을 함께 반환하는 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class JwtTokenDTO {
|
||||
|
||||
/**
|
||||
* Access Token
|
||||
* 단기 인증 토큰 (기본 1시간)
|
||||
*/
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* Refresh Token
|
||||
* 장기 인증 토큰 (기본 7일)
|
||||
*/
|
||||
private String refreshToken;
|
||||
|
||||
/**
|
||||
* Access Token 유효 기간 (초)
|
||||
*/
|
||||
private Long accessTokenValidity;
|
||||
|
||||
/**
|
||||
* Refresh Token 유효 기간 (초)
|
||||
*/
|
||||
private Long refreshTokenValidity;
|
||||
|
||||
/**
|
||||
* 토큰 타입 (Bearer)
|
||||
*/
|
||||
@Builder.Default
|
||||
private String tokenType = "Bearer";
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package com.unicorn.hgzero.common.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* JWT Token 갱신 요청/응답 DTO
|
||||
* Refresh Token을 사용하여 새로운 Access Token을 발급받을 때 사용
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class JwtTokenRefreshDTO {
|
||||
|
||||
/**
|
||||
* Refresh Token
|
||||
*/
|
||||
private String refreshToken;
|
||||
|
||||
/**
|
||||
* 새로 발급된 Access Token
|
||||
*/
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* Access Token 유효 기간 (초)
|
||||
*/
|
||||
private Long accessTokenValidity;
|
||||
|
||||
/**
|
||||
* 토큰 타입 (Bearer)
|
||||
*/
|
||||
@Builder.Default
|
||||
private String tokenType = "Bearer";
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package com.unicorn.hgzero.common.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* JWT Token 검증 결과 DTO
|
||||
* 토큰 유효성 검증 결과를 반환하는 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class JwtTokenVerifyDTO {
|
||||
|
||||
/**
|
||||
* 토큰 유효 여부
|
||||
*/
|
||||
private Boolean valid;
|
||||
|
||||
/**
|
||||
* 사용자 ID
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 사용자 이름
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 권한
|
||||
*/
|
||||
private String authority;
|
||||
|
||||
/**
|
||||
* 토큰 만료 여부
|
||||
*/
|
||||
private Boolean expired;
|
||||
|
||||
/**
|
||||
* 에러 메시지 (유효하지 않은 경우)
|
||||
*/
|
||||
private String errorMessage;
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.unicorn.hgzero.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;
|
||||
|
||||
/**
|
||||
* 생성일시/수정일시 자동 관리 Entity
|
||||
* 모든 Entity의 기본 클래스로 사용되며, 생성일시와 수정일시를 자동으로 관리
|
||||
*/
|
||||
@Getter
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public abstract class BaseTimeEntity {
|
||||
|
||||
/**
|
||||
* 생성일시
|
||||
* Entity가 처음 생성될 때 자동으로 설정
|
||||
*/
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 수정일시
|
||||
* Entity가 수정될 때마다 자동으로 갱신
|
||||
*/
|
||||
@LastModifiedDate
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
package com.unicorn.hgzero.common.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 비즈니스 로직 예외
|
||||
* 비즈니스 규칙 위반 또는 비즈니스 로직 처리 중 발생하는 예외
|
||||
*/
|
||||
@Getter
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* 에러 코드
|
||||
*/
|
||||
private final ErrorCode errorCode;
|
||||
|
||||
/**
|
||||
* 에러 상세 정보
|
||||
*/
|
||||
private final Object 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, Object 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 = null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package com.unicorn.hgzero.common.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/**
|
||||
* 에러 코드 정의
|
||||
* 시스템 전체에서 사용되는 표준 에러 코드
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum ErrorCode {
|
||||
|
||||
// 공통 에러 (1xxx)
|
||||
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "C001", "잘못된 입력 값입니다."),
|
||||
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "C002", "허용되지 않은 HTTP 메서드입니다."),
|
||||
ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "C003", "요청한 리소스를 찾을 수 없습니다."),
|
||||
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "C004", "서버 내부 오류가 발생했습니다."),
|
||||
INVALID_TYPE_VALUE(HttpStatus.BAD_REQUEST, "C005", "잘못된 타입의 값입니다."),
|
||||
ACCESS_DENIED(HttpStatus.FORBIDDEN, "C006", "접근 권한이 없습니다."),
|
||||
|
||||
// 인증/인가 에러 (2xxx)
|
||||
AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "A001", "인증에 실패했습니다."),
|
||||
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "A002", "유효하지 않은 토큰입니다."),
|
||||
EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "A003", "만료된 토큰입니다."),
|
||||
REFRESH_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "A004", "Refresh Token을 찾을 수 없습니다."),
|
||||
INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "A005", "유효하지 않은 Refresh Token입니다."),
|
||||
ACCOUNT_LOCKED(HttpStatus.UNAUTHORIZED, "A006", "계정이 잠금 상태입니다."),
|
||||
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "A007", "인증이 필요합니다."),
|
||||
|
||||
// 비즈니스 로직 에러 (3xxx)
|
||||
DUPLICATE_RESOURCE(HttpStatus.CONFLICT, "B001", "이미 존재하는 리소스입니다."),
|
||||
INVALID_STATUS_TRANSITION(HttpStatus.BAD_REQUEST, "B002", "유효하지 않은 상태 전환입니다."),
|
||||
RESOURCE_CONFLICT(HttpStatus.CONFLICT, "B003", "리소스 충돌이 발생했습니다."),
|
||||
OPERATION_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "B004", "허용되지 않은 작업입니다."),
|
||||
BUSINESS_RULE_VIOLATION(HttpStatus.BAD_REQUEST, "B005", "비즈니스 규칙 위반입니다."),
|
||||
|
||||
// 외부 시스템 에러 (4xxx)
|
||||
EXTERNAL_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E001", "외부 API 호출 중 오류가 발생했습니다."),
|
||||
DATABASE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E002", "데이터베이스 오류가 발생했습니다."),
|
||||
CACHE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E003", "캐시 오류가 발생했습니다."),
|
||||
MESSAGE_QUEUE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E004", "메시지 큐 오류가 발생했습니다."),
|
||||
STORAGE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E005", "스토리지 오류가 발생했습니다.");
|
||||
|
||||
/**
|
||||
* HTTP 상태 코드
|
||||
*/
|
||||
private final HttpStatus httpStatus;
|
||||
|
||||
/**
|
||||
* 에러 코드
|
||||
*/
|
||||
private final String code;
|
||||
|
||||
/**
|
||||
* 에러 메시지
|
||||
*/
|
||||
private final String message;
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
package com.unicorn.hgzero.common.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 인프라스트럭처 예외
|
||||
* 외부 시스템(데이터베이스, 캐시, 메시지 큐, 스토리지 등) 연동 중 발생하는 예외
|
||||
*/
|
||||
@Getter
|
||||
public class InfraException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* 에러 코드
|
||||
*/
|
||||
private final ErrorCode errorCode;
|
||||
|
||||
/**
|
||||
* 에러 상세 정보
|
||||
*/
|
||||
private final Object 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, Object 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 = null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,134 @@
|
||||
package com.unicorn.hgzero.common.util;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
/**
|
||||
* 날짜/시간 유틸리티 클래스
|
||||
* 날짜와 시간 관련 공통 기능을 제공
|
||||
*/
|
||||
public class DateUtil {
|
||||
|
||||
/**
|
||||
* 기본 날짜 포맷 (yyyy-MM-dd)
|
||||
*/
|
||||
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
/**
|
||||
* 기본 날짜시간 포맷 (yyyy-MM-dd HH:mm:ss)
|
||||
*/
|
||||
public static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/**
|
||||
* ISO 8601 날짜시간 포맷 (yyyy-MM-dd'T'HH:mm:ss)
|
||||
*/
|
||||
public static final DateTimeFormatter ISO_DATETIME_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||
|
||||
private DateUtil() {
|
||||
// Utility class - prevent instantiation
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalDate를 문자열로 변환
|
||||
*
|
||||
* @param date 변환할 날짜
|
||||
* @return 포맷된 날짜 문자열 (yyyy-MM-dd)
|
||||
*/
|
||||
public static String format(LocalDate date) {
|
||||
return date != null ? date.format(DATE_FORMATTER) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalDateTime을 문자열로 변환
|
||||
*
|
||||
* @param dateTime 변환할 날짜시간
|
||||
* @return 포맷된 날짜시간 문자열 (yyyy-MM-dd HH:mm:ss)
|
||||
*/
|
||||
public static String format(LocalDateTime dateTime) {
|
||||
return dateTime != null ? dateTime.format(DATETIME_FORMATTER) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalDateTime을 커스텀 포맷으로 변환
|
||||
*
|
||||
* @param dateTime 변환할 날짜시간
|
||||
* @param formatter 날짜시간 포맷터
|
||||
* @return 포맷된 날짜시간 문자열
|
||||
*/
|
||||
public static String format(LocalDateTime dateTime, DateTimeFormatter formatter) {
|
||||
return dateTime != null ? dateTime.format(formatter) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열을 LocalDate로 변환
|
||||
*
|
||||
* @param dateString 날짜 문자열 (yyyy-MM-dd)
|
||||
* @return LocalDate 객체
|
||||
*/
|
||||
public static LocalDate parseDate(String dateString) {
|
||||
return dateString != null ? LocalDate.parse(dateString, DATE_FORMATTER) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열을 LocalDateTime으로 변환
|
||||
*
|
||||
* @param dateTimeString 날짜시간 문자열 (yyyy-MM-dd HH:mm:ss)
|
||||
* @return LocalDateTime 객체
|
||||
*/
|
||||
public static LocalDateTime parseDateTime(String dateTimeString) {
|
||||
return dateTimeString != null ? LocalDateTime.parse(dateTimeString, DATETIME_FORMATTER) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 날짜 사이의 일수 계산
|
||||
*
|
||||
* @param startDate 시작 날짜
|
||||
* @param endDate 종료 날짜
|
||||
* @return 일수 차이
|
||||
*/
|
||||
public static long daysBetween(LocalDate startDate, LocalDate endDate) {
|
||||
return ChronoUnit.DAYS.between(startDate, endDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 날짜시간 사이의 시간 계산 (시간 단위)
|
||||
*
|
||||
* @param startDateTime 시작 날짜시간
|
||||
* @param endDateTime 종료 날짜시간
|
||||
* @return 시간 차이
|
||||
*/
|
||||
public static long hoursBetween(LocalDateTime startDateTime, LocalDateTime endDateTime) {
|
||||
return ChronoUnit.HOURS.between(startDateTime, endDateTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 날짜시간 사이의 시간 계산 (분 단위)
|
||||
*
|
||||
* @param startDateTime 시작 날짜시간
|
||||
* @param endDateTime 종료 날짜시간
|
||||
* @return 분 차이
|
||||
*/
|
||||
public static long minutesBetween(LocalDateTime startDateTime, LocalDateTime endDateTime) {
|
||||
return ChronoUnit.MINUTES.between(startDateTime, endDateTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 날짜 반환
|
||||
*
|
||||
* @return 현재 날짜
|
||||
*/
|
||||
public static LocalDate now() {
|
||||
return LocalDate.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 날짜시간 반환
|
||||
*
|
||||
* @return 현재 날짜시간
|
||||
*/
|
||||
public static LocalDateTime nowDateTime() {
|
||||
return LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,197 @@
|
||||
package com.unicorn.hgzero.common.util;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* 문자열 유틸리티 클래스
|
||||
* 문자열 관련 공통 기능을 제공
|
||||
*/
|
||||
public class StringUtil {
|
||||
|
||||
private StringUtil() {
|
||||
// Utility class - prevent instantiation
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열이 비어있는지 확인
|
||||
*
|
||||
* @param str 확인할 문자열
|
||||
* @return 비어있으면 true, 아니면 false
|
||||
*/
|
||||
public static boolean isEmpty(String str) {
|
||||
return !StringUtils.hasText(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열이 비어있지 않은지 확인
|
||||
*
|
||||
* @param str 확인할 문자열
|
||||
* @return 비어있지 않으면 true, 아니면 false
|
||||
*/
|
||||
public static boolean isNotEmpty(String str) {
|
||||
return StringUtils.hasText(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열 앞뒤 공백 제거
|
||||
* null-safe 처리
|
||||
*
|
||||
* @param str 입력 문자열
|
||||
* @return 공백이 제거된 문자열 (null인 경우 null 반환)
|
||||
*/
|
||||
public static String trim(String str) {
|
||||
return str != null ? str.trim() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열 앞뒤 공백 제거
|
||||
* null인 경우 빈 문자열 반환
|
||||
*
|
||||
* @param str 입력 문자열
|
||||
* @return 공백이 제거된 문자열 (null인 경우 빈 문자열 반환)
|
||||
*/
|
||||
public static String trimToEmpty(String str) {
|
||||
return str != null ? str.trim() : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열이 null 또는 빈 문자열인 경우 기본값 반환
|
||||
*
|
||||
* @param str 입력 문자열
|
||||
* @param defaultValue 기본값
|
||||
* @return 문자열이 비어있으면 기본값, 아니면 입력 문자열
|
||||
*/
|
||||
public static String defaultIfEmpty(String str, String defaultValue) {
|
||||
return isEmpty(str) ? defaultValue : str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열 마스킹
|
||||
* 이메일, 전화번호 등 민감한 정보를 부분적으로 마스킹
|
||||
*
|
||||
* @param str 마스킹할 문자열
|
||||
* @param visibleStart 앞에서 보일 문자 수
|
||||
* @param visibleEnd 뒤에서 보일 문자 수
|
||||
* @param maskChar 마스킹 문자
|
||||
* @return 마스킹된 문자열
|
||||
*/
|
||||
public static String mask(String str, int visibleStart, int visibleEnd, char maskChar) {
|
||||
if (isEmpty(str) || str.length() <= (visibleStart + visibleEnd)) {
|
||||
return str;
|
||||
}
|
||||
|
||||
int maskLength = str.length() - visibleStart - visibleEnd;
|
||||
String maskedPart = String.valueOf(maskChar).repeat(maskLength);
|
||||
|
||||
return str.substring(0, visibleStart) +
|
||||
maskedPart +
|
||||
str.substring(str.length() - visibleEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* 이메일 마스킹
|
||||
* 예: test@example.com -> te**@example.com
|
||||
*
|
||||
* @param email 마스킹할 이메일
|
||||
* @return 마스킹된 이메일
|
||||
*/
|
||||
public static String maskEmail(String email) {
|
||||
if (isEmpty(email) || !email.contains("@")) {
|
||||
return email;
|
||||
}
|
||||
|
||||
String[] parts = email.split("@");
|
||||
String localPart = parts[0];
|
||||
String domain = parts[1];
|
||||
|
||||
if (localPart.length() <= 2) {
|
||||
return localPart + "@" + domain;
|
||||
}
|
||||
|
||||
String maskedLocal = localPart.substring(0, 2) +
|
||||
"*".repeat(localPart.length() - 2);
|
||||
|
||||
return maskedLocal + "@" + domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 전화번호 마스킹
|
||||
* 예: 010-1234-5678 -> 010-****-5678
|
||||
*
|
||||
* @param phone 마스킹할 전화번호
|
||||
* @return 마스킹된 전화번호
|
||||
*/
|
||||
public static String maskPhone(String phone) {
|
||||
if (isEmpty(phone)) {
|
||||
return phone;
|
||||
}
|
||||
|
||||
// 하이픈 제거
|
||||
String cleanPhone = phone.replaceAll("-", "");
|
||||
|
||||
if (cleanPhone.length() < 8) {
|
||||
return phone;
|
||||
}
|
||||
|
||||
// 뒤 4자리만 보이도록 마스킹
|
||||
String masked = "*".repeat(cleanPhone.length() - 4) +
|
||||
cleanPhone.substring(cleanPhone.length() - 4);
|
||||
|
||||
// 원본 형식에 따라 하이픈 추가
|
||||
if (phone.contains("-")) {
|
||||
if (cleanPhone.length() == 10) {
|
||||
return masked.substring(0, 3) + "-" +
|
||||
masked.substring(3, 6) + "-" +
|
||||
masked.substring(6);
|
||||
} else if (cleanPhone.length() == 11) {
|
||||
return masked.substring(0, 3) + "-" +
|
||||
masked.substring(3, 7) + "-" +
|
||||
masked.substring(7);
|
||||
}
|
||||
}
|
||||
|
||||
return masked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Camel Case를 Snake Case로 변환
|
||||
* 예: userName -> user_name
|
||||
*
|
||||
* @param camelCase Camel Case 문자열
|
||||
* @return Snake Case 문자열
|
||||
*/
|
||||
public static String toSnakeCase(String camelCase) {
|
||||
if (isEmpty(camelCase)) {
|
||||
return camelCase;
|
||||
}
|
||||
|
||||
return camelCase.replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Snake Case를 Camel Case로 변환
|
||||
* 예: user_name -> userName
|
||||
*
|
||||
* @param snakeCase Snake Case 문자열
|
||||
* @return Camel Case 문자열
|
||||
*/
|
||||
public static String toCamelCase(String snakeCase) {
|
||||
if (isEmpty(snakeCase)) {
|
||||
return snakeCase;
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean nextUpper = false;
|
||||
|
||||
for (char c : snakeCase.toCharArray()) {
|
||||
if (c == '_') {
|
||||
nextUpper = true;
|
||||
} else {
|
||||
result.append(nextUpper ? Character.toUpperCase(c) : c);
|
||||
nextUpper = false;
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
419
develop/dev/package-structure.md
Normal file
419
develop/dev/package-structure.md
Normal file
@ -0,0 +1,419 @@
|
||||
# 패키지 구조도
|
||||
|
||||
## 전체 프로젝트 구조
|
||||
|
||||
```
|
||||
HGZero/
|
||||
├── common/ # 공통 모듈
|
||||
├── user/ # User Service (Layered)
|
||||
├── meeting/ # Meeting Service (Clean)
|
||||
├── stt/ # STT Service (Layered)
|
||||
├── ai/ # AI Service (Clean)
|
||||
├── notification/ # Notification Service (Layered)
|
||||
├── settings.gradle # Gradle 설정
|
||||
└── build.gradle # 루트 build.gradle
|
||||
```
|
||||
|
||||
## 1. Common 모듈
|
||||
|
||||
```
|
||||
common/
|
||||
└── src/main/java/com/unicorn/hgzero/common/
|
||||
├── dto/
|
||||
│ ├── ApiResponse.java
|
||||
│ ├── JwtTokenDTO.java
|
||||
│ ├── JwtTokenRefreshDTO.java
|
||||
│ └── JwtTokenVerifyDTO.java
|
||||
├── entity/
|
||||
│ └── BaseTimeEntity.java
|
||||
├── config/
|
||||
│ └── JpaConfig.java
|
||||
├── util/
|
||||
│ ├── DateUtil.java
|
||||
│ └── StringUtil.java
|
||||
├── aop/
|
||||
│ └── LoggingAspect.java
|
||||
└── exception/
|
||||
├── ErrorCode.java
|
||||
├── BusinessException.java
|
||||
└── InfraException.java
|
||||
```
|
||||
|
||||
## 2. User Service (Layered Architecture)
|
||||
|
||||
```
|
||||
user/
|
||||
└── src/main/java/com/unicorn/hgzero/user/
|
||||
├── UserApplication.java
|
||||
├── controller/
|
||||
│ └── UserController.java
|
||||
├── dto/
|
||||
│ ├── LoginRequest.java
|
||||
│ ├── LoginResponse.java
|
||||
│ ├── RefreshTokenRequest.java
|
||||
│ ├── RefreshTokenResponse.java
|
||||
│ ├── LogoutRequest.java
|
||||
│ └── TokenValidateResponse.java
|
||||
├── service/
|
||||
│ ├── UserService.java
|
||||
│ └── UserServiceImpl.java
|
||||
├── domain/
|
||||
│ └── User.java
|
||||
├── repository/
|
||||
│ ├── entity/
|
||||
│ │ └── UserEntity.java
|
||||
│ └── jpa/
|
||||
│ └── UserRepository.java
|
||||
└── config/
|
||||
├── SecurityConfig.java
|
||||
├── SwaggerConfig.java
|
||||
├── LdapConfig.java
|
||||
└── jwt/
|
||||
├── JwtAuthenticationFilter.java
|
||||
├── JwtTokenProvider.java
|
||||
└── UserPrincipal.java
|
||||
```
|
||||
|
||||
## 3. Meeting Service (Clean Architecture)
|
||||
|
||||
```
|
||||
meeting/
|
||||
└── src/main/java/com/unicorn/hgzero/meeting/
|
||||
├── biz/
|
||||
│ ├── domain/
|
||||
│ │ ├── Meeting.java
|
||||
│ │ ├── Minutes.java
|
||||
│ │ ├── MinutesSection.java
|
||||
│ │ ├── Todo.java
|
||||
│ │ ├── Template.java
|
||||
│ │ └── Dashboard.java
|
||||
│ ├── dto/
|
||||
│ │ ├── MeetingDto.java
|
||||
│ │ ├── MinutesDto.java
|
||||
│ │ ├── TodoDto.java
|
||||
│ │ ├── TemplateDto.java
|
||||
│ │ ├── DashboardDto.java
|
||||
│ │ └── WebSocketMessageDto.java
|
||||
│ ├── usecase/
|
||||
│ │ ├── in/
|
||||
│ │ │ ├── CreateMeetingUseCase.java
|
||||
│ │ │ ├── SelectTemplateUseCase.java
|
||||
│ │ │ ├── StartMeetingUseCase.java
|
||||
│ │ │ ├── EndMeetingUseCase.java
|
||||
│ │ │ ├── GetMinutesListUseCase.java
|
||||
│ │ │ ├── GetMinutesDetailUseCase.java
|
||||
│ │ │ ├── UpdateMinutesUseCase.java
|
||||
│ │ │ ├── FinalizeMinutesUseCase.java
|
||||
│ │ │ ├── VerifySectionUseCase.java
|
||||
│ │ │ ├── LockSectionUseCase.java
|
||||
│ │ │ ├── UnlockSectionUseCase.java
|
||||
│ │ │ ├── CreateTodoUseCase.java
|
||||
│ │ │ ├── CompleteTodoUseCase.java
|
||||
│ │ │ ├── GetTemplateListUseCase.java
|
||||
│ │ │ ├── GetTemplateDetailUseCase.java
|
||||
│ │ │ └── GetDashboardUseCase.java
|
||||
│ │ └── out/
|
||||
│ │ ├── MeetingReader.java
|
||||
│ │ ├── MeetingWriter.java
|
||||
│ │ ├── MinutesReader.java
|
||||
│ │ ├── MinutesWriter.java
|
||||
│ │ ├── TodoReader.java
|
||||
│ │ ├── TodoWriter.java
|
||||
│ │ ├── TemplateReader.java
|
||||
│ │ ├── CacheManager.java
|
||||
│ │ └── EventPublisher.java
|
||||
│ └── service/
|
||||
│ ├── MeetingService.java
|
||||
│ ├── MinutesService.java
|
||||
│ ├── TodoService.java
|
||||
│ ├── TemplateService.java
|
||||
│ ├── DashboardService.java
|
||||
│ └── WebSocketService.java
|
||||
└── infra/
|
||||
├── MeetingApplication.java
|
||||
├── controller/
|
||||
│ ├── MeetingController.java
|
||||
│ ├── MinutesController.java
|
||||
│ ├── TodoController.java
|
||||
│ ├── TemplateController.java
|
||||
│ ├── DashboardController.java
|
||||
│ └── WebSocketController.java
|
||||
├── gateway/
|
||||
│ ├── entity/
|
||||
│ │ ├── MeetingEntity.java
|
||||
│ │ ├── MinutesEntity.java
|
||||
│ │ ├── MinutesSectionEntity.java
|
||||
│ │ ├── TodoEntity.java
|
||||
│ │ └── TemplateEntity.java
|
||||
│ ├── repository/
|
||||
│ │ ├── MeetingJpaRepository.java
|
||||
│ │ ├── MinutesJpaRepository.java
|
||||
│ │ ├── TodoJpaRepository.java
|
||||
│ │ └── TemplateJpaRepository.java
|
||||
│ ├── MeetingGateway.java
|
||||
│ ├── MinutesGateway.java
|
||||
│ ├── TodoGateway.java
|
||||
│ ├── TemplateGateway.java
|
||||
│ ├── CacheGateway.java
|
||||
│ └── EventPublisherGateway.java
|
||||
└── config/
|
||||
├── SecurityConfig.java
|
||||
├── SwaggerConfig.java
|
||||
├── WebSocketConfig.java
|
||||
├── RedisConfig.java
|
||||
└── jwt/
|
||||
├── JwtAuthenticationFilter.java
|
||||
├── JwtTokenProvider.java
|
||||
└── UserPrincipal.java
|
||||
```
|
||||
|
||||
## 4. STT Service (Layered Architecture)
|
||||
|
||||
```
|
||||
stt/
|
||||
└── src/main/java/com/unicorn/hgzero/stt/
|
||||
├── SttApplication.java
|
||||
├── controller/
|
||||
│ ├── RecordingController.java
|
||||
│ ├── TranscriptionController.java
|
||||
│ └── SpeakerController.java
|
||||
├── dto/
|
||||
│ ├── RecordingDto.java
|
||||
│ ├── TranscriptionDto.java
|
||||
│ ├── SpeakerDto.java
|
||||
│ └── TranscriptSegmentDto.java
|
||||
├── service/
|
||||
│ ├── RecordingService.java
|
||||
│ ├── RecordingServiceImpl.java
|
||||
│ ├── TranscriptionService.java
|
||||
│ ├── TranscriptionServiceImpl.java
|
||||
│ ├── SpeakerService.java
|
||||
│ └── SpeakerServiceImpl.java
|
||||
├── domain/
|
||||
│ ├── Recording.java
|
||||
│ ├── Transcription.java
|
||||
│ ├── TranscriptSegment.java
|
||||
│ └── Speaker.java
|
||||
├── repository/
|
||||
│ ├── entity/
|
||||
│ │ ├── RecordingEntity.java
|
||||
│ │ ├── TranscriptionEntity.java
|
||||
│ │ └── SpeakerEntity.java
|
||||
│ └── jpa/
|
||||
│ ├── RecordingRepository.java
|
||||
│ ├── TranscriptionRepository.java
|
||||
│ └── SpeakerRepository.java
|
||||
└── config/
|
||||
├── SecurityConfig.java
|
||||
├── SwaggerConfig.java
|
||||
├── AzureSpeechConfig.java
|
||||
├── AzureBlobConfig.java
|
||||
├── WebSocketConfig.java
|
||||
└── jwt/
|
||||
├── JwtAuthenticationFilter.java
|
||||
├── JwtTokenProvider.java
|
||||
└── UserPrincipal.java
|
||||
```
|
||||
|
||||
## 5. AI Service (Clean Architecture)
|
||||
|
||||
```
|
||||
ai/
|
||||
└── src/main/java/com/unicorn/hgzero/ai/
|
||||
├── biz/
|
||||
│ ├── domain/
|
||||
│ │ ├── ProcessedTranscript.java
|
||||
│ │ ├── ExtractedTodo.java
|
||||
│ │ ├── RelatedMinutes.java
|
||||
│ │ ├── Term.java
|
||||
│ │ └── Suggestion.java
|
||||
│ ├── dto/
|
||||
│ │ ├── TranscriptDto.java
|
||||
│ │ ├── TodoDto.java
|
||||
│ │ ├── RelatedMinutesDto.java
|
||||
│ │ ├── TermDto.java
|
||||
│ │ └── SuggestionDto.java
|
||||
│ ├── usecase/
|
||||
│ │ ├── in/
|
||||
│ │ │ ├── ProcessTranscriptUseCase.java
|
||||
│ │ │ ├── ImproveTranscriptUseCase.java
|
||||
│ │ │ ├── ExtractTodoUseCase.java
|
||||
│ │ │ ├── GetRelatedMinutesUseCase.java
|
||||
│ │ │ ├── DetectTermsUseCase.java
|
||||
│ │ │ ├── ExplainTermUseCase.java
|
||||
│ │ │ ├── SuggestDiscussionUseCase.java
|
||||
│ │ │ └── SuggestDecisionUseCase.java
|
||||
│ │ └── out/
|
||||
│ │ ├── TranscriptReader.java
|
||||
│ │ ├── TranscriptWriter.java
|
||||
│ │ ├── TodoWriter.java
|
||||
│ │ ├── RelatedMinutesReader.java
|
||||
│ │ ├── TermReader.java
|
||||
│ │ ├── LlmClient.java
|
||||
│ │ ├── VectorSearchClient.java
|
||||
│ │ └── CacheManager.java
|
||||
│ └── service/
|
||||
│ ├── TranscriptService.java
|
||||
│ ├── TodoService.java
|
||||
│ ├── RelatedMinutesService.java
|
||||
│ ├── TermService.java
|
||||
│ └── SuggestionService.java
|
||||
└── infra/
|
||||
├── AiApplication.java
|
||||
├── controller/
|
||||
│ ├── TranscriptController.java
|
||||
│ ├── TodoController.java
|
||||
│ ├── TermController.java
|
||||
│ └── SuggestionController.java
|
||||
├── gateway/
|
||||
│ ├── entity/
|
||||
│ │ ├── TranscriptEntity.java
|
||||
│ │ └── TermEntity.java
|
||||
│ ├── repository/
|
||||
│ │ ├── TranscriptJpaRepository.java
|
||||
│ │ └── TermJpaRepository.java
|
||||
│ ├── TranscriptGateway.java
|
||||
│ ├── TodoGateway.java
|
||||
│ ├── RelatedMinutesGateway.java
|
||||
│ ├── TermGateway.java
|
||||
│ ├── LlmGateway.java
|
||||
│ ├── VectorSearchGateway.java
|
||||
│ └── CacheGateway.java
|
||||
└── config/
|
||||
├── SecurityConfig.java
|
||||
├── SwaggerConfig.java
|
||||
├── OpenAiConfig.java
|
||||
├── AzureAiSearchConfig.java
|
||||
├── RedisConfig.java
|
||||
└── jwt/
|
||||
├── JwtAuthenticationFilter.java
|
||||
├── JwtTokenProvider.java
|
||||
└── UserPrincipal.java
|
||||
```
|
||||
|
||||
## 6. Notification Service (Layered Architecture)
|
||||
|
||||
```
|
||||
notification/
|
||||
└── src/main/java/com/unicorn/hgzero/notification/
|
||||
├── NotificationApplication.java
|
||||
├── controller/
|
||||
│ ├── NotificationController.java
|
||||
│ └── NotificationSettingsController.java
|
||||
├── dto/
|
||||
│ ├── NotificationDto.java
|
||||
│ ├── NotificationSettingsDto.java
|
||||
│ ├── EmailTemplateDto.java
|
||||
│ └── InvitationNotificationDto.java
|
||||
├── service/
|
||||
│ ├── NotificationService.java
|
||||
│ ├── NotificationServiceImpl.java
|
||||
│ ├── NotificationSettingsService.java
|
||||
│ ├── NotificationSettingsServiceImpl.java
|
||||
│ ├── EmailService.java
|
||||
│ └── EmailServiceImpl.java
|
||||
├── domain/
|
||||
│ ├── Notification.java
|
||||
│ ├── NotificationSettings.java
|
||||
│ └── EmailTemplate.java
|
||||
├── repository/
|
||||
│ ├── entity/
|
||||
│ │ ├── NotificationEntity.java
|
||||
│ │ └── NotificationSettingsEntity.java
|
||||
│ └── jpa/
|
||||
│ ├── NotificationRepository.java
|
||||
│ └── NotificationSettingsRepository.java
|
||||
└── config/
|
||||
├── SecurityConfig.java
|
||||
├── SwaggerConfig.java
|
||||
├── EmailConfig.java
|
||||
├── EventHubConfig.java
|
||||
└── jwt/
|
||||
├── JwtAuthenticationFilter.java
|
||||
├── JwtTokenProvider.java
|
||||
└── UserPrincipal.java
|
||||
```
|
||||
|
||||
## 패키지 네이밍 규칙
|
||||
|
||||
### 공통
|
||||
- **Base Package**: `com.unicorn.hgzero`
|
||||
|
||||
### Layered Architecture (User, STT, Notification)
|
||||
- **Package**: `com.unicorn.hgzero.{service-name}`
|
||||
- **구조**:
|
||||
- controller: REST API 컨트롤러
|
||||
- service: 비즈니스 로직
|
||||
- domain: 도메인 모델
|
||||
- dto: 데이터 전송 객체
|
||||
- repository: 데이터 액세스
|
||||
- entity: JPA 엔티티
|
||||
- jpa: JPA Repository
|
||||
- config: 설정 클래스
|
||||
|
||||
### Clean Architecture (Meeting, AI)
|
||||
- **Base Package**: `com.unicorn.hgzero.{service-name}`
|
||||
- **구조**:
|
||||
- biz: 비즈니스 레이어
|
||||
- domain: 도메인 모델
|
||||
- dto: 비즈니스 DTO
|
||||
- usecase: 유스케이스
|
||||
- in: 입력 포트 (인터페이스)
|
||||
- out: 출력 포트 (인터페이스)
|
||||
- service: 유스케이스 구현체
|
||||
- infra: 인프라스트럭처 레이어
|
||||
- controller: REST API 컨트롤러
|
||||
- gateway: 출력 포트 구현체
|
||||
- entity: JPA 엔티티
|
||||
- repository: JPA Repository
|
||||
- config: 설정 클래스
|
||||
|
||||
## 설정 파일 구조
|
||||
|
||||
```
|
||||
각 서비스/
|
||||
├── src/main/resources/
|
||||
│ ├── application.yml # 메인 설정
|
||||
│ └── application-dev.yml # 개발 환경 설정
|
||||
└── build.gradle # Gradle 빌드 설정
|
||||
```
|
||||
|
||||
## 공통 라이브러리 의존성 (루트 build.gradle에 정의)
|
||||
|
||||
- Spring Boot Starter Web
|
||||
- Spring Boot Starter Data JPA
|
||||
- Spring Boot Starter Security
|
||||
- Spring Boot Starter Actuator
|
||||
- Spring Boot Configuration Processor
|
||||
- Lombok
|
||||
- MapStruct
|
||||
- Springdoc OpenAPI (Swagger)
|
||||
- JWT (jjwt-api, jjwt-impl, jjwt-jackson)
|
||||
- PostgreSQL Driver
|
||||
- Redis (Lettuce)
|
||||
- Apache Commons Lang3
|
||||
- Apache Commons IO
|
||||
|
||||
## 서비스별 추가 의존성
|
||||
|
||||
### User Service
|
||||
- Spring LDAP
|
||||
|
||||
### Meeting Service
|
||||
- Spring WebSocket
|
||||
- Spring Messaging
|
||||
|
||||
### STT Service
|
||||
- Azure Speech SDK
|
||||
- Azure Blob Storage SDK
|
||||
- Spring WebSocket
|
||||
|
||||
### AI Service
|
||||
- OpenAI Java Client
|
||||
- Azure AI Search SDK
|
||||
- Spring AI
|
||||
|
||||
### Notification Service
|
||||
- Spring Mail
|
||||
- Azure Event Hubs SDK
|
||||
- Thymeleaf (Email Template)
|
||||
9
meeting/build.gradle
Normal file
9
meeting/build.gradle
Normal file
@ -0,0 +1,9 @@
|
||||
bootJar {
|
||||
archiveFileName = 'meeting.jar'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// WebSocket
|
||||
implementation 'org.springframework.boot:spring-boot-starter-websocket'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-reactor-netty'
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
package com.unicorn.hgzero.meeting.biz.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 대시보드 도메인 모델
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Dashboard {
|
||||
|
||||
/**
|
||||
* 다가오는 회의 목록
|
||||
*/
|
||||
private List<Meeting> upcomingMeetings;
|
||||
|
||||
/**
|
||||
* 최근 회의록 목록
|
||||
*/
|
||||
private List<Minutes> recentMinutes;
|
||||
|
||||
/**
|
||||
* 할당된 Todo 목록
|
||||
*/
|
||||
private List<Todo> assignedTodos;
|
||||
|
||||
/**
|
||||
* 통계 정보
|
||||
*/
|
||||
private Statistics statistics;
|
||||
|
||||
/**
|
||||
* 통계 정보 내부 클래스
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Statistics {
|
||||
/**
|
||||
* 전체 회의 수
|
||||
*/
|
||||
private Integer totalMeetings;
|
||||
|
||||
/**
|
||||
* 진행 중인 회의 수
|
||||
*/
|
||||
private Integer inProgressMeetings;
|
||||
|
||||
/**
|
||||
* 완료된 회의 수
|
||||
*/
|
||||
private Integer completedMeetings;
|
||||
|
||||
/**
|
||||
* 전체 Todo 수
|
||||
*/
|
||||
private Integer totalTodos;
|
||||
|
||||
/**
|
||||
* 완료된 Todo 수
|
||||
*/
|
||||
private Integer completedTodos;
|
||||
|
||||
/**
|
||||
* 지연된 Todo 수
|
||||
*/
|
||||
private Integer overdueTodos;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
package com.unicorn.hgzero.meeting.biz.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의 도메인 모델
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Meeting {
|
||||
|
||||
/**
|
||||
* 회의 ID
|
||||
*/
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* 회의 제목
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 회의 설명
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 회의 일시
|
||||
*/
|
||||
private LocalDateTime scheduledAt;
|
||||
|
||||
/**
|
||||
* 회의 시작 일시
|
||||
*/
|
||||
private LocalDateTime startedAt;
|
||||
|
||||
/**
|
||||
* 회의 종료 일시
|
||||
*/
|
||||
private LocalDateTime endedAt;
|
||||
|
||||
/**
|
||||
* 회의 상태 (SCHEDULED, IN_PROGRESS, COMPLETED, CANCELLED)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 주최자 ID
|
||||
*/
|
||||
private String organizerId;
|
||||
|
||||
/**
|
||||
* 참석자 ID 목록
|
||||
*/
|
||||
private List<String> participants;
|
||||
|
||||
/**
|
||||
* 템플릿 ID
|
||||
*/
|
||||
private String templateId;
|
||||
|
||||
/**
|
||||
* 회의 시작
|
||||
*/
|
||||
public void start() {
|
||||
this.status = "IN_PROGRESS";
|
||||
this.startedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 종료
|
||||
*/
|
||||
public void end() {
|
||||
this.status = "COMPLETED";
|
||||
this.endedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 취소
|
||||
*/
|
||||
public void cancel() {
|
||||
this.status = "CANCELLED";
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 진행 중 여부 확인
|
||||
*/
|
||||
public boolean isInProgress() {
|
||||
return "IN_PROGRESS".equals(this.status);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
package com.unicorn.hgzero.meeting.biz.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의록 도메인 모델
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Minutes {
|
||||
|
||||
/**
|
||||
* 회의록 ID
|
||||
*/
|
||||
private String minutesId;
|
||||
|
||||
/**
|
||||
* 회의 ID
|
||||
*/
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* 회의록 제목
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 목록
|
||||
*/
|
||||
private List<MinutesSection> sections;
|
||||
|
||||
/**
|
||||
* 회의록 상태 (DRAFT, FINALIZED)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 버전
|
||||
*/
|
||||
private Integer version;
|
||||
|
||||
/**
|
||||
* 작성자 ID
|
||||
*/
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 확정자 ID
|
||||
*/
|
||||
private String finalizedBy;
|
||||
|
||||
/**
|
||||
* 확정 일시
|
||||
*/
|
||||
private LocalDateTime finalizedAt;
|
||||
|
||||
/**
|
||||
* 회의록 확정
|
||||
*/
|
||||
public void finalize(String userId) {
|
||||
this.status = "FINALIZED";
|
||||
this.finalizedBy = userId;
|
||||
this.finalizedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의록 수정
|
||||
*/
|
||||
public void update() {
|
||||
this.version++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의록 확정 여부 확인
|
||||
*/
|
||||
public boolean isFinalized() {
|
||||
return "FINALIZED".equals(this.status);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
package com.unicorn.hgzero.meeting.biz.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 도메인 모델
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MinutesSection {
|
||||
|
||||
/**
|
||||
* 섹션 ID
|
||||
*/
|
||||
private String sectionId;
|
||||
|
||||
/**
|
||||
* 회의록 ID
|
||||
*/
|
||||
private String minutesId;
|
||||
|
||||
/**
|
||||
* 섹션 유형 (AGENDA, DISCUSSION, DECISION, ACTION_ITEM)
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 섹션 제목
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 섹션 내용
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 섹션 순서
|
||||
*/
|
||||
private Integer order;
|
||||
|
||||
/**
|
||||
* 검증 완료 여부
|
||||
*/
|
||||
private Boolean verified;
|
||||
|
||||
/**
|
||||
* 잠금 여부
|
||||
*/
|
||||
private Boolean locked;
|
||||
|
||||
/**
|
||||
* 잠금 사용자 ID
|
||||
*/
|
||||
private String lockedBy;
|
||||
|
||||
/**
|
||||
* 섹션 잠금
|
||||
*/
|
||||
public void lock(String userId) {
|
||||
this.locked = true;
|
||||
this.lockedBy = userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션 잠금 해제
|
||||
*/
|
||||
public void unlock() {
|
||||
this.locked = false;
|
||||
this.lockedBy = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션 검증 완료
|
||||
*/
|
||||
public void verify() {
|
||||
this.verified = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션 잠금 여부 확인
|
||||
*/
|
||||
public boolean isLocked() {
|
||||
return Boolean.TRUE.equals(this.locked);
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션 검증 완료 여부 확인
|
||||
*/
|
||||
public boolean isVerified() {
|
||||
return Boolean.TRUE.equals(this.verified);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
package com.unicorn.hgzero.meeting.biz.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의록 템플릿 도메인 모델
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Template {
|
||||
|
||||
/**
|
||||
* 템플릿 ID
|
||||
*/
|
||||
private String templateId;
|
||||
|
||||
/**
|
||||
* 템플릿 이름
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 템플릿 설명
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 템플릿 카테고리 (GENERAL, TECHNICAL, MANAGEMENT, SALES)
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 템플릿 섹션 목록
|
||||
*/
|
||||
private List<TemplateSection> sections;
|
||||
|
||||
/**
|
||||
* 공개 여부
|
||||
*/
|
||||
private Boolean isPublic;
|
||||
|
||||
/**
|
||||
* 생성자 ID
|
||||
*/
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 템플릿 섹션 내부 클래스
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class TemplateSection {
|
||||
/**
|
||||
* 섹션 유형
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 섹션 제목
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 섹션 순서
|
||||
*/
|
||||
private Integer order;
|
||||
|
||||
/**
|
||||
* 기본 내용
|
||||
*/
|
||||
private String defaultContent;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
package com.unicorn.hgzero.meeting.biz.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Todo 도메인 모델
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Todo {
|
||||
|
||||
/**
|
||||
* Todo ID
|
||||
*/
|
||||
private String todoId;
|
||||
|
||||
/**
|
||||
* 회의록 ID
|
||||
*/
|
||||
private String minutesId;
|
||||
|
||||
/**
|
||||
* 회의 ID
|
||||
*/
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* Todo 제목
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* Todo 설명
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 담당자 ID
|
||||
*/
|
||||
private String assigneeId;
|
||||
|
||||
/**
|
||||
* 마감일
|
||||
*/
|
||||
private LocalDate dueDate;
|
||||
|
||||
/**
|
||||
* Todo 상태 (PENDING, IN_PROGRESS, COMPLETED, CANCELLED)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 우선순위 (HIGH, MEDIUM, LOW)
|
||||
*/
|
||||
private String priority;
|
||||
|
||||
/**
|
||||
* 완료 일시
|
||||
*/
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
/**
|
||||
* Todo 완료
|
||||
*/
|
||||
public void complete() {
|
||||
this.status = "COMPLETED";
|
||||
this.completedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Todo 취소
|
||||
*/
|
||||
public void cancel() {
|
||||
this.status = "CANCELLED";
|
||||
}
|
||||
|
||||
/**
|
||||
* Todo 완료 여부 확인
|
||||
*/
|
||||
public boolean isCompleted() {
|
||||
return "COMPLETED".equals(this.status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 마감일 지남 여부 확인
|
||||
*/
|
||||
public boolean isOverdue() {
|
||||
return this.dueDate != null &&
|
||||
LocalDate.now().isAfter(this.dueDate) &&
|
||||
!isCompleted();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package com.unicorn.hgzero.meeting.biz.service;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Dashboard;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.in.dashboard.GetDashboardUseCase;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.DashboardReader;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 대시보드 Service
|
||||
* Dashboard 관련 모든 UseCase 구현
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardService implements GetDashboardUseCase {
|
||||
|
||||
private final DashboardReader dashboardReader;
|
||||
|
||||
/**
|
||||
* 사용자 대시보드 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Dashboard getDashboard(String userId) {
|
||||
log.debug("Getting dashboard for user: {}", userId);
|
||||
|
||||
return dashboardReader.getDashboardByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 대시보드 (기간 필터) 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Dashboard getDashboardByPeriod(String userId, String period) {
|
||||
log.debug("Getting dashboard for user: {} with period: {}", userId, period);
|
||||
|
||||
return dashboardReader.getDashboardByUserIdAndPeriod(userId, period);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,201 @@
|
||||
package com.unicorn.hgzero.meeting.biz.service;
|
||||
|
||||
import com.unicorn.hgzero.common.exception.BusinessException;
|
||||
import com.unicorn.hgzero.common.exception.ErrorCode;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.*;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingWriter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 회의 Service
|
||||
* Meeting 관련 모든 UseCase 구현
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MeetingService implements
|
||||
CreateMeetingUseCase,
|
||||
StartMeetingUseCase,
|
||||
EndMeetingUseCase,
|
||||
CancelMeetingUseCase,
|
||||
GetMeetingUseCase {
|
||||
|
||||
private final MeetingReader meetingReader;
|
||||
private final MeetingWriter meetingWriter;
|
||||
|
||||
/**
|
||||
* 회의 생성
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Meeting createMeeting(CreateMeetingCommand command) {
|
||||
log.info("Creating meeting: {}", command.title());
|
||||
|
||||
// 회의 ID 생성
|
||||
String meetingId = UUID.randomUUID().toString();
|
||||
|
||||
// 회의 도메인 객체 생성
|
||||
Meeting meeting = Meeting.builder()
|
||||
.meetingId(meetingId)
|
||||
.title(command.title())
|
||||
.description(command.description())
|
||||
.scheduledAt(command.scheduledAt())
|
||||
.status("SCHEDULED")
|
||||
.organizerId(command.organizerId())
|
||||
.participants(command.participants())
|
||||
.templateId(command.templateId())
|
||||
.build();
|
||||
|
||||
// 회의 저장
|
||||
Meeting savedMeeting = meetingWriter.save(meeting);
|
||||
|
||||
log.info("Meeting created successfully: {}", savedMeeting.getMeetingId());
|
||||
return savedMeeting;
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 시작
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Meeting startMeeting(String meetingId) {
|
||||
log.info("Starting meeting: {}", meetingId);
|
||||
|
||||
// 회의 조회
|
||||
Meeting meeting = meetingReader.findById(meetingId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
|
||||
// 회의 상태 검증
|
||||
if (!"SCHEDULED".equals(meeting.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||
}
|
||||
|
||||
// 회의 시작
|
||||
meeting.start();
|
||||
|
||||
// 저장
|
||||
Meeting updatedMeeting = meetingWriter.save(meeting);
|
||||
|
||||
log.info("Meeting started successfully: {}", meetingId);
|
||||
return updatedMeeting;
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 종료
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Meeting endMeeting(String meetingId) {
|
||||
log.info("Ending meeting: {}", meetingId);
|
||||
|
||||
// 회의 조회
|
||||
Meeting meeting = meetingReader.findById(meetingId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
|
||||
// 회의 상태 검증
|
||||
if (!"IN_PROGRESS".equals(meeting.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||
}
|
||||
|
||||
// 회의 종료
|
||||
meeting.end();
|
||||
|
||||
// 저장
|
||||
Meeting updatedMeeting = meetingWriter.save(meeting);
|
||||
|
||||
log.info("Meeting ended successfully: {}", meetingId);
|
||||
return updatedMeeting;
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 취소
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Meeting cancelMeeting(String meetingId) {
|
||||
log.info("Canceling meeting: {}", meetingId);
|
||||
|
||||
// 회의 조회
|
||||
Meeting meeting = meetingReader.findById(meetingId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
|
||||
// 회의 취소 가능 상태 검증
|
||||
if ("COMPLETED".equals(meeting.getStatus()) || "CANCELLED".equals(meeting.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||
}
|
||||
|
||||
// 회의 취소
|
||||
meeting.cancel();
|
||||
|
||||
// 저장
|
||||
Meeting updatedMeeting = meetingWriter.save(meeting);
|
||||
|
||||
log.info("Meeting cancelled successfully: {}", meetingId);
|
||||
return updatedMeeting;
|
||||
}
|
||||
|
||||
/**
|
||||
* ID로 회의 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Meeting getMeeting(String meetingId) {
|
||||
log.debug("Getting meeting: {}", meetingId);
|
||||
|
||||
return meetingReader.findById(meetingId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
}
|
||||
|
||||
/**
|
||||
* 주최자 ID로 회의 목록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Meeting> getMeetingsByOrganizer(String organizerId) {
|
||||
log.debug("Getting meetings by organizer: {}", organizerId);
|
||||
|
||||
return meetingReader.findByOrganizerId(organizerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태로 회의 목록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Meeting> getMeetingsByStatus(String status) {
|
||||
log.debug("Getting meetings by status: {}", status);
|
||||
|
||||
return meetingReader.findByStatus(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 일정 시간 범위로 회의 목록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Meeting> getMeetingsByScheduledTime(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
log.debug("Getting meetings by scheduled time: {} ~ {}", startTime, endTime);
|
||||
|
||||
return meetingReader.findByScheduledTimeBetween(startTime, endTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 주최자 ID와 상태로 회의 목록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Meeting> getMeetingsByOrganizerAndStatus(String organizerId, String status) {
|
||||
log.debug("Getting meetings by organizer: {} and status: {}", organizerId, status);
|
||||
|
||||
return meetingReader.findByOrganizerIdAndStatus(organizerId, status);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,234 @@
|
||||
package com.unicorn.hgzero.meeting.biz.service;
|
||||
|
||||
import com.unicorn.hgzero.common.exception.BusinessException;
|
||||
import com.unicorn.hgzero.common.exception.ErrorCode;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.in.section.*;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesSectionReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesSectionWriter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 Service
|
||||
* MinutesSection 관련 모든 UseCase 구현
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MinutesSectionService implements
|
||||
CreateSectionUseCase,
|
||||
UpdateSectionUseCase,
|
||||
DeleteSectionUseCase,
|
||||
LockSectionUseCase,
|
||||
VerifySectionUseCase,
|
||||
GetSectionUseCase {
|
||||
|
||||
private final MinutesSectionReader sectionReader;
|
||||
private final MinutesSectionWriter sectionWriter;
|
||||
|
||||
/**
|
||||
* 섹션 생성
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public MinutesSection createSection(CreateSectionCommand command) {
|
||||
log.info("Creating section for minutes: {}", command.minutesId());
|
||||
|
||||
// 섹션 ID 생성
|
||||
String sectionId = UUID.randomUUID().toString();
|
||||
|
||||
// 섹션 도메인 객체 생성
|
||||
MinutesSection section = MinutesSection.builder()
|
||||
.sectionId(sectionId)
|
||||
.minutesId(command.minutesId())
|
||||
.type(command.type())
|
||||
.title(command.title())
|
||||
.content(command.content())
|
||||
.order(command.order())
|
||||
.verified(false)
|
||||
.locked(false)
|
||||
.build();
|
||||
|
||||
// 섹션 저장
|
||||
MinutesSection savedSection = sectionWriter.save(section);
|
||||
|
||||
log.info("Section created successfully: {}", savedSection.getSectionId());
|
||||
return savedSection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션 내용 수정
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public MinutesSection updateSection(UpdateSectionCommand command) {
|
||||
log.info("Updating section: {}", command.sectionId());
|
||||
|
||||
// 섹션 조회
|
||||
MinutesSection section = sectionReader.findById(command.sectionId())
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
|
||||
// 잠금 상태 검증
|
||||
if (Boolean.TRUE.equals(section.getLocked())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||
}
|
||||
|
||||
// 검증 상태 검증
|
||||
if (Boolean.TRUE.equals(section.getVerified())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||
}
|
||||
|
||||
// 섹션 수정
|
||||
section.update(command.title(), command.content());
|
||||
|
||||
// 저장
|
||||
MinutesSection updatedSection = sectionWriter.save(section);
|
||||
|
||||
log.info("Section updated successfully: {}", command.sectionId());
|
||||
return updatedSection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션 삭제
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public void deleteSection(String sectionId) {
|
||||
log.info("Deleting section: {}", sectionId);
|
||||
|
||||
// 섹션 조회
|
||||
MinutesSection section = sectionReader.findById(sectionId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
|
||||
// 검증 상태 확인 (검증된 섹션은 삭제 불가)
|
||||
if (Boolean.TRUE.equals(section.getVerified())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||
}
|
||||
|
||||
// 섹션 삭제
|
||||
sectionWriter.delete(sectionId);
|
||||
|
||||
log.info("Section deleted successfully: {}", sectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션 잠금
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public MinutesSection lockSection(String sectionId, String userId) {
|
||||
log.info("Locking section: {} by user: {}", sectionId, userId);
|
||||
|
||||
// 섹션 조회
|
||||
MinutesSection section = sectionReader.findById(sectionId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
|
||||
// 이미 잠금 상태 확인
|
||||
if (Boolean.TRUE.equals(section.getLocked())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||
}
|
||||
|
||||
// 섹션 잠금
|
||||
section.lock(userId);
|
||||
|
||||
// 저장
|
||||
MinutesSection lockedSection = sectionWriter.save(section);
|
||||
|
||||
log.info("Section locked successfully: {}", sectionId);
|
||||
return lockedSection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션 잠금 해제
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public MinutesSection unlockSection(String sectionId) {
|
||||
log.info("Unlocking section: {}", sectionId);
|
||||
|
||||
// 섹션 조회
|
||||
MinutesSection section = sectionReader.findById(sectionId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
|
||||
// 잠금 상태 확인
|
||||
if (Boolean.FALSE.equals(section.getLocked())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||
}
|
||||
|
||||
// 섹션 잠금 해제
|
||||
section.unlock();
|
||||
|
||||
// 저장
|
||||
MinutesSection unlockedSection = sectionWriter.save(section);
|
||||
|
||||
log.info("Section unlocked successfully: {}", sectionId);
|
||||
return unlockedSection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 섹션 검증
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public MinutesSection verifySection(String sectionId) {
|
||||
log.info("Verifying section: {}", sectionId);
|
||||
|
||||
// 섹션 조회
|
||||
MinutesSection section = sectionReader.findById(sectionId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
|
||||
// 이미 검증 상태 확인
|
||||
if (Boolean.TRUE.equals(section.getVerified())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||
}
|
||||
|
||||
// 섹션 검증
|
||||
section.verify();
|
||||
|
||||
// 저장
|
||||
MinutesSection verifiedSection = sectionWriter.save(section);
|
||||
|
||||
log.info("Section verified successfully: {}", sectionId);
|
||||
return verifiedSection;
|
||||
}
|
||||
|
||||
/**
|
||||
* ID로 섹션 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public MinutesSection getSection(String sectionId) {
|
||||
log.debug("Getting section: {}", sectionId);
|
||||
|
||||
return sectionReader.findById(sectionId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의록 ID로 섹션 목록 조회 (순서대로)
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<MinutesSection> getSectionsByMinutes(String minutesId) {
|
||||
log.debug("Getting sections by minutes: {}", minutesId);
|
||||
|
||||
return sectionReader.findByMinutesIdOrderByOrder(minutesId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의록 ID와 타입으로 섹션 목록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<MinutesSection> getSectionsByMinutesAndType(String minutesId, String type) {
|
||||
log.debug("Getting sections by minutes: {} and type: {}", minutesId, type);
|
||||
|
||||
return sectionReader.findByMinutesIdAndType(minutesId, type);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,193 @@
|
||||
package com.unicorn.hgzero.meeting.biz.service;
|
||||
|
||||
import com.unicorn.hgzero.common.exception.BusinessException;
|
||||
import com.unicorn.hgzero.common.exception.ErrorCode;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.in.minutes.*;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesWriter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 회의록 Service
|
||||
* Minutes 관련 모든 UseCase 구현
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MinutesService implements
|
||||
CreateMinutesUseCase,
|
||||
UpdateMinutesUseCase,
|
||||
FinalizeMinutesUseCase,
|
||||
GetMinutesUseCase {
|
||||
|
||||
private final MinutesReader minutesReader;
|
||||
private final MinutesWriter minutesWriter;
|
||||
|
||||
/**
|
||||
* 회의록 생성
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Minutes createMinutes(CreateMinutesCommand command) {
|
||||
log.info("Creating minutes for meeting: {}", command.meetingId());
|
||||
|
||||
// 회의록 ID 생성
|
||||
String minutesId = UUID.randomUUID().toString();
|
||||
|
||||
// 회의록 도메인 객체 생성
|
||||
Minutes minutes = Minutes.builder()
|
||||
.minutesId(minutesId)
|
||||
.meetingId(command.meetingId())
|
||||
.title(command.title())
|
||||
.status("DRAFT")
|
||||
.version(1)
|
||||
.createdBy(command.createdBy())
|
||||
.build();
|
||||
|
||||
// 회의록 저장
|
||||
Minutes savedMinutes = minutesWriter.save(minutes);
|
||||
|
||||
log.info("Minutes created successfully: {}", savedMinutes.getMinutesId());
|
||||
return savedMinutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의록 제목 수정
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Minutes updateMinutesTitle(String minutesId, String title) {
|
||||
log.info("Updating minutes title: {}", minutesId);
|
||||
|
||||
// 회의록 조회
|
||||
Minutes minutes = minutesReader.findById(minutesId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
|
||||
// 상태 검증 (확정된 회의록은 수정 불가)
|
||||
if ("FINALIZED".equals(minutes.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||
}
|
||||
|
||||
// 제목 수정
|
||||
minutes.update(title, minutes.getSections());
|
||||
|
||||
// 저장
|
||||
Minutes updatedMinutes = minutesWriter.save(minutes);
|
||||
|
||||
log.info("Minutes title updated successfully: {}", minutesId);
|
||||
return updatedMinutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의록 버전 증가
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Minutes incrementVersion(String minutesId) {
|
||||
log.info("Incrementing minutes version: {}", minutesId);
|
||||
|
||||
// 회의록 조회
|
||||
Minutes minutes = minutesReader.findById(minutesId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
|
||||
// 버전 증가
|
||||
minutes.incrementVersion();
|
||||
|
||||
// 저장
|
||||
Minutes updatedMinutes = minutesWriter.save(minutes);
|
||||
|
||||
log.info("Minutes version incremented: {} -> {}", minutesId, updatedMinutes.getVersion());
|
||||
return updatedMinutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의록 확정
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Minutes finalizeMinutes(String minutesId, String userId) {
|
||||
log.info("Finalizing minutes: {}", minutesId);
|
||||
|
||||
// 회의록 조회
|
||||
Minutes minutes = minutesReader.findById(minutesId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
|
||||
// 상태 검증
|
||||
if ("FINALIZED".equals(minutes.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||
}
|
||||
|
||||
// 회의록 확정
|
||||
minutes.finalize(userId);
|
||||
|
||||
// 저장
|
||||
Minutes finalizedMinutes = minutesWriter.save(minutes);
|
||||
|
||||
log.info("Minutes finalized successfully: {}", minutesId);
|
||||
return finalizedMinutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* ID로 회의록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Minutes getMinutes(String minutesId) {
|
||||
log.debug("Getting minutes: {}", minutesId);
|
||||
|
||||
return minutesReader.findById(minutesId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 ID로 회의록 목록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Minutes> getMinutesByMeeting(String meetingId) {
|
||||
log.debug("Getting minutes by meeting: {}", meetingId);
|
||||
|
||||
return minutesReader.findByMeetingId(meetingId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 ID로 최신 회의록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Minutes getLatestMinutes(String meetingId) {
|
||||
log.debug("Getting latest minutes for meeting: {}", meetingId);
|
||||
|
||||
return minutesReader.findLatestByMeetingId(meetingId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
}
|
||||
|
||||
/**
|
||||
* 작성자 ID로 회의록 목록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Minutes> getMinutesByCreator(String createdBy) {
|
||||
log.debug("Getting minutes by creator: {}", createdBy);
|
||||
|
||||
return minutesReader.findByCreatedBy(createdBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태로 회의록 목록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Minutes> getMinutesByStatus(String status) {
|
||||
log.debug("Getting minutes by status: {}", status);
|
||||
|
||||
return minutesReader.findByStatus(status);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
package com.unicorn.hgzero.meeting.biz.service;
|
||||
|
||||
import com.unicorn.hgzero.common.exception.BusinessException;
|
||||
import com.unicorn.hgzero.common.exception.ErrorCode;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Template;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.in.template.CreateTemplateUseCase;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.in.template.GetTemplateUseCase;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.TemplateReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.TemplateWriter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 템플릿 Service
|
||||
* Template 관련 모든 UseCase 구현
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TemplateService implements
|
||||
CreateTemplateUseCase,
|
||||
GetTemplateUseCase {
|
||||
|
||||
private final TemplateReader templateReader;
|
||||
private final TemplateWriter templateWriter;
|
||||
|
||||
/**
|
||||
* 템플릿 생성
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Template createTemplate(CreateTemplateCommand command) {
|
||||
log.info("Creating template: {}", command.name());
|
||||
|
||||
// 템플릿 ID 생성
|
||||
String templateId = UUID.randomUUID().toString();
|
||||
|
||||
// 템플릿 도메인 객체 생성
|
||||
Template template = Template.builder()
|
||||
.templateId(templateId)
|
||||
.name(command.name())
|
||||
.description(command.description())
|
||||
.category(command.category())
|
||||
.sections(command.sections())
|
||||
.isPublic(command.isPublic() != null ? command.isPublic() : true)
|
||||
.createdBy(command.createdBy())
|
||||
.build();
|
||||
|
||||
// 템플릿 저장
|
||||
Template savedTemplate = templateWriter.save(template);
|
||||
|
||||
log.info("Template created successfully: {}", savedTemplate.getTemplateId());
|
||||
return savedTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* ID로 템플릿 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Template getTemplate(String templateId) {
|
||||
log.debug("Getting template: {}", templateId);
|
||||
|
||||
return templateReader.findById(templateId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리로 템플릿 목록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Template> getTemplatesByCategory(String category) {
|
||||
log.debug("Getting templates by category: {}", category);
|
||||
|
||||
return templateReader.findByCategory(category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 공개 템플릿 목록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Template> getPublicTemplates() {
|
||||
log.debug("Getting public templates");
|
||||
|
||||
return templateReader.findByIsPublic(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 작성자 ID로 템플릿 목록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Template> getTemplatesByCreator(String createdBy) {
|
||||
log.debug("Getting templates by creator: {}", createdBy);
|
||||
|
||||
return templateReader.findByCreatedBy(createdBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 이름으로 템플릿 검색
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Template> searchTemplatesByName(String name) {
|
||||
log.debug("Searching templates by name: {}", name);
|
||||
|
||||
return templateReader.findByNameContaining(name);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,219 @@
|
||||
package com.unicorn.hgzero.meeting.biz.service;
|
||||
|
||||
import com.unicorn.hgzero.common.exception.BusinessException;
|
||||
import com.unicorn.hgzero.common.exception.ErrorCode;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Todo;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.in.todo.*;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.TodoReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.TodoWriter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Todo Service
|
||||
* Todo 관련 모든 UseCase 구현
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TodoService implements
|
||||
CreateTodoUseCase,
|
||||
UpdateTodoUseCase,
|
||||
CompleteTodoUseCase,
|
||||
CancelTodoUseCase,
|
||||
GetTodoUseCase {
|
||||
|
||||
private final TodoReader todoReader;
|
||||
private final TodoWriter todoWriter;
|
||||
|
||||
/**
|
||||
* Todo 생성
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Todo createTodo(CreateTodoCommand command) {
|
||||
log.info("Creating todo: {}", command.title());
|
||||
|
||||
// Todo ID 생성
|
||||
String todoId = UUID.randomUUID().toString();
|
||||
|
||||
// Todo 도메인 객체 생성
|
||||
Todo todo = Todo.builder()
|
||||
.todoId(todoId)
|
||||
.minutesId(command.minutesId())
|
||||
.meetingId(command.meetingId())
|
||||
.title(command.title())
|
||||
.description(command.description())
|
||||
.assigneeId(command.assigneeId())
|
||||
.dueDate(command.dueDate())
|
||||
.status("PENDING")
|
||||
.priority(command.priority() != null ? command.priority() : "MEDIUM")
|
||||
.build();
|
||||
|
||||
// Todo 저장
|
||||
Todo savedTodo = todoWriter.save(todo);
|
||||
|
||||
log.info("Todo created successfully: {}", savedTodo.getTodoId());
|
||||
return savedTodo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Todo 수정
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Todo updateTodo(UpdateTodoCommand command) {
|
||||
log.info("Updating todo: {}", command.todoId());
|
||||
|
||||
// Todo 조회
|
||||
Todo todo = todoReader.findById(command.todoId())
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
|
||||
// 완료/취소 상태 검증
|
||||
if ("COMPLETED".equals(todo.getStatus()) || "CANCELLED".equals(todo.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||
}
|
||||
|
||||
// Todo 수정
|
||||
todo.update(
|
||||
command.title(),
|
||||
command.description(),
|
||||
command.assigneeId(),
|
||||
command.dueDate(),
|
||||
command.priority()
|
||||
);
|
||||
|
||||
// 저장
|
||||
Todo updatedTodo = todoWriter.save(todo);
|
||||
|
||||
log.info("Todo updated successfully: {}", command.todoId());
|
||||
return updatedTodo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Todo 완료
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Todo completeTodo(String todoId) {
|
||||
log.info("Completing todo: {}", todoId);
|
||||
|
||||
// Todo 조회
|
||||
Todo todo = todoReader.findById(todoId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
|
||||
// 상태 검증
|
||||
if ("COMPLETED".equals(todo.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||
}
|
||||
|
||||
// Todo 완료
|
||||
todo.complete();
|
||||
|
||||
// 저장
|
||||
Todo completedTodo = todoWriter.save(todo);
|
||||
|
||||
log.info("Todo completed successfully: {}", todoId);
|
||||
return completedTodo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Todo 취소
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Todo cancelTodo(String todoId) {
|
||||
log.info("Cancelling todo: {}", todoId);
|
||||
|
||||
// Todo 조회
|
||||
Todo todo = todoReader.findById(todoId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
|
||||
// 상태 검증
|
||||
if ("COMPLETED".equals(todo.getStatus()) || "CANCELLED".equals(todo.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||
}
|
||||
|
||||
// Todo 취소
|
||||
todo.cancel();
|
||||
|
||||
// 저장
|
||||
Todo cancelledTodo = todoWriter.save(todo);
|
||||
|
||||
log.info("Todo cancelled successfully: {}", todoId);
|
||||
return cancelledTodo;
|
||||
}
|
||||
|
||||
/**
|
||||
* ID로 Todo 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Todo getTodo(String todoId) {
|
||||
log.debug("Getting todo: {}", todoId);
|
||||
|
||||
return todoReader.findById(todoId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 ID로 Todo 목록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Todo> getTodosByMeeting(String meetingId) {
|
||||
log.debug("Getting todos by meeting: {}", meetingId);
|
||||
|
||||
return todoReader.findByMeetingId(meetingId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의록 ID로 Todo 목록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Todo> getTodosByMinutes(String minutesId) {
|
||||
log.debug("Getting todos by minutes: {}", minutesId);
|
||||
|
||||
return todoReader.findByMinutesId(minutesId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 담당자 ID로 Todo 목록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Todo> getTodosByAssignee(String assigneeId) {
|
||||
log.debug("Getting todos by assignee: {}", assigneeId);
|
||||
|
||||
return todoReader.findByAssigneeId(assigneeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 담당자 ID와 상태로 Todo 목록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Todo> getTodosByAssigneeAndStatus(String assigneeId, String status) {
|
||||
log.debug("Getting todos by assignee: {} and status: {}", assigneeId, status);
|
||||
|
||||
return todoReader.findByAssigneeIdAndStatus(assigneeId, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 담당자 ID와 마감일 범위로 Todo 목록 조회
|
||||
*/
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<Todo> getTodosByAssigneeAndDueDateRange(String assigneeId, LocalDate startDate, LocalDate endDate) {
|
||||
log.debug("Getting todos by assignee: {} and due date range: {} ~ {}", assigneeId, startDate, endDate);
|
||||
|
||||
return todoReader.findByAssigneeIdAndDueDateBetween(assigneeId, startDate, endDate);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.dashboard;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Dashboard;
|
||||
|
||||
/**
|
||||
* 대시보드 조회 UseCase
|
||||
*/
|
||||
public interface GetDashboardUseCase {
|
||||
|
||||
/**
|
||||
* 사용자 대시보드 조회
|
||||
*/
|
||||
Dashboard getDashboard(String userId);
|
||||
|
||||
/**
|
||||
* 사용자 대시보드 (기간 필터) 조회
|
||||
*/
|
||||
Dashboard getDashboardByPeriod(String userId, String period);
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
|
||||
/**
|
||||
* 회의 취소 UseCase
|
||||
*/
|
||||
public interface CancelMeetingUseCase {
|
||||
|
||||
/**
|
||||
* 회의 취소
|
||||
*/
|
||||
Meeting cancelMeeting(String meetingId);
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의 생성 UseCase
|
||||
*/
|
||||
public interface CreateMeetingUseCase {
|
||||
|
||||
/**
|
||||
* 회의 생성
|
||||
*/
|
||||
Meeting createMeeting(CreateMeetingCommand command);
|
||||
|
||||
/**
|
||||
* 회의 생성 명령
|
||||
*/
|
||||
record CreateMeetingCommand(
|
||||
String title,
|
||||
String description,
|
||||
LocalDateTime scheduledAt,
|
||||
String organizerId,
|
||||
List<String> participants,
|
||||
String templateId
|
||||
) {}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
|
||||
/**
|
||||
* 회의 종료 UseCase
|
||||
*/
|
||||
public interface EndMeetingUseCase {
|
||||
|
||||
/**
|
||||
* 회의 종료
|
||||
*/
|
||||
Meeting endMeeting(String meetingId);
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의 조회 UseCase
|
||||
*/
|
||||
public interface GetMeetingUseCase {
|
||||
|
||||
/**
|
||||
* ID로 회의 조회
|
||||
*/
|
||||
Meeting getMeeting(String meetingId);
|
||||
|
||||
/**
|
||||
* 주최자 ID로 회의 목록 조회
|
||||
*/
|
||||
List<Meeting> getMeetingsByOrganizer(String organizerId);
|
||||
|
||||
/**
|
||||
* 상태로 회의 목록 조회
|
||||
*/
|
||||
List<Meeting> getMeetingsByStatus(String status);
|
||||
|
||||
/**
|
||||
* 일정 시간 범위로 회의 목록 조회
|
||||
*/
|
||||
List<Meeting> getMeetingsByScheduledTime(LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 주최자 ID와 상태로 회의 목록 조회
|
||||
*/
|
||||
List<Meeting> getMeetingsByOrganizerAndStatus(String organizerId, String status);
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
|
||||
/**
|
||||
* 회의 시작 UseCase
|
||||
*/
|
||||
public interface StartMeetingUseCase {
|
||||
|
||||
/**
|
||||
* 회의 시작
|
||||
*/
|
||||
Meeting startMeeting(String meetingId);
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.minutes;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
|
||||
|
||||
/**
|
||||
* 회의록 생성 UseCase
|
||||
*/
|
||||
public interface CreateMinutesUseCase {
|
||||
|
||||
/**
|
||||
* 회의록 생성
|
||||
*/
|
||||
Minutes createMinutes(CreateMinutesCommand command);
|
||||
|
||||
/**
|
||||
* 회의록 생성 명령
|
||||
*/
|
||||
record CreateMinutesCommand(
|
||||
String meetingId,
|
||||
String title,
|
||||
String createdBy
|
||||
) {}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.minutes;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
|
||||
|
||||
/**
|
||||
* 회의록 확정 UseCase
|
||||
*/
|
||||
public interface FinalizeMinutesUseCase {
|
||||
|
||||
/**
|
||||
* 회의록 확정
|
||||
*/
|
||||
Minutes finalizeMinutes(String minutesId, String userId);
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.minutes;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의록 조회 UseCase
|
||||
*/
|
||||
public interface GetMinutesUseCase {
|
||||
|
||||
/**
|
||||
* ID로 회의록 조회
|
||||
*/
|
||||
Minutes getMinutes(String minutesId);
|
||||
|
||||
/**
|
||||
* 회의 ID로 회의록 목록 조회
|
||||
*/
|
||||
List<Minutes> getMinutesByMeeting(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의 ID로 최신 회의록 조회
|
||||
*/
|
||||
Minutes getLatestMinutes(String meetingId);
|
||||
|
||||
/**
|
||||
* 작성자 ID로 회의록 목록 조회
|
||||
*/
|
||||
List<Minutes> getMinutesByCreator(String createdBy);
|
||||
|
||||
/**
|
||||
* 상태로 회의록 목록 조회
|
||||
*/
|
||||
List<Minutes> getMinutesByStatus(String status);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.minutes;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
|
||||
|
||||
/**
|
||||
* 회의록 수정 UseCase
|
||||
*/
|
||||
public interface UpdateMinutesUseCase {
|
||||
|
||||
/**
|
||||
* 회의록 제목 수정
|
||||
*/
|
||||
Minutes updateMinutesTitle(String minutesId, String title);
|
||||
|
||||
/**
|
||||
* 회의록 버전 증가
|
||||
*/
|
||||
Minutes incrementVersion(String minutesId);
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.section;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 생성 UseCase
|
||||
*/
|
||||
public interface CreateSectionUseCase {
|
||||
|
||||
/**
|
||||
* 섹션 생성
|
||||
*/
|
||||
MinutesSection createSection(CreateSectionCommand command);
|
||||
|
||||
/**
|
||||
* 섹션 생성 명령
|
||||
*/
|
||||
record CreateSectionCommand(
|
||||
String minutesId,
|
||||
String type,
|
||||
String title,
|
||||
String content,
|
||||
Integer order
|
||||
) {}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.section;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 삭제 UseCase
|
||||
*/
|
||||
public interface DeleteSectionUseCase {
|
||||
|
||||
/**
|
||||
* 섹션 삭제
|
||||
*/
|
||||
void deleteSection(String sectionId);
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.section;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 조회 UseCase
|
||||
*/
|
||||
public interface GetSectionUseCase {
|
||||
|
||||
/**
|
||||
* ID로 섹션 조회
|
||||
*/
|
||||
MinutesSection getSection(String sectionId);
|
||||
|
||||
/**
|
||||
* 회의록 ID로 섹션 목록 조회 (순서대로)
|
||||
*/
|
||||
List<MinutesSection> getSectionsByMinutes(String minutesId);
|
||||
|
||||
/**
|
||||
* 회의록 ID와 타입으로 섹션 목록 조회
|
||||
*/
|
||||
List<MinutesSection> getSectionsByMinutesAndType(String minutesId, String type);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.section;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 잠금 UseCase
|
||||
*/
|
||||
public interface LockSectionUseCase {
|
||||
|
||||
/**
|
||||
* 섹션 잠금
|
||||
*/
|
||||
MinutesSection lockSection(String sectionId, String userId);
|
||||
|
||||
/**
|
||||
* 섹션 잠금 해제
|
||||
*/
|
||||
MinutesSection unlockSection(String sectionId);
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.section;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 수정 UseCase
|
||||
*/
|
||||
public interface UpdateSectionUseCase {
|
||||
|
||||
/**
|
||||
* 섹션 내용 수정
|
||||
*/
|
||||
MinutesSection updateSection(UpdateSectionCommand command);
|
||||
|
||||
/**
|
||||
* 섹션 수정 명령
|
||||
*/
|
||||
record UpdateSectionCommand(
|
||||
String sectionId,
|
||||
String title,
|
||||
String content
|
||||
) {}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.section;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 검증 UseCase
|
||||
*/
|
||||
public interface VerifySectionUseCase {
|
||||
|
||||
/**
|
||||
* 섹션 검증
|
||||
*/
|
||||
MinutesSection verifySection(String sectionId);
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.template;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Template;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 템플릿 생성 UseCase
|
||||
*/
|
||||
public interface CreateTemplateUseCase {
|
||||
|
||||
/**
|
||||
* 템플릿 생성
|
||||
*/
|
||||
Template createTemplate(CreateTemplateCommand command);
|
||||
|
||||
/**
|
||||
* 템플릿 생성 명령
|
||||
*/
|
||||
record CreateTemplateCommand(
|
||||
String name,
|
||||
String description,
|
||||
String category,
|
||||
List<Template.TemplateSection> sections,
|
||||
Boolean isPublic,
|
||||
String createdBy
|
||||
) {}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.template;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Template;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 템플릿 조회 UseCase
|
||||
*/
|
||||
public interface GetTemplateUseCase {
|
||||
|
||||
/**
|
||||
* ID로 템플릿 조회
|
||||
*/
|
||||
Template getTemplate(String templateId);
|
||||
|
||||
/**
|
||||
* 카테고리로 템플릿 목록 조회
|
||||
*/
|
||||
List<Template> getTemplatesByCategory(String category);
|
||||
|
||||
/**
|
||||
* 공개 템플릿 목록 조회
|
||||
*/
|
||||
List<Template> getPublicTemplates();
|
||||
|
||||
/**
|
||||
* 작성자 ID로 템플릿 목록 조회
|
||||
*/
|
||||
List<Template> getTemplatesByCreator(String createdBy);
|
||||
|
||||
/**
|
||||
* 이름으로 템플릿 검색
|
||||
*/
|
||||
List<Template> searchTemplatesByName(String name);
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.todo;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Todo;
|
||||
|
||||
/**
|
||||
* Todo 취소 UseCase
|
||||
*/
|
||||
public interface CancelTodoUseCase {
|
||||
|
||||
/**
|
||||
* Todo 취소
|
||||
*/
|
||||
Todo cancelTodo(String todoId);
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.todo;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Todo;
|
||||
|
||||
/**
|
||||
* Todo 완료 UseCase
|
||||
*/
|
||||
public interface CompleteTodoUseCase {
|
||||
|
||||
/**
|
||||
* Todo 완료
|
||||
*/
|
||||
Todo completeTodo(String todoId);
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.todo;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Todo;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* Todo 생성 UseCase
|
||||
*/
|
||||
public interface CreateTodoUseCase {
|
||||
|
||||
/**
|
||||
* Todo 생성
|
||||
*/
|
||||
Todo createTodo(CreateTodoCommand command);
|
||||
|
||||
/**
|
||||
* Todo 생성 명령
|
||||
*/
|
||||
record CreateTodoCommand(
|
||||
String minutesId,
|
||||
String meetingId,
|
||||
String title,
|
||||
String description,
|
||||
String assigneeId,
|
||||
LocalDate dueDate,
|
||||
String priority
|
||||
) {}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.todo;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Todo;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Todo 조회 UseCase
|
||||
*/
|
||||
public interface GetTodoUseCase {
|
||||
|
||||
/**
|
||||
* ID로 Todo 조회
|
||||
*/
|
||||
Todo getTodo(String todoId);
|
||||
|
||||
/**
|
||||
* 회의 ID로 Todo 목록 조회
|
||||
*/
|
||||
List<Todo> getTodosByMeeting(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의록 ID로 Todo 목록 조회
|
||||
*/
|
||||
List<Todo> getTodosByMinutes(String minutesId);
|
||||
|
||||
/**
|
||||
* 담당자 ID로 Todo 목록 조회
|
||||
*/
|
||||
List<Todo> getTodosByAssignee(String assigneeId);
|
||||
|
||||
/**
|
||||
* 담당자 ID와 상태로 Todo 목록 조회
|
||||
*/
|
||||
List<Todo> getTodosByAssigneeAndStatus(String assigneeId, String status);
|
||||
|
||||
/**
|
||||
* 담당자 ID와 마감일 범위로 Todo 목록 조회
|
||||
*/
|
||||
List<Todo> getTodosByAssigneeAndDueDateRange(String assigneeId, LocalDate startDate, LocalDate endDate);
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.todo;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Todo;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* Todo 수정 UseCase
|
||||
*/
|
||||
public interface UpdateTodoUseCase {
|
||||
|
||||
/**
|
||||
* Todo 수정
|
||||
*/
|
||||
Todo updateTodo(UpdateTodoCommand command);
|
||||
|
||||
/**
|
||||
* Todo 수정 명령
|
||||
*/
|
||||
record UpdateTodoCommand(
|
||||
String todoId,
|
||||
String title,
|
||||
String description,
|
||||
String assigneeId,
|
||||
LocalDate dueDate,
|
||||
String priority
|
||||
) {}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Dashboard;
|
||||
|
||||
/**
|
||||
* 대시보드 조회 Gateway Interface (Out Port)
|
||||
*/
|
||||
public interface DashboardReader {
|
||||
|
||||
/**
|
||||
* 사용자 대시보드 조회
|
||||
*/
|
||||
Dashboard getDashboardByUserId(String userId);
|
||||
|
||||
/**
|
||||
* 사용자 대시보드 (기간 필터) 조회
|
||||
*/
|
||||
Dashboard getDashboardByUserIdAndPeriod(String userId, String period);
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 회의 조회 Gateway Interface (Out Port)
|
||||
*/
|
||||
public interface MeetingReader {
|
||||
|
||||
/**
|
||||
* ID로 회의 조회
|
||||
*/
|
||||
Optional<Meeting> findById(String meetingId);
|
||||
|
||||
/**
|
||||
* 주최자 ID로 회의 목록 조회
|
||||
*/
|
||||
List<Meeting> findByOrganizerId(String organizerId);
|
||||
|
||||
/**
|
||||
* 상태로 회의 목록 조회
|
||||
*/
|
||||
List<Meeting> findByStatus(String status);
|
||||
|
||||
/**
|
||||
* 주최자 ID와 상태로 회의 목록 조회
|
||||
*/
|
||||
List<Meeting> findByOrganizerIdAndStatus(String organizerId, String status);
|
||||
|
||||
/**
|
||||
* 일정 시간 범위로 회의 목록 조회
|
||||
*/
|
||||
List<Meeting> findByScheduledTimeBetween(LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 템플릿 ID로 회의 목록 조회
|
||||
*/
|
||||
List<Meeting> findByTemplateId(String templateId);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
|
||||
/**
|
||||
* 회의 저장 Gateway Interface (Out Port)
|
||||
*/
|
||||
public interface MeetingWriter {
|
||||
|
||||
/**
|
||||
* 회의 저장
|
||||
*/
|
||||
Meeting save(Meeting meeting);
|
||||
|
||||
/**
|
||||
* 회의 삭제
|
||||
*/
|
||||
void delete(String meetingId);
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 회의록 조회 Gateway Interface (Out Port)
|
||||
*/
|
||||
public interface MinutesReader {
|
||||
|
||||
/**
|
||||
* ID로 회의록 조회
|
||||
*/
|
||||
Optional<Minutes> findById(String minutesId);
|
||||
|
||||
/**
|
||||
* 회의 ID로 회의록 목록 조회
|
||||
*/
|
||||
List<Minutes> findByMeetingId(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의 ID로 최신 회의록 조회
|
||||
*/
|
||||
Optional<Minutes> findLatestByMeetingId(String meetingId);
|
||||
|
||||
/**
|
||||
* 작성자 ID로 회의록 목록 조회
|
||||
*/
|
||||
List<Minutes> findByCreatedBy(String createdBy);
|
||||
|
||||
/**
|
||||
* 상태로 회의록 목록 조회
|
||||
*/
|
||||
List<Minutes> findByStatus(String status);
|
||||
|
||||
/**
|
||||
* 확정자 ID로 회의록 목록 조회
|
||||
*/
|
||||
List<Minutes> findByFinalizedBy(String finalizedBy);
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 조회 Gateway Interface (Out Port)
|
||||
*/
|
||||
public interface MinutesSectionReader {
|
||||
|
||||
/**
|
||||
* ID로 섹션 조회
|
||||
*/
|
||||
Optional<MinutesSection> findById(String sectionId);
|
||||
|
||||
/**
|
||||
* 회의록 ID로 섹션 목록 조회 (순서대로)
|
||||
*/
|
||||
List<MinutesSection> findByMinutesIdOrderByOrder(String minutesId);
|
||||
|
||||
/**
|
||||
* 회의록 ID와 타입으로 섹션 목록 조회
|
||||
*/
|
||||
List<MinutesSection> findByMinutesIdAndType(String minutesId, String type);
|
||||
|
||||
/**
|
||||
* 회의록 ID와 검증 여부로 섹션 목록 조회
|
||||
*/
|
||||
List<MinutesSection> findByMinutesIdAndVerified(String minutesId, Boolean verified);
|
||||
|
||||
/**
|
||||
* 회의록 ID와 잠금 여부로 섹션 목록 조회
|
||||
*/
|
||||
List<MinutesSection> findByMinutesIdAndLocked(String minutesId, Boolean locked);
|
||||
|
||||
/**
|
||||
* 잠금한 사용자 ID로 섹션 목록 조회
|
||||
*/
|
||||
List<MinutesSection> findByLockedBy(String lockedBy);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 저장 Gateway Interface (Out Port)
|
||||
*/
|
||||
public interface MinutesSectionWriter {
|
||||
|
||||
/**
|
||||
* 섹션 저장
|
||||
*/
|
||||
MinutesSection save(MinutesSection section);
|
||||
|
||||
/**
|
||||
* 섹션 삭제
|
||||
*/
|
||||
void delete(String sectionId);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
|
||||
|
||||
/**
|
||||
* 회의록 저장 Gateway Interface (Out Port)
|
||||
*/
|
||||
public interface MinutesWriter {
|
||||
|
||||
/**
|
||||
* 회의록 저장
|
||||
*/
|
||||
Minutes save(Minutes minutes);
|
||||
|
||||
/**
|
||||
* 회의록 삭제
|
||||
*/
|
||||
void delete(String minutesId);
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Template;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 템플릿 조회 Gateway Interface (Out Port)
|
||||
*/
|
||||
public interface TemplateReader {
|
||||
|
||||
/**
|
||||
* ID로 템플릿 조회
|
||||
*/
|
||||
Optional<Template> findById(String templateId);
|
||||
|
||||
/**
|
||||
* 카테고리로 템플릿 목록 조회
|
||||
*/
|
||||
List<Template> findByCategory(String category);
|
||||
|
||||
/**
|
||||
* 공개 여부로 템플릿 목록 조회
|
||||
*/
|
||||
List<Template> findByIsPublic(Boolean isPublic);
|
||||
|
||||
/**
|
||||
* 작성자 ID로 템플릿 목록 조회
|
||||
*/
|
||||
List<Template> findByCreatedBy(String createdBy);
|
||||
|
||||
/**
|
||||
* 카테고리와 공개 여부로 템플릿 목록 조회
|
||||
*/
|
||||
List<Template> findByCategoryAndIsPublic(String category, Boolean isPublic);
|
||||
|
||||
/**
|
||||
* 이름으로 템플릿 검색 (부분 일치)
|
||||
*/
|
||||
List<Template> findByNameContaining(String name);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Template;
|
||||
|
||||
/**
|
||||
* 템플릿 저장 Gateway Interface (Out Port)
|
||||
*/
|
||||
public interface TemplateWriter {
|
||||
|
||||
/**
|
||||
* 템플릿 저장
|
||||
*/
|
||||
Template save(Template template);
|
||||
|
||||
/**
|
||||
* 템플릿 삭제
|
||||
*/
|
||||
void delete(String templateId);
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Todo;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Todo 조회 Gateway Interface (Out Port)
|
||||
*/
|
||||
public interface TodoReader {
|
||||
|
||||
/**
|
||||
* ID로 Todo 조회
|
||||
*/
|
||||
Optional<Todo> findById(String todoId);
|
||||
|
||||
/**
|
||||
* 회의 ID로 Todo 목록 조회
|
||||
*/
|
||||
List<Todo> findByMeetingId(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의록 ID로 Todo 목록 조회
|
||||
*/
|
||||
List<Todo> findByMinutesId(String minutesId);
|
||||
|
||||
/**
|
||||
* 담당자 ID로 Todo 목록 조회
|
||||
*/
|
||||
List<Todo> findByAssigneeId(String assigneeId);
|
||||
|
||||
/**
|
||||
* 상태로 Todo 목록 조회
|
||||
*/
|
||||
List<Todo> findByStatus(String status);
|
||||
|
||||
/**
|
||||
* 담당자 ID와 상태로 Todo 목록 조회
|
||||
*/
|
||||
List<Todo> findByAssigneeIdAndStatus(String assigneeId, String status);
|
||||
|
||||
/**
|
||||
* 마감일로 Todo 목록 조회
|
||||
*/
|
||||
List<Todo> findByDueDate(LocalDate dueDate);
|
||||
|
||||
/**
|
||||
* 마감일 이전 Todo 목록 조회
|
||||
*/
|
||||
List<Todo> findByDueDateBefore(LocalDate date);
|
||||
|
||||
/**
|
||||
* 우선순위로 Todo 목록 조회
|
||||
*/
|
||||
List<Todo> findByPriority(String priority);
|
||||
|
||||
/**
|
||||
* 담당자 ID와 마감일 범위로 Todo 목록 조회
|
||||
*/
|
||||
List<Todo> findByAssigneeIdAndDueDateBetween(String assigneeId, LocalDate startDate, LocalDate endDate);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Todo;
|
||||
|
||||
/**
|
||||
* Todo 저장 Gateway Interface (Out Port)
|
||||
*/
|
||||
public interface TodoWriter {
|
||||
|
||||
/**
|
||||
* Todo 저장
|
||||
*/
|
||||
Todo save(Todo todo);
|
||||
|
||||
/**
|
||||
* Todo 삭제
|
||||
*/
|
||||
void delete(String todoId);
|
||||
}
|
||||
@ -0,0 +1,160 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Dashboard;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.DashboardReader;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.TodoJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 대시보드 Gateway 구현체
|
||||
* DashboardReader 인터페이스 구현
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardGateway implements DashboardReader {
|
||||
|
||||
private final MeetingJpaRepository meetingJpaRepository;
|
||||
private final MinutesJpaRepository minutesJpaRepository;
|
||||
private final TodoJpaRepository todoJpaRepository;
|
||||
|
||||
@Override
|
||||
public Dashboard getDashboardByUserId(String userId) {
|
||||
log.debug("Getting dashboard for user: {}", userId);
|
||||
|
||||
// 회의 통계 조회
|
||||
long totalMeetings = meetingJpaRepository.findByOrganizerId(userId).size();
|
||||
long scheduledMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "SCHEDULED").size();
|
||||
long inProgressMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "IN_PROGRESS").size();
|
||||
long completedMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "COMPLETED").size();
|
||||
|
||||
// 회의록 통계 조회
|
||||
long totalMinutes = minutesJpaRepository.findByCreatedBy(userId).size();
|
||||
long draftMinutes = minutesJpaRepository.findByCreatedBy(userId).stream()
|
||||
.filter(m -> "DRAFT".equals(m.getStatus()))
|
||||
.count();
|
||||
long finalizedMinutes = minutesJpaRepository.findByCreatedBy(userId).stream()
|
||||
.filter(m -> "FINALIZED".equals(m.getStatus()))
|
||||
.count();
|
||||
|
||||
// Todo 통계 조회
|
||||
long totalTodos = todoJpaRepository.findByAssigneeId(userId).size();
|
||||
long pendingTodos = todoJpaRepository.findByAssigneeIdAndStatus(userId, "PENDING").size();
|
||||
long completedTodos = todoJpaRepository.findByAssigneeIdAndStatus(userId, "COMPLETED").size();
|
||||
long overdueTodos = todoJpaRepository.findByAssigneeId(userId).stream()
|
||||
.filter(todo -> todo.getDueDate() != null
|
||||
&& LocalDate.now().isAfter(todo.getDueDate())
|
||||
&& !"COMPLETED".equals(todo.getStatus()))
|
||||
.count();
|
||||
|
||||
// 통계 객체 생성
|
||||
Dashboard.Statistics statistics = new Dashboard.Statistics(
|
||||
totalMeetings,
|
||||
scheduledMeetings,
|
||||
inProgressMeetings,
|
||||
completedMeetings,
|
||||
totalMinutes,
|
||||
draftMinutes,
|
||||
finalizedMinutes,
|
||||
totalTodos,
|
||||
pendingTodos,
|
||||
completedTodos,
|
||||
overdueTodos
|
||||
);
|
||||
|
||||
// 대시보드 생성
|
||||
return Dashboard.builder()
|
||||
.userId(userId)
|
||||
.statistics(statistics)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dashboard getDashboardByUserIdAndPeriod(String userId, String period) {
|
||||
log.debug("Getting dashboard for user: {} with period: {}", userId, period);
|
||||
|
||||
// 기간 계산
|
||||
LocalDateTime startTime = calculateStartTime(period);
|
||||
LocalDateTime endTime = LocalDateTime.now();
|
||||
|
||||
// 기간 내 회의 통계 조회
|
||||
long totalMeetings = meetingJpaRepository.findByOrganizerId(userId).stream()
|
||||
.filter(m -> m.getScheduledAt().isAfter(startTime) && m.getScheduledAt().isBefore(endTime))
|
||||
.count();
|
||||
|
||||
long scheduledMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "SCHEDULED").stream()
|
||||
.filter(m -> m.getScheduledAt().isAfter(startTime) && m.getScheduledAt().isBefore(endTime))
|
||||
.count();
|
||||
|
||||
long inProgressMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "IN_PROGRESS").stream()
|
||||
.filter(m -> m.getScheduledAt().isAfter(startTime) && m.getScheduledAt().isBefore(endTime))
|
||||
.count();
|
||||
|
||||
long completedMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "COMPLETED").stream()
|
||||
.filter(m -> m.getScheduledAt().isAfter(startTime) && m.getScheduledAt().isBefore(endTime))
|
||||
.count();
|
||||
|
||||
// 회의록 통계 조회 (전체 기간)
|
||||
long totalMinutes = minutesJpaRepository.findByCreatedBy(userId).size();
|
||||
long draftMinutes = minutesJpaRepository.findByCreatedBy(userId).stream()
|
||||
.filter(m -> "DRAFT".equals(m.getStatus()))
|
||||
.count();
|
||||
long finalizedMinutes = minutesJpaRepository.findByCreatedBy(userId).stream()
|
||||
.filter(m -> "FINALIZED".equals(m.getStatus()))
|
||||
.count();
|
||||
|
||||
// Todo 통계 조회 (전체 기간)
|
||||
long totalTodos = todoJpaRepository.findByAssigneeId(userId).size();
|
||||
long pendingTodos = todoJpaRepository.findByAssigneeIdAndStatus(userId, "PENDING").size();
|
||||
long completedTodos = todoJpaRepository.findByAssigneeIdAndStatus(userId, "COMPLETED").size();
|
||||
long overdueTodos = todoJpaRepository.findByAssigneeId(userId).stream()
|
||||
.filter(todo -> todo.getDueDate() != null
|
||||
&& LocalDate.now().isAfter(todo.getDueDate())
|
||||
&& !"COMPLETED".equals(todo.getStatus()))
|
||||
.count();
|
||||
|
||||
// 통계 객체 생성
|
||||
Dashboard.Statistics statistics = new Dashboard.Statistics(
|
||||
totalMeetings,
|
||||
scheduledMeetings,
|
||||
inProgressMeetings,
|
||||
completedMeetings,
|
||||
totalMinutes,
|
||||
draftMinutes,
|
||||
finalizedMinutes,
|
||||
totalTodos,
|
||||
pendingTodos,
|
||||
completedTodos,
|
||||
overdueTodos
|
||||
);
|
||||
|
||||
// 대시보드 생성
|
||||
return Dashboard.builder()
|
||||
.userId(userId)
|
||||
.period(period)
|
||||
.statistics(statistics)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 기간 문자열로부터 시작 시간 계산
|
||||
*/
|
||||
private LocalDateTime calculateStartTime(String period) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
return switch (period.toUpperCase()) {
|
||||
case "WEEK" -> now.minusWeeks(1);
|
||||
case "MONTH" -> now.minusMonths(1);
|
||||
case "QUARTER" -> now.minusMonths(3);
|
||||
case "YEAR" -> now.minusYears(1);
|
||||
default -> now.minusMonths(1); // 기본값: 1개월
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingWriter;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 회의 Gateway 구현체
|
||||
* MeetingReader, MeetingWriter 인터페이스 구현
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MeetingGateway implements MeetingReader, MeetingWriter {
|
||||
|
||||
private final MeetingJpaRepository meetingJpaRepository;
|
||||
|
||||
@Override
|
||||
public Optional<Meeting> findById(String meetingId) {
|
||||
return meetingJpaRepository.findById(meetingId)
|
||||
.map(MeetingEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Meeting> findByOrganizerId(String organizerId) {
|
||||
return meetingJpaRepository.findByOrganizerId(organizerId).stream()
|
||||
.map(MeetingEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Meeting> findByStatus(String status) {
|
||||
return meetingJpaRepository.findByStatus(status).stream()
|
||||
.map(MeetingEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Meeting> findByOrganizerIdAndStatus(String organizerId, String status) {
|
||||
return meetingJpaRepository.findByOrganizerIdAndStatus(organizerId, status).stream()
|
||||
.map(MeetingEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Meeting> findByScheduledTimeBetween(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return meetingJpaRepository.findByScheduledAtBetween(startTime, endTime).stream()
|
||||
.map(MeetingEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Meeting> findByTemplateId(String templateId) {
|
||||
return meetingJpaRepository.findByTemplateId(templateId).stream()
|
||||
.map(MeetingEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Meeting save(Meeting meeting) {
|
||||
MeetingEntity entity = MeetingEntity.fromDomain(meeting);
|
||||
MeetingEntity savedEntity = meetingJpaRepository.save(entity);
|
||||
return savedEntity.toDomain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String meetingId) {
|
||||
meetingJpaRepository.deleteById(meetingId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesWriter;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 회의록 Gateway 구현체
|
||||
* MinutesReader, MinutesWriter 인터페이스 구현
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MinutesGateway implements MinutesReader, MinutesWriter {
|
||||
|
||||
private final MinutesJpaRepository minutesJpaRepository;
|
||||
|
||||
@Override
|
||||
public Optional<Minutes> findById(String minutesId) {
|
||||
return minutesJpaRepository.findById(minutesId)
|
||||
.map(MinutesEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Minutes> findByMeetingId(String meetingId) {
|
||||
return minutesJpaRepository.findByMeetingId(meetingId).stream()
|
||||
.map(MinutesEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Minutes> findLatestByMeetingId(String meetingId) {
|
||||
return minutesJpaRepository.findFirstByMeetingIdOrderByVersionDesc(meetingId)
|
||||
.map(MinutesEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Minutes> findByCreatedBy(String createdBy) {
|
||||
return minutesJpaRepository.findByCreatedBy(createdBy).stream()
|
||||
.map(MinutesEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Minutes> findByStatus(String status) {
|
||||
return minutesJpaRepository.findByStatus(status).stream()
|
||||
.map(MinutesEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Minutes> findByFinalizedBy(String finalizedBy) {
|
||||
return minutesJpaRepository.findByFinalizedBy(finalizedBy).stream()
|
||||
.map(MinutesEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Minutes save(Minutes minutes) {
|
||||
MinutesEntity entity = MinutesEntity.fromDomain(minutes);
|
||||
MinutesEntity savedEntity = minutesJpaRepository.save(entity);
|
||||
return savedEntity.toDomain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String minutesId) {
|
||||
minutesJpaRepository.deleteById(minutesId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesSectionReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesSectionWriter;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesSectionEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesSectionJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 Gateway 구현체
|
||||
* MinutesSectionReader, MinutesSectionWriter 인터페이스 구현
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MinutesSectionGateway implements MinutesSectionReader, MinutesSectionWriter {
|
||||
|
||||
private final MinutesSectionJpaRepository sectionJpaRepository;
|
||||
|
||||
@Override
|
||||
public Optional<MinutesSection> findById(String sectionId) {
|
||||
return sectionJpaRepository.findById(sectionId)
|
||||
.map(MinutesSectionEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MinutesSection> findByMinutesIdOrderByOrder(String minutesId) {
|
||||
return sectionJpaRepository.findByMinutesIdOrderByOrderAsc(minutesId).stream()
|
||||
.map(MinutesSectionEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MinutesSection> findByMinutesIdAndType(String minutesId, String type) {
|
||||
return sectionJpaRepository.findByMinutesIdAndType(minutesId, type).stream()
|
||||
.map(MinutesSectionEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MinutesSection> findByMinutesIdAndVerified(String minutesId, Boolean verified) {
|
||||
return sectionJpaRepository.findByMinutesIdAndVerified(minutesId, verified).stream()
|
||||
.map(MinutesSectionEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MinutesSection> findByMinutesIdAndLocked(String minutesId, Boolean locked) {
|
||||
return sectionJpaRepository.findByMinutesIdAndLocked(minutesId, locked).stream()
|
||||
.map(MinutesSectionEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MinutesSection> findByLockedBy(String lockedBy) {
|
||||
return sectionJpaRepository.findByLockedBy(lockedBy).stream()
|
||||
.map(MinutesSectionEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public MinutesSection save(MinutesSection section) {
|
||||
MinutesSectionEntity entity = MinutesSectionEntity.fromDomain(section);
|
||||
MinutesSectionEntity savedEntity = sectionJpaRepository.save(entity);
|
||||
return savedEntity.toDomain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String sectionId) {
|
||||
sectionJpaRepository.deleteById(sectionId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Template;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.TemplateReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.TemplateWriter;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.TemplateEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.TemplateJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 템플릿 Gateway 구현체
|
||||
* TemplateReader, TemplateWriter 인터페이스 구현
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class TemplateGateway implements TemplateReader, TemplateWriter {
|
||||
|
||||
private final TemplateJpaRepository templateJpaRepository;
|
||||
|
||||
@Override
|
||||
public Optional<Template> findById(String templateId) {
|
||||
return templateJpaRepository.findById(templateId)
|
||||
.map(TemplateEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Template> findByCategory(String category) {
|
||||
return templateJpaRepository.findByCategory(category).stream()
|
||||
.map(TemplateEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Template> findByIsPublic(Boolean isPublic) {
|
||||
return templateJpaRepository.findByIsPublic(isPublic).stream()
|
||||
.map(TemplateEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Template> findByCreatedBy(String createdBy) {
|
||||
return templateJpaRepository.findByCreatedBy(createdBy).stream()
|
||||
.map(TemplateEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Template> findByCategoryAndIsPublic(String category, Boolean isPublic) {
|
||||
return templateJpaRepository.findByCategoryAndIsPublic(category, isPublic).stream()
|
||||
.map(TemplateEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Template> findByNameContaining(String name) {
|
||||
return templateJpaRepository.findByNameContaining(name).stream()
|
||||
.map(TemplateEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Template save(Template template) {
|
||||
TemplateEntity entity = TemplateEntity.fromDomain(template);
|
||||
TemplateEntity savedEntity = templateJpaRepository.save(entity);
|
||||
return savedEntity.toDomain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String templateId) {
|
||||
templateJpaRepository.deleteById(templateId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Todo;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.TodoReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.TodoWriter;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.TodoEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.TodoJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Todo Gateway 구현체
|
||||
* TodoReader, TodoWriter 인터페이스 구현
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class TodoGateway implements TodoReader, TodoWriter {
|
||||
|
||||
private final TodoJpaRepository todoJpaRepository;
|
||||
|
||||
@Override
|
||||
public Optional<Todo> findById(String todoId) {
|
||||
return todoJpaRepository.findById(todoId)
|
||||
.map(TodoEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Todo> findByMeetingId(String meetingId) {
|
||||
return todoJpaRepository.findByMeetingId(meetingId).stream()
|
||||
.map(TodoEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Todo> findByMinutesId(String minutesId) {
|
||||
return todoJpaRepository.findByMinutesId(minutesId).stream()
|
||||
.map(TodoEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Todo> findByAssigneeId(String assigneeId) {
|
||||
return todoJpaRepository.findByAssigneeId(assigneeId).stream()
|
||||
.map(TodoEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Todo> findByStatus(String status) {
|
||||
return todoJpaRepository.findByStatus(status).stream()
|
||||
.map(TodoEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Todo> findByAssigneeIdAndStatus(String assigneeId, String status) {
|
||||
return todoJpaRepository.findByAssigneeIdAndStatus(assigneeId, status).stream()
|
||||
.map(TodoEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Todo> findByDueDate(LocalDate dueDate) {
|
||||
return todoJpaRepository.findByDueDate(dueDate).stream()
|
||||
.map(TodoEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Todo> findByDueDateBefore(LocalDate date) {
|
||||
return todoJpaRepository.findByDueDateBefore(date).stream()
|
||||
.map(TodoEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Todo> findByPriority(String priority) {
|
||||
return todoJpaRepository.findByPriority(priority).stream()
|
||||
.map(TodoEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Todo> findByAssigneeIdAndDueDateBetween(String assigneeId, LocalDate startDate, LocalDate endDate) {
|
||||
return todoJpaRepository.findByAssigneeIdAndDueDateBetween(assigneeId, startDate, endDate).stream()
|
||||
.map(TodoEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Todo save(Todo todo) {
|
||||
TodoEntity entity = TodoEntity.fromDomain(todo);
|
||||
TodoEntity savedEntity = todoJpaRepository.save(entity);
|
||||
return savedEntity.toDomain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String todoId) {
|
||||
todoJpaRepository.deleteById(todoId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway.entity;
|
||||
|
||||
import com.unicorn.hgzero.common.entity.BaseTimeEntity;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 회의 Entity
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "meetings")
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MeetingEntity extends BaseTimeEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "meeting_id", length = 50)
|
||||
private String meetingId;
|
||||
|
||||
@Column(name = "title", length = 200, nullable = false)
|
||||
private String title;
|
||||
|
||||
@Column(name = "description", columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@Column(name = "scheduled_at", nullable = false)
|
||||
private LocalDateTime scheduledAt;
|
||||
|
||||
@Column(name = "started_at")
|
||||
private LocalDateTime startedAt;
|
||||
|
||||
@Column(name = "ended_at")
|
||||
private LocalDateTime endedAt;
|
||||
|
||||
@Column(name = "status", length = 20, nullable = false)
|
||||
@Builder.Default
|
||||
private String status = "SCHEDULED";
|
||||
|
||||
@Column(name = "organizer_id", length = 50, nullable = false)
|
||||
private String organizerId;
|
||||
|
||||
@Column(name = "participants", columnDefinition = "TEXT")
|
||||
private String participants;
|
||||
|
||||
@Column(name = "template_id", length = 50)
|
||||
private String templateId;
|
||||
|
||||
public Meeting toDomain() {
|
||||
return Meeting.builder()
|
||||
.meetingId(this.meetingId)
|
||||
.title(this.title)
|
||||
.description(this.description)
|
||||
.scheduledAt(this.scheduledAt)
|
||||
.startedAt(this.startedAt)
|
||||
.endedAt(this.endedAt)
|
||||
.status(this.status)
|
||||
.organizerId(this.organizerId)
|
||||
.participants(parseParticipants(this.participants))
|
||||
.templateId(this.templateId)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static MeetingEntity fromDomain(Meeting meeting) {
|
||||
return MeetingEntity.builder()
|
||||
.meetingId(meeting.getMeetingId())
|
||||
.title(meeting.getTitle())
|
||||
.description(meeting.getDescription())
|
||||
.scheduledAt(meeting.getScheduledAt())
|
||||
.startedAt(meeting.getStartedAt())
|
||||
.endedAt(meeting.getEndedAt())
|
||||
.status(meeting.getStatus())
|
||||
.organizerId(meeting.getOrganizerId())
|
||||
.participants(formatParticipants(meeting.getParticipants()))
|
||||
.templateId(meeting.getTemplateId())
|
||||
.build();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
this.status = "IN_PROGRESS";
|
||||
this.startedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void end() {
|
||||
this.status = "COMPLETED";
|
||||
this.endedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
private static List<String> parseParticipants(String participants) {
|
||||
if (participants == null || participants.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return Arrays.asList(participants.split(","));
|
||||
}
|
||||
|
||||
private static String formatParticipants(List<String> participants) {
|
||||
if (participants == null || participants.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return String.join(",", participants);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway.entity;
|
||||
|
||||
import com.unicorn.hgzero.common.entity.BaseTimeEntity;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 회의록 Entity
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "minutes")
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MinutesEntity extends BaseTimeEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "minutes_id", length = 50)
|
||||
private String minutesId;
|
||||
|
||||
@Column(name = "meeting_id", length = 50, nullable = false)
|
||||
private String meetingId;
|
||||
|
||||
@Column(name = "title", length = 200, nullable = false)
|
||||
private String title;
|
||||
|
||||
@OneToMany(mappedBy = "minutes", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@OrderBy("order ASC")
|
||||
private List<MinutesSectionEntity> sections = new ArrayList<>();
|
||||
|
||||
@Column(name = "status", length = 20, nullable = false)
|
||||
@Builder.Default
|
||||
private String status = "DRAFT";
|
||||
|
||||
@Column(name = "version", nullable = false)
|
||||
@Builder.Default
|
||||
private Integer version = 1;
|
||||
|
||||
@Column(name = "created_by", length = 50, nullable = false)
|
||||
private String createdBy;
|
||||
|
||||
@Column(name = "finalized_by", length = 50)
|
||||
private String finalizedBy;
|
||||
|
||||
@Column(name = "finalized_at")
|
||||
private LocalDateTime finalizedAt;
|
||||
|
||||
public Minutes toDomain() {
|
||||
return Minutes.builder()
|
||||
.minutesId(this.minutesId)
|
||||
.meetingId(this.meetingId)
|
||||
.title(this.title)
|
||||
.sections(this.sections.stream()
|
||||
.map(MinutesSectionEntity::toDomain)
|
||||
.collect(Collectors.toList()))
|
||||
.status(this.status)
|
||||
.version(this.version)
|
||||
.createdBy(this.createdBy)
|
||||
.finalizedBy(this.finalizedBy)
|
||||
.finalizedAt(this.finalizedAt)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static MinutesEntity fromDomain(Minutes minutes) {
|
||||
return MinutesEntity.builder()
|
||||
.minutesId(minutes.getMinutesId())
|
||||
.meetingId(minutes.getMeetingId())
|
||||
.title(minutes.getTitle())
|
||||
.status(minutes.getStatus())
|
||||
.version(minutes.getVersion())
|
||||
.createdBy(minutes.getCreatedBy())
|
||||
.finalizedBy(minutes.getFinalizedBy())
|
||||
.finalizedAt(minutes.getFinalizedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
public void finalize(String userId) {
|
||||
this.status = "FINALIZED";
|
||||
this.finalizedBy = userId;
|
||||
this.finalizedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void updateVersion() {
|
||||
this.version++;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway.entity;
|
||||
|
||||
import com.unicorn.hgzero.common.entity.BaseTimeEntity;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 Entity
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "minutes_sections")
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MinutesSectionEntity extends BaseTimeEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "section_id", length = 50)
|
||||
private String sectionId;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "minutes_id", nullable = false)
|
||||
private MinutesEntity minutes;
|
||||
|
||||
@Column(name = "minutes_id", insertable = false, updatable = false)
|
||||
private String minutesId;
|
||||
|
||||
@Column(name = "type", length = 50, nullable = false)
|
||||
private String type;
|
||||
|
||||
@Column(name = "title", length = 200, nullable = false)
|
||||
private String title;
|
||||
|
||||
@Column(name = "content", columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@Column(name = "order", nullable = false)
|
||||
private Integer order;
|
||||
|
||||
@Column(name = "verified", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean verified = false;
|
||||
|
||||
@Column(name = "locked", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean locked = false;
|
||||
|
||||
@Column(name = "locked_by", length = 50)
|
||||
private String lockedBy;
|
||||
|
||||
public MinutesSection toDomain() {
|
||||
return MinutesSection.builder()
|
||||
.sectionId(this.sectionId)
|
||||
.minutesId(this.minutesId)
|
||||
.type(this.type)
|
||||
.title(this.title)
|
||||
.content(this.content)
|
||||
.order(this.order)
|
||||
.verified(this.verified)
|
||||
.locked(this.locked)
|
||||
.lockedBy(this.lockedBy)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static MinutesSectionEntity fromDomain(MinutesSection section) {
|
||||
return MinutesSectionEntity.builder()
|
||||
.sectionId(section.getSectionId())
|
||||
.minutesId(section.getMinutesId())
|
||||
.type(section.getType())
|
||||
.title(section.getTitle())
|
||||
.content(section.getContent())
|
||||
.order(section.getOrder())
|
||||
.verified(section.getVerified())
|
||||
.locked(section.getLocked())
|
||||
.lockedBy(section.getLockedBy())
|
||||
.build();
|
||||
}
|
||||
|
||||
public void lock(String userId) {
|
||||
this.locked = true;
|
||||
this.lockedBy = userId;
|
||||
}
|
||||
|
||||
public void unlock() {
|
||||
this.locked = false;
|
||||
this.lockedBy = null;
|
||||
}
|
||||
|
||||
public void verify() {
|
||||
this.verified = true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
package com.unicorn.hgzero.meeting.biz.domain;
|
||||
|
||||
import com.unicorn.hgzero.common.entity.BaseTimeEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 템플릿 Entity
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "templates")
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TemplateEntity extends BaseTimeEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "template_id", length = 50)
|
||||
private String templateId;
|
||||
|
||||
@Column(name = "name", length = 200, nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(name = "description", columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@Column(name = "category", length = 50, nullable = false)
|
||||
private String category;
|
||||
|
||||
@Column(name = "sections", columnDefinition = "TEXT")
|
||||
private String sections;
|
||||
|
||||
@Column(name = "is_public", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean isPublic = true;
|
||||
|
||||
@Column(name = "created_by", length = 50, nullable = false)
|
||||
private String createdBy;
|
||||
|
||||
public Template toDomain() {
|
||||
return Template.builder()
|
||||
.templateId(this.templateId)
|
||||
.name(this.name)
|
||||
.description(this.description)
|
||||
.category(this.category)
|
||||
.isPublic(this.isPublic)
|
||||
.createdBy(this.createdBy)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static TemplateEntity fromDomain(Template template) {
|
||||
return TemplateEntity.builder()
|
||||
.templateId(template.getTemplateId())
|
||||
.name(template.getName())
|
||||
.description(template.getDescription())
|
||||
.category(template.getCategory())
|
||||
.isPublic(template.getIsPublic())
|
||||
.createdBy(template.getCreatedBy())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway.entity;
|
||||
|
||||
import com.unicorn.hgzero.common.entity.BaseTimeEntity;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Todo;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Todo Entity
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "todos")
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TodoEntity extends BaseTimeEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "todo_id", length = 50)
|
||||
private String todoId;
|
||||
|
||||
@Column(name = "minutes_id", length = 50)
|
||||
private String minutesId;
|
||||
|
||||
@Column(name = "meeting_id", length = 50, nullable = false)
|
||||
private String meetingId;
|
||||
|
||||
@Column(name = "title", length = 200, nullable = false)
|
||||
private String title;
|
||||
|
||||
@Column(name = "description", columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@Column(name = "assignee_id", length = 50, nullable = false)
|
||||
private String assigneeId;
|
||||
|
||||
@Column(name = "due_date")
|
||||
private LocalDate dueDate;
|
||||
|
||||
@Column(name = "status", length = 20, nullable = false)
|
||||
@Builder.Default
|
||||
private String status = "PENDING";
|
||||
|
||||
@Column(name = "priority", length = 20)
|
||||
@Builder.Default
|
||||
private String priority = "MEDIUM";
|
||||
|
||||
@Column(name = "completed_at")
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
public Todo toDomain() {
|
||||
return Todo.builder()
|
||||
.todoId(this.todoId)
|
||||
.minutesId(this.minutesId)
|
||||
.meetingId(this.meetingId)
|
||||
.title(this.title)
|
||||
.description(this.description)
|
||||
.assigneeId(this.assigneeId)
|
||||
.dueDate(this.dueDate)
|
||||
.status(this.status)
|
||||
.priority(this.priority)
|
||||
.completedAt(this.completedAt)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static TodoEntity fromDomain(Todo todo) {
|
||||
return TodoEntity.builder()
|
||||
.todoId(todo.getTodoId())
|
||||
.minutesId(todo.getMinutesId())
|
||||
.meetingId(todo.getMeetingId())
|
||||
.title(todo.getTitle())
|
||||
.description(todo.getDescription())
|
||||
.assigneeId(todo.getAssigneeId())
|
||||
.dueDate(todo.getDueDate())
|
||||
.status(todo.getStatus())
|
||||
.priority(todo.getPriority())
|
||||
.completedAt(todo.getCompletedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
public void complete() {
|
||||
this.status = "COMPLETED";
|
||||
this.completedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway.repository;
|
||||
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의 JPA Repository
|
||||
*/
|
||||
@Repository
|
||||
public interface MeetingJpaRepository extends JpaRepository<MeetingEntity, String> {
|
||||
|
||||
/**
|
||||
* 주최자 ID로 회의 목록 조회
|
||||
*/
|
||||
List<MeetingEntity> findByOrganizerId(String organizerId);
|
||||
|
||||
/**
|
||||
* 상태로 회의 목록 조회
|
||||
*/
|
||||
List<MeetingEntity> findByStatus(String status);
|
||||
|
||||
/**
|
||||
* 주최자 ID와 상태로 회의 목록 조회
|
||||
*/
|
||||
List<MeetingEntity> findByOrganizerIdAndStatus(String organizerId, String status);
|
||||
|
||||
/**
|
||||
* 일정 시간 범위로 회의 목록 조회
|
||||
*/
|
||||
List<MeetingEntity> findByScheduledAtBetween(LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
/**
|
||||
* 템플릿 ID로 회의 목록 조회
|
||||
*/
|
||||
List<MeetingEntity> findByTemplateId(String templateId);
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway.repository;
|
||||
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 회의록 JPA Repository
|
||||
*/
|
||||
@Repository
|
||||
public interface MinutesJpaRepository extends JpaRepository<MinutesEntity, String> {
|
||||
|
||||
/**
|
||||
* 회의 ID로 회의록 조회
|
||||
*/
|
||||
List<MinutesEntity> findByMeetingId(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의 ID로 최신 회의록 조회
|
||||
*/
|
||||
Optional<MinutesEntity> findFirstByMeetingIdOrderByVersionDesc(String meetingId);
|
||||
|
||||
/**
|
||||
* 상태로 회의록 목록 조회
|
||||
*/
|
||||
List<MinutesEntity> findByStatus(String status);
|
||||
|
||||
/**
|
||||
* 작성자 ID로 회의록 목록 조회
|
||||
*/
|
||||
List<MinutesEntity> findByCreatedBy(String createdBy);
|
||||
|
||||
/**
|
||||
* 확정자 ID로 회의록 목록 조회
|
||||
*/
|
||||
List<MinutesEntity> findByFinalizedBy(String finalizedBy);
|
||||
|
||||
/**
|
||||
* 회의 ID와 버전으로 회의록 조회
|
||||
*/
|
||||
Optional<MinutesEntity> findByMeetingIdAndVersion(String meetingId, Integer version);
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway.repository;
|
||||
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesSectionEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 JPA Repository
|
||||
*/
|
||||
@Repository
|
||||
public interface MinutesSectionJpaRepository extends JpaRepository<MinutesSectionEntity, String> {
|
||||
|
||||
/**
|
||||
* 회의록 ID로 섹션 목록 조회 (순서대로)
|
||||
*/
|
||||
List<MinutesSectionEntity> findByMinutesIdOrderByOrderAsc(String minutesId);
|
||||
|
||||
/**
|
||||
* 회의록 ID와 타입으로 섹션 목록 조회
|
||||
*/
|
||||
List<MinutesSectionEntity> findByMinutesIdAndType(String minutesId, String type);
|
||||
|
||||
/**
|
||||
* 회의록 ID와 검증 여부로 섹션 목록 조회
|
||||
*/
|
||||
List<MinutesSectionEntity> findByMinutesIdAndVerified(String minutesId, Boolean verified);
|
||||
|
||||
/**
|
||||
* 회의록 ID와 잠금 여부로 섹션 목록 조회
|
||||
*/
|
||||
List<MinutesSectionEntity> findByMinutesIdAndLocked(String minutesId, Boolean locked);
|
||||
|
||||
/**
|
||||
* 잠금한 사용자 ID로 섹션 목록 조회
|
||||
*/
|
||||
List<MinutesSectionEntity> findByLockedBy(String lockedBy);
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway.repository;
|
||||
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.TemplateEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 템플릿 JPA Repository
|
||||
*/
|
||||
@Repository
|
||||
public interface TemplateJpaRepository extends JpaRepository<TemplateEntity, String> {
|
||||
|
||||
/**
|
||||
* 카테고리로 템플릿 목록 조회
|
||||
*/
|
||||
List<TemplateEntity> findByCategory(String category);
|
||||
|
||||
/**
|
||||
* 공개 여부로 템플릿 목록 조회
|
||||
*/
|
||||
List<TemplateEntity> findByIsPublic(Boolean isPublic);
|
||||
|
||||
/**
|
||||
* 작성자 ID로 템플릿 목록 조회
|
||||
*/
|
||||
List<TemplateEntity> findByCreatedBy(String createdBy);
|
||||
|
||||
/**
|
||||
* 카테고리와 공개 여부로 템플릿 목록 조회
|
||||
*/
|
||||
List<TemplateEntity> findByCategoryAndIsPublic(String category, Boolean isPublic);
|
||||
|
||||
/**
|
||||
* 이름으로 템플릿 검색 (부분 일치)
|
||||
*/
|
||||
List<TemplateEntity> findByNameContaining(String name);
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway.repository;
|
||||
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.TodoEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Todo JPA Repository
|
||||
*/
|
||||
@Repository
|
||||
public interface TodoJpaRepository extends JpaRepository<TodoEntity, String> {
|
||||
|
||||
/**
|
||||
* 회의 ID로 Todo 목록 조회
|
||||
*/
|
||||
List<TodoEntity> findByMeetingId(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의록 ID로 Todo 목록 조회
|
||||
*/
|
||||
List<TodoEntity> findByMinutesId(String minutesId);
|
||||
|
||||
/**
|
||||
* 담당자 ID로 Todo 목록 조회
|
||||
*/
|
||||
List<TodoEntity> findByAssigneeId(String assigneeId);
|
||||
|
||||
/**
|
||||
* 상태로 Todo 목록 조회
|
||||
*/
|
||||
List<TodoEntity> findByStatus(String status);
|
||||
|
||||
/**
|
||||
* 담당자 ID와 상태로 Todo 목록 조회
|
||||
*/
|
||||
List<TodoEntity> findByAssigneeIdAndStatus(String assigneeId, String status);
|
||||
|
||||
/**
|
||||
* 마감일로 Todo 목록 조회
|
||||
*/
|
||||
List<TodoEntity> findByDueDate(LocalDate dueDate);
|
||||
|
||||
/**
|
||||
* 마감일 이전 Todo 목록 조회
|
||||
*/
|
||||
List<TodoEntity> findByDueDateBefore(LocalDate date);
|
||||
|
||||
/**
|
||||
* 우선순위로 Todo 목록 조회
|
||||
*/
|
||||
List<TodoEntity> findByPriority(String priority);
|
||||
|
||||
/**
|
||||
* 담당자 ID와 마감일 범위로 Todo 목록 조회
|
||||
*/
|
||||
List<TodoEntity> findByAssigneeIdAndDueDateBetween(String assigneeId, LocalDate startDate, LocalDate endDate);
|
||||
}
|
||||
98
meeting/src/main/resources/application.yml
Normal file
98
meeting/src/main/resources/application.yml
Normal file
@ -0,0 +1,98 @@
|
||||
spring:
|
||||
application:
|
||||
name: meeting
|
||||
|
||||
# Database Configuration
|
||||
datasource:
|
||||
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:4.230.48.72}:${DB_PORT:5432}/${DB_NAME:meetingdb}
|
||||
username: ${DB_USERNAME:hgzerouser}
|
||||
password: ${DB_PASSWORD:}
|
||||
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 Configuration
|
||||
jpa:
|
||||
show-sql: ${SHOW_SQL:true}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
use_sql_comments: true
|
||||
hibernate:
|
||||
ddl-auto: ${DDL_AUTO:update}
|
||||
|
||||
# Redis Configuration
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:20.249.177.114}
|
||||
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:1}
|
||||
|
||||
# Server Configuration
|
||||
server:
|
||||
port: ${SERVER_PORT:8081}
|
||||
|
||||
# JWT Configuration
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600}
|
||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800}
|
||||
|
||||
# CORS Configuration
|
||||
cors:
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:*}
|
||||
|
||||
# Actuator Configuration
|
||||
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 Configuration
|
||||
logging:
|
||||
level:
|
||||
com.unicorn.hgzero.meeting: ${LOG_LEVEL_APP:DEBUG}
|
||||
org.springframework.web: ${LOG_LEVEL_WEB:INFO}
|
||||
org.springframework.security: ${LOG_LEVEL_SECURITY:DEBUG}
|
||||
org.springframework.websocket: ${LOG_LEVEL_WEBSOCKET:DEBUG}
|
||||
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/meeting.log}
|
||||
14
notification/build.gradle
Normal file
14
notification/build.gradle
Normal file
@ -0,0 +1,14 @@
|
||||
bootJar {
|
||||
archiveFileName = 'notification.jar'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Email
|
||||
implementation 'org.springframework.boot:spring-boot-starter-mail'
|
||||
|
||||
// Thymeleaf for email templates
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
|
||||
// Azure Event Hubs
|
||||
implementation "com.azure:azure-messaging-eventhubs:${azureEventHubsVersion}"
|
||||
}
|
||||
139
notification/src/main/resources/application.yml
Normal file
139
notification/src/main/resources/application.yml
Normal file
@ -0,0 +1,139 @@
|
||||
spring:
|
||||
application:
|
||||
name: notification
|
||||
|
||||
# Database Configuration
|
||||
datasource:
|
||||
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:4.230.159.143}:${DB_PORT:5432}/${DB_NAME:notificationdb}
|
||||
username: ${DB_USERNAME:hgzerouser}
|
||||
password: ${DB_PASSWORD:}
|
||||
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 Configuration
|
||||
jpa:
|
||||
show-sql: ${SHOW_SQL:true}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
use_sql_comments: true
|
||||
hibernate:
|
||||
ddl-auto: ${DDL_AUTO:update}
|
||||
|
||||
# Redis Configuration
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:20.249.177.114}
|
||||
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:4}
|
||||
|
||||
# Mail Configuration
|
||||
mail:
|
||||
host: ${MAIL_HOST:smtp.gmail.com}
|
||||
port: ${MAIL_PORT:587}
|
||||
username: ${MAIL_USERNAME:}
|
||||
password: ${MAIL_PASSWORD:}
|
||||
properties:
|
||||
mail:
|
||||
smtp:
|
||||
auth: true
|
||||
starttls:
|
||||
enable: true
|
||||
required: true
|
||||
connectiontimeout: 5000
|
||||
timeout: 5000
|
||||
writetimeout: 5000
|
||||
|
||||
# Thymeleaf Configuration
|
||||
thymeleaf:
|
||||
prefix: classpath:/templates/
|
||||
suffix: .html
|
||||
mode: HTML
|
||||
encoding: UTF-8
|
||||
cache: false
|
||||
|
||||
# Server Configuration
|
||||
server:
|
||||
port: ${SERVER_PORT:8084}
|
||||
|
||||
# JWT Configuration
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600}
|
||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800}
|
||||
|
||||
# CORS Configuration
|
||||
cors:
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:*}
|
||||
|
||||
# Azure Event Hubs Configuration
|
||||
azure:
|
||||
eventhub:
|
||||
connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING:}
|
||||
name: ${AZURE_EVENTHUB_NAME:notification-events}
|
||||
consumer-group: ${AZURE_EVENTHUB_CONSUMER_GROUP:$Default}
|
||||
|
||||
# Notification Configuration
|
||||
notification:
|
||||
from-email: ${NOTIFICATION_FROM_EMAIL:noreply@hgzero.com}
|
||||
from-name: ${NOTIFICATION_FROM_NAME:HGZero}
|
||||
retry:
|
||||
max-attempts: ${NOTIFICATION_RETRY_MAX_ATTEMPTS:3}
|
||||
initial-interval: ${NOTIFICATION_RETRY_INITIAL_INTERVAL:1000}
|
||||
multiplier: ${NOTIFICATION_RETRY_MULTIPLIER:2.0}
|
||||
|
||||
# Actuator Configuration
|
||||
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 Configuration
|
||||
logging:
|
||||
level:
|
||||
com.unicorn.hgzero.notification: ${LOG_LEVEL_APP:DEBUG}
|
||||
org.springframework.web: ${LOG_LEVEL_WEB:INFO}
|
||||
org.springframework.security: ${LOG_LEVEL_SECURITY:DEBUG}
|
||||
org.springframework.mail: ${LOG_LEVEL_MAIL:DEBUG}
|
||||
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/notification.log}
|
||||
8
settings.gradle
Normal file
8
settings.gradle
Normal file
@ -0,0 +1,8 @@
|
||||
rootProject.name = 'hgzero'
|
||||
|
||||
include 'common'
|
||||
include 'user'
|
||||
include 'meeting'
|
||||
include 'stt'
|
||||
include 'ai'
|
||||
include 'notification'
|
||||
17
stt/build.gradle
Normal file
17
stt/build.gradle
Normal file
@ -0,0 +1,17 @@
|
||||
bootJar {
|
||||
archiveFileName = 'stt.jar'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Azure Speech SDK
|
||||
implementation "com.microsoft.cognitiveservices.speech:client-sdk:${azureSpeechVersion}"
|
||||
|
||||
// Azure Blob Storage
|
||||
implementation "com.azure:azure-storage-blob:${azureBlobVersion}"
|
||||
|
||||
// Azure Event Hubs
|
||||
implementation "com.azure:azure-messaging-eventhubs:${azureEventHubsVersion}"
|
||||
|
||||
// WebSocket
|
||||
implementation 'org.springframework.boot:spring-boot-starter-websocket'
|
||||
}
|
||||
112
stt/src/main/resources/application.yml
Normal file
112
stt/src/main/resources/application.yml
Normal file
@ -0,0 +1,112 @@
|
||||
spring:
|
||||
application:
|
||||
name: stt
|
||||
|
||||
# Database Configuration
|
||||
datasource:
|
||||
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:4.230.65.89}:${DB_PORT:5432}/${DB_NAME:sttdb}
|
||||
username: ${DB_USERNAME:hgzerouser}
|
||||
password: ${DB_PASSWORD:}
|
||||
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 Configuration
|
||||
jpa:
|
||||
show-sql: ${SHOW_SQL:true}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
use_sql_comments: true
|
||||
hibernate:
|
||||
ddl-auto: ${DDL_AUTO:update}
|
||||
|
||||
# Redis Configuration
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:20.249.177.114}
|
||||
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:2}
|
||||
|
||||
# Server Configuration
|
||||
server:
|
||||
port: ${SERVER_PORT:8082}
|
||||
|
||||
# JWT Configuration
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600}
|
||||
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800}
|
||||
|
||||
# CORS Configuration
|
||||
cors:
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:*}
|
||||
|
||||
# Azure Speech Service Configuration
|
||||
azure:
|
||||
speech:
|
||||
subscription-key: ${AZURE_SPEECH_SUBSCRIPTION_KEY:}
|
||||
region: ${AZURE_SPEECH_REGION:eastus}
|
||||
language: ${AZURE_SPEECH_LANGUAGE:ko-KR}
|
||||
blob:
|
||||
connection-string: ${AZURE_BLOB_CONNECTION_STRING:}
|
||||
container-name: ${AZURE_BLOB_CONTAINER_NAME:recordings}
|
||||
eventhub:
|
||||
connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING:}
|
||||
name: ${AZURE_EVENTHUB_NAME:transcription-events}
|
||||
consumer-group: ${AZURE_EVENTHUB_CONSUMER_GROUP:$Default}
|
||||
|
||||
# Actuator Configuration
|
||||
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 Configuration
|
||||
logging:
|
||||
level:
|
||||
com.unicorn.hgzero.stt: ${LOG_LEVEL_APP:DEBUG}
|
||||
org.springframework.web: ${LOG_LEVEL_WEB:INFO}
|
||||
org.springframework.security: ${LOG_LEVEL_SECURITY:DEBUG}
|
||||
org.springframework.websocket: ${LOG_LEVEL_WEBSOCKET:DEBUG}
|
||||
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/stt.log}
|
||||
9
user/build.gradle
Normal file
9
user/build.gradle
Normal file
@ -0,0 +1,9 @@
|
||||
bootJar {
|
||||
archiveFileName = 'user.jar'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// LDAP
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-ldap'
|
||||
implementation 'org.springframework.ldap:spring-ldap-core'
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.unicorn.hgzero.user;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
|
||||
/**
|
||||
* User Service Application
|
||||
* 사용자 인증 서비스 메인 클래스
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@ComponentScan(basePackages = {"com.unicorn.hgzero.user", "com.unicorn.hgzero.common"})
|
||||
public class UserApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(UserApplication.java, args);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package com.unicorn.hgzero.user.config;
|
||||
|
||||
import com.unicorn.hgzero.user.config.jwt.JwtAuthenticationFilter;
|
||||
import com.unicorn.hgzero.user.config.jwt.JwtTokenProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Spring Security 설정
|
||||
* JWT 기반 인증 및 API 보안 설정
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Value("${cors.allowed-origins:http://localhost:*}")
|
||||
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
|
||||
// Public endpoints
|
||||
.requestMatchers("/api/v1/auth/login").permitAll()
|
||||
// Actuator endpoints
|
||||
.requestMatchers("/actuator/**").permitAll()
|
||||
// Swagger UI endpoints
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package com.unicorn.hgzero.user.config;
|
||||
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Swagger/OpenAPI 설정
|
||||
* User Service API 문서화를 위한 설정
|
||||
*/
|
||||
@Configuration
|
||||
public class SwaggerConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI openAPI() {
|
||||
return new OpenAPI()
|
||||
.info(apiInfo())
|
||||
.addServersItem(new Server()
|
||||
.url("http://localhost:8080")
|
||||
.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("8080")
|
||||
.description("Server port"))))
|
||||
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
|
||||
.components(new Components()
|
||||
.addSecuritySchemes("Bearer Authentication", createAPIKeyScheme()));
|
||||
}
|
||||
|
||||
private Info apiInfo() {
|
||||
return new Info()
|
||||
.title("User Service API")
|
||||
.description("사용자 인증 및 JWT 토큰 관리 API")
|
||||
.version("1.0.0")
|
||||
.contact(new Contact()
|
||||
.name("HGZero Development Team")
|
||||
.email("dev@hgzero.com"));
|
||||
}
|
||||
|
||||
private SecurityScheme createAPIKeyScheme() {
|
||||
return new SecurityScheme()
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.bearerFormat("JWT")
|
||||
.scheme("bearer");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
package com.unicorn.hgzero.user.config.jwt;
|
||||
|
||||
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.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
package com.unicorn.hgzero.user.config.jwt;
|
||||
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package com.unicorn.hgzero.user.config.jwt;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 인증된 사용자 정보
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,127 @@
|
||||
package com.unicorn.hgzero.user.controller;
|
||||
|
||||
import com.unicorn.hgzero.common.dto.ApiResponse;
|
||||
import com.unicorn.hgzero.user.config.jwt.UserPrincipal;
|
||||
import com.unicorn.hgzero.user.dto.*;
|
||||
import com.unicorn.hgzero.user.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 사용자 인증 Controller
|
||||
* 로그인, 토큰 갱신, 로그아웃, 토큰 검증 API 제공
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "User Authentication", description = "사용자 인증 API")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
/**
|
||||
* 사용자 로그인
|
||||
* LDAP 인증 수행 후 JWT 토큰 발급
|
||||
*
|
||||
* @param request 로그인 요청 정보 (userId, password)
|
||||
* @return 로그인 응답 (Access Token, Refresh Token, 사용자 정보)
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
@Operation(
|
||||
summary = "사용자 로그인",
|
||||
description = "LDAP 인증 수행 후 JWT Access Token과 Refresh Token을 발급합니다."
|
||||
)
|
||||
public ResponseEntity<ApiResponse<LoginResponse>> login(
|
||||
@Valid @RequestBody LoginRequest request) {
|
||||
|
||||
log.info("로그인 요청: userId={}", request.getUserId());
|
||||
|
||||
LoginResponse response = userService.login(request);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success("로그인 성공", response));
|
||||
}
|
||||
|
||||
/**
|
||||
* Access Token 갱신
|
||||
* Refresh Token을 사용하여 새로운 Access Token 발급
|
||||
*
|
||||
* @param request Refresh Token 요청 정보
|
||||
* @return 새로운 Access Token
|
||||
*/
|
||||
@PostMapping("/refresh")
|
||||
@Operation(
|
||||
summary = "Access Token 갱신",
|
||||
description = "Refresh Token을 사용하여 새로운 Access Token을 발급합니다."
|
||||
)
|
||||
public ResponseEntity<ApiResponse<RefreshTokenResponse>> refresh(
|
||||
@Valid @RequestBody RefreshTokenRequest request) {
|
||||
|
||||
log.info("토큰 갱신 요청");
|
||||
|
||||
RefreshTokenResponse response = userService.refresh(request);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success("토큰 갱신 성공", response));
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그아웃
|
||||
* Refresh Token 삭제
|
||||
*
|
||||
* @param request 로그아웃 요청 정보 (선택적 Refresh Token)
|
||||
* @param userPrincipal 인증된 사용자 정보
|
||||
* @return 로그아웃 성공 메시지
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
@Operation(
|
||||
summary = "로그아웃",
|
||||
description = "Refresh Token을 삭제하고 로그아웃합니다."
|
||||
)
|
||||
public ResponseEntity<ApiResponse<Void>> logout(
|
||||
@RequestBody(required = false) LogoutRequest request,
|
||||
@AuthenticationPrincipal UserPrincipal userPrincipal) {
|
||||
|
||||
log.info("로그아웃 요청: userId={}", userPrincipal.getUserId());
|
||||
|
||||
userService.logout(request, userPrincipal.getUserId());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success("로그아웃 성공", null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Access Token 검증
|
||||
* 토큰 유효성 검증 및 사용자 정보 반환
|
||||
*
|
||||
* @param authorization Authorization 헤더 (Bearer {token})
|
||||
* @return 토큰 검증 결과 및 사용자 정보
|
||||
*/
|
||||
@GetMapping("/validate")
|
||||
@Operation(
|
||||
summary = "Access Token 검증",
|
||||
description = "Access Token의 유효성을 검증하고 사용자 정보를 반환합니다."
|
||||
)
|
||||
public ResponseEntity<ApiResponse<TokenValidateResponse>> validate(
|
||||
@Parameter(description = "Bearer {token}", required = true)
|
||||
@RequestHeader("Authorization") String authorization) {
|
||||
|
||||
log.info("토큰 검증 요청");
|
||||
|
||||
// Bearer 제거
|
||||
String token = authorization.replace("Bearer ", "");
|
||||
|
||||
TokenValidateResponse response = userService.validateToken(token);
|
||||
|
||||
if (response.getValid()) {
|
||||
return ResponseEntity.ok(ApiResponse.success("토큰 검증 성공", response));
|
||||
} else {
|
||||
return ResponseEntity.ok(ApiResponse.success("토큰 검증 실패", response));
|
||||
}
|
||||
}
|
||||
}
|
||||
104
user/src/main/java/com/unicorn/hgzero/user/domain/User.java
Normal file
104
user/src/main/java/com/unicorn/hgzero/user/domain/User.java
Normal file
@ -0,0 +1,104 @@
|
||||
package com.unicorn.hgzero.user.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 사용자 도메인 모델
|
||||
* 사용자 인증 및 계정 정보를 표현
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class User {
|
||||
|
||||
/**
|
||||
* 사용자 ID
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 사용자 이름
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 이메일
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 권한 (USER, ADMIN)
|
||||
*/
|
||||
private String authority;
|
||||
|
||||
/**
|
||||
* 계정 잠금 여부
|
||||
*/
|
||||
private Boolean locked;
|
||||
|
||||
/**
|
||||
* 로그인 실패 횟수
|
||||
*/
|
||||
private Integer failedLoginAttempts;
|
||||
|
||||
/**
|
||||
* 마지막 로그인 일시
|
||||
*/
|
||||
private LocalDateTime lastLoginAt;
|
||||
|
||||
/**
|
||||
* 계정 잠금 일시
|
||||
*/
|
||||
private LocalDateTime lockedAt;
|
||||
|
||||
/**
|
||||
* 계정 잠금 해제
|
||||
*/
|
||||
public void unlock() {
|
||||
this.locked = false;
|
||||
this.failedLoginAttempts = 0;
|
||||
this.lockedAt = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인 실패 기록
|
||||
* 5회 실패 시 계정 잠금
|
||||
*/
|
||||
public void recordLoginFailure() {
|
||||
this.failedLoginAttempts++;
|
||||
if (this.failedLoginAttempts >= 5) {
|
||||
this.locked = true;
|
||||
this.lockedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인 성공 기록
|
||||
*/
|
||||
public void recordLoginSuccess() {
|
||||
this.failedLoginAttempts = 0;
|
||||
this.lastLoginAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정 잠금 여부 확인
|
||||
* 30분 경과 시 자동 해제
|
||||
*/
|
||||
public boolean isLocked() {
|
||||
if (this.locked && this.lockedAt != null) {
|
||||
// 30분 경과 시 자동 해제
|
||||
if (LocalDateTime.now().isAfter(this.lockedAt.plusMinutes(30))) {
|
||||
this.unlock();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.unicorn.hgzero.user.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 로그인 요청 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginRequest {
|
||||
|
||||
/**
|
||||
* 사용자 ID
|
||||
*/
|
||||
@NotBlank(message = "사용자 ID는 필수입니다.")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 비밀번호
|
||||
*/
|
||||
@NotBlank(message = "비밀번호는 필수입니다.")
|
||||
private String password;
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
package com.unicorn.hgzero.user.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 로그인 응답 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginResponse {
|
||||
|
||||
/**
|
||||
* Access Token
|
||||
*/
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* Refresh Token
|
||||
*/
|
||||
private String refreshToken;
|
||||
|
||||
/**
|
||||
* 토큰 타입 (Bearer)
|
||||
*/
|
||||
@Builder.Default
|
||||
private String tokenType = "Bearer";
|
||||
|
||||
/**
|
||||
* Access Token 유효 기간 (초)
|
||||
*/
|
||||
private Long accessTokenValidity;
|
||||
|
||||
/**
|
||||
* Refresh Token 유효 기간 (초)
|
||||
*/
|
||||
private Long refreshTokenValidity;
|
||||
|
||||
/**
|
||||
* 사용자 정보
|
||||
*/
|
||||
private UserInfo user;
|
||||
|
||||
/**
|
||||
* 사용자 정보 내부 클래스
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class UserInfo {
|
||||
/**
|
||||
* 사용자 ID
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 사용자 이름
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 이메일
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 권한
|
||||
*/
|
||||
private String authority;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user