Skip to content
Merged
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
root = true

[*.kt]
ktlint_standard_package-name = disabled
ktlint_standard_filename = disabled
67 changes: 18 additions & 49 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,70 +1,39 @@
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import kotlin.collections.plus

plugins {
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
id("org.springframework.boot") version "3.4.4"
id("io.spring.dependency-management") version "1.1.7"
id("org.jlleitschuh.gradle.ktlint").version("12.1.1")
id("io.gitlab.arturbosch.detekt") version "1.23.6"
id("casper.documentation-convention")
id(Plugin.KOTLIN_JVM) version PluginVersion.KOTLIN_VERSION apply false
id(Plugin.DETEKT) version PluginVersion.DETEKT_VERSION
id(Plugin.KTLINT) version PluginVersion.KTLINT_VERSION apply false
}

// 서브프로젝트 설정
subprojects {
// 서브프로젝트에 공통 설정 적용
repositories {
mavenCentral()
group = "hs.kr.entrydsm"
version = "0.0.1"

apply(plugin = Plugin.KTLINT)

configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
filter {
exclude("**/build/generated/**")
exclude("**/*Grpc*.kt")
exclude("**/*Proto*.kt")
}
}
}

tasks.register("checkAll") {
group = "verification"
description = "모든 모듈(includeBuild 포함)에 대해 check 태스크를 실행합니다"

// 루트 프로젝트의 check 태스크에 의존
dependsOn(tasks.named("check"))

// 모든 서브프로젝트의 check 태스크에 의존
subprojects.forEach { subproject ->
dependsOn(subproject.tasks.matching { it.name.startsWith("check") })
}

// build-logic, convention 등 includeBuild 모듈의 check 태스크에 의존
dependsOn(gradle.includedBuilds.map { it.task(":check") })
}

group = "hs.kr.entrydsm"
version = "0.0.1-SNAPSHOT"

dependencies {
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.jetbrains.kotlin:kotlin-reflect")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

kotlin {
jvmToolchain(17)
}

tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
}
}

tasks.withType<Test> {
useJUnitPlatform()
}

detekt {
config.setFrom(files("detekt.yml"))
buildUponDefaultConfig = false // yml에서 설정한 룰만 허용
parallel = true // 병렬 실행으로 성능 최적화
buildUponDefaultConfig = false
parallel = true
}

tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
Expand All @@ -73,5 +42,5 @@ tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
txt.required.set(false)
}

jvmTarget = ("17") // Detekt가 사용하는 JVM 타겟을 Java 17로 지정
}
jvmTarget = "17"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package hs.kr.entrydsm.user.global.base

import jakarta.persistence.EntityListeners
import jakarta.persistence.MappedSuperclass
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.LocalDateTime

/**
* 생성 및 수정 시간을 자동으로 관리하는 JPA 엔티티의 기본 클래스입니다.
* Spring Data JPA의 Auditing 기능을 사용하여 엔티티의 생성/수정 시간을 자동으로 설정합니다.
*
* @property createdAt 엔티티 생성 시간
* @property modifiedAt 엔티티 최종 수정 시간
*/
@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class BaseTimeEntity(
@CreatedDate
private val createdAt: LocalDateTime? = LocalDateTime.now(),
@LastModifiedDate
private val modifiedAt: LocalDateTime? = LocalDateTime.now(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package hs.kr.entrydsm.user.global.base

import jakarta.persistence.Column
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.MappedSuperclass
import java.util.UUID

/**
* UUID 기본키를 사용하는 JPA 엔티티의 기본 클래스입니다.
* 모든 엔티티가 공통으로 사용하는 UUID 기본키 필드를 정의합니다.
*
* @property id UUID 타입의 기본키
*/
@MappedSuperclass
abstract class BaseUUIDEntity(
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(
columnDefinition = "BINARY(16)",
nullable = false,
)
val id: UUID?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package hs.kr.entrydsm.user.global.config

import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.context.annotation.Configuration

/**
* Configuration Properties 스캔 설정 클래스입니다.
* 애플리케이션 전반의 설정 프로퍼티들을 자동으로 스캔하여 등록합니다.
*/
@Configuration
@ConfigurationPropertiesScan("hs.kr.entrydsm")
class ConfigurationProperties
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package hs.kr.entrydsm.user.global.config

import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.annotation.EnableScheduling

/**
* 스케줄링 기능을 활성화하는 설정 클래스입니다.
* Spring의 @Scheduled 어노테이션을 사용한 작업을 가능하게 합니다.
*/
@Configuration
@EnableScheduling
class EnableScheduling
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package hs.kr.entrydsm.user.global.converter

import hs.kr.entrydsm.user.global.utils.encryption.EncryptionUtil
import jakarta.persistence.AttributeConverter
import jakarta.persistence.Converter
import org.springframework.stereotype.Component

/**
* JPA 엔티티의 문자열 필드를 암호화하여 저장하는 컨버터 클래스입니다.
* 민감한 개인정보를 데이터베이스에 안전하게 저장하기 위해 사용됩니다.
*
* @property encryptionUtil 암호화/복호화를 담당하는 유틸리티
*/
@Component
@Converter
class EncryptedStringConverter(
private val encryptionUtil: EncryptionUtil,
) : AttributeConverter<String, String> {
/**
* 엔티티 속성을 데이터베이스 컬럼으로 변환할 때 암호화를 수행합니다.
*
* @param attributes 암호화할 엔티티 속성값
* @return 암호화된 문자열 (null이면 null 반환)
*/
override fun convertToDatabaseColumn(attributes: String?): String? {
return attributes?.let { encryptionUtil.encrypt(it) }
}

/**
* 데이터베이스 컬럼을 엔티티 속성으로 변환할 때 복호화를 수행합니다.
*
* @param dbData 복호화할 데이터베이스 값
* @return 복호화된 문자열 (null이면 null 반환)
*/
override fun convertToEntityAttribute(dbData: String?): String? {
return dbData?.let { encryptionUtil.decrypt(it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package hs.kr.entrydsm.user.global.document.admin

import hs.kr.entrydsm.user.domain.admin.adapter.`in`.web.dto.request.AdminLoginRequest
import hs.kr.entrydsm.user.domain.admin.adapter.`in`.web.dto.response.InternalAdminResponse
import hs.kr.entrydsm.user.global.utils.token.dto.TokenResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import java.util.UUID

/**
* 관리자 API 문서화를 위한 인터페이스입니다.
*/
@Tag(name = "Admin", description = "관리자 API")
interface AdminApiDocument {
/**
* 관리자 로그인을 처리합니다.
*/
@Operation(
summary = "관리자 로그인",
description = "관리자 ID와 비밀번호로 로그인하여 JWT 토큰을 발급받습니다. 성공 시 UserInfo도 Redis에 저장됩니다.",
)
@ApiResponses(
ApiResponse(
responseCode = "200",
description = "로그인 성공",
content = arrayOf(Content(schema = Schema(implementation = TokenResponse::class))),
),
ApiResponse(
responseCode = "401",
description = "비밀번호가 일치하지 않음 - Invalid User Password",
content = arrayOf(Content()),
),
ApiResponse(
responseCode = "404",
description = "존재하지 않는 관리자 ID - Admin Not Found",
content = arrayOf(Content()),
),
)
fun login(
@RequestBody @Valid adminLoginRequest: AdminLoginRequest,
): TokenResponse

/**
* 관리자 토큰을 갱신합니다.
*/
@Operation(
summary = "관리자 토큰 갱신",
description = "Refresh Token을 사용하여 새로운 Access Token을 발급받습니다.",
)
@ApiResponses(
ApiResponse(
responseCode = "200",
description = "토큰 갱신 성공",
content = arrayOf(Content(schema = Schema(implementation = TokenResponse::class))),
),
ApiResponse(
responseCode = "401",
description = "유효하지 않거나 만료된 토큰 - Invalid Token | Expired Token",
content = arrayOf(Content()),
),
)
fun tokenRefresh(
@Parameter(description = "갱신할 Refresh Token", required = true)
@RequestHeader("X-Refresh-Token") refreshToken: String,
): TokenResponse

@Operation(
summary = "모든 테이블 삭제 (Kafka 메시지 발송)",
description = "Kafka를 통해 모든 테이블 삭제 메시지를 발송합니다. 실제 삭제는 Consumer에서 처리됩니다.",
)
@ApiResponses(
ApiResponse(
responseCode = "204",
description = "삭제 메시지 발송 완료",
content = arrayOf(Content()),
),
ApiResponse(
responseCode = "401",
description = "관리자 권한 없음 - Admin UnAuthorized",
content = arrayOf(Content()),
),
)
/**
* 모든 테이블 삭제 메시지를 발송합니다.
*/
@SecurityRequirement(name = "bearerAuth")
fun deleteAllTable()

/**
* UUID로 관리자 정보를 조회합니다.
*/
@Operation(
summary = "관리자 정보 조회",
description = "UUID로 특정 관리자의 정보를 조회합니다.",
)
@ApiResponses(
ApiResponse(
responseCode = "200",
description = "조회 성공",
content = arrayOf(Content(schema = Schema(implementation = InternalAdminResponse::class))),
),
ApiResponse(
responseCode = "404",
description = "존재하지 않는 관리자 - Admin Not Found",
content = arrayOf(Content()),
),
)
fun findAdminById(
@Parameter(description = "조회할 관리자의 UUID", required = true)
@PathVariable adminId: UUID,
): InternalAdminResponse
}
Loading