Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ out/
/dist/
/nbdist/
/.nb-gradle/
.DS_Store

### VS Code ###
.vscode/
Expand Down
301 changes: 301 additions & 0 deletions prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
## 아키텍처 및 설계 원칙

### 기본 원칙
- 도메인 주도 개발(DDD)로 개발해야함
- 헥사고날 아키텍처를 준수해야함
- N+1 문제와 같은 오버헤드를 방지해야함
- 모든 코드는 객체지향 원칙을 준수해야함
- 모든 코드는 객체지향 생활체조 원칙도 준수해야함
- 함수의 최대 depth는 2임
- 모든 코드는 최대한 nullable하지 않게 작성해야함
- Kotlin의 null safety를 활용하고, `@Nonnull` 같은 어노테이션을 사용하지 않음

### CQRS 패턴
- Command와 Query를 명확하게 분리해야함
- Controller: `{Domain}CommandController`, `{Domain}QueryController`로 분리
- Service: `{Domain}CommandService`, `{Domain}QueryService`로 분리
- Command는 상태 변경, Query는 조회만 담당

## 패키지 구조

### 도메인 계층 구조
```
domain/
{domain}/
application/ # 애플리케이션 계층
dto/
request/ # 요청 DTO (infrastructure에서 사용)
response/ # 응답 DTO (application에서 사용)
exception/ # 애플리케이션 레벨 예외
mapper/ # Entity <-> DTO 변환
service/ # 비즈니스 로직
domain/ # 도메인 계층
{Entity}.kt # 도메인 엔티티
vo/ # Value Object
type/ # Enum 타입
exception/ # 도메인 레벨 예외
presentation/ # 프레젠테이션 계층
{Domain}CommandController.kt
{Domain}QueryController.kt
repository/ # 저장소 계층
{Domain}Repository.kt # 인터페이스
{Domain}RepositoryImpl.kt # 구현체
{Domain}PersistenceRepository.kt # JPA Repository
```

### 계층별 책임
- **도메인 계층 (domain/)**: 엔티티, VO, 도메인 예외, 도메인 규칙
- **애플리케이션 계층 (application/)**: DTO, 매퍼, 서비스, 애플리케이션 예외
- **프레젠테이션 계층 (presentation/)**: 컨트롤러
- **인프라 계층 (infrastructure/)**: 외부 API 클라이언트, 어댑터

### Global 계층
```
global/
aspect/ # AOP
config/ # 설정 클래스
error/ # 글로벌 에러 처리
properties/ # 프로퍼티
security/ # 보안 설정
util/ # 유틸리티
```

## 네이밍 컨벤션

### 파일 네이밍
- Entity: `{Domain}.kt` (예: `Item.kt`, `User.kt`)
- VO: `{Domain}{Purpose}.kt` (예: `ItemInfo.kt`, `Stock.kt`, `UserAccountInfo.kt`)
- DTO: `{Domain}{Action}Request/Response.kt` (예: `ItemUpdateRequest.kt`, `ItemResponse.kt`)
- Service: `{Domain}CommandService.kt`, `{Domain}QueryService.kt`
- Controller: `{Domain}CommandController.kt`, `{Domain}QueryController.kt`
- Repository: `{Domain}Repository.kt`, `{Domain}RepositoryImpl.kt`, `{Domain}PersistenceRepository.kt`
- Exception: `{Purpose}Exception.kt` (예: `ItemNotFoundException.kt`, `ItemStockNegativeException.kt`)
- Mapper: `{Domain}Mapper.kt` (object로 구현)

### 변수 네이밍
- 모든 변수는 camelCase 사용
- private 필드는 `private val/var {fieldName}` 형식
- Boolean 타입은 `is{Something}` 형식 (예: `isDeleted`)

### 함수 네이밍
- 조회: `query{Something}`, `find{Something}`, `get{Something}`
- 생성: `create{Something}`, `save{Something}`
- 수정: `update{Something}`, `modify{Something}`
- 삭제: `delete{Something}`, `deactivate{Something}` (소프트 삭제)
- 검증: `validate{Something}`, `ensure{Something}`
- 변환: `to{Something}`, `map{Something}`

## Entity 작성 규칙

### 기본 구조
```kotlin
@Entity
@Table(name = "table_name")
data class Entity(
@Id
@field:Column(name = "id_column")
private val id: Long = 0L,

@Embedded
private var vo: ValueObject,

private var isDeleted: Boolean = false,
) {
// 비즈니스 로직 메서드
fun businessLogic() {
// 로직
}

// Getter 메서드 (필드명과 동일)
fun id() = id
fun fieldName() = vo.fieldName()
}
```

### Entity 규칙
- data class 사용
- 모든 필드는 private
- `@field:Column`으로 JPA 어노테이션 적용
- 비즈니스 로직은 Entity에 포함
- Getter는 필드명과 동일한 메서드로 제공 (get 접두사 없음)
- VO는 `@Embedded`로 포함

## Value Object (VO) 작성 규칙

### 기본 구조
```kotlin
@Embeddable
class ValueObject(
@field:Column(name = "field_name", nullable = false)
private val fieldName: Type,
) {
// 비즈니스 로직 및 검증
fun businessLogic(): ValueObject {
validateSomething()
return ValueObject(newValue)
}

private fun validateSomething() {
if (condition) {
throw DomainException()
}
}

// Getter
fun fieldName() = fieldName
}
```

### VO 규칙
- class 사용 (data class 아님)
- 불변성 유지 (val 사용)
- 변경이 필요한 경우 새 인스턴스 반환
- 검증 로직 포함
- `@Embeddable` 어노테이션 사용

## Repository 작성 규칙

### 3계층 구조
1. **{Domain}Repository (인터페이스)**: 도메인 레이어의 포트
2. **{Domain}RepositoryImpl**: 실제 구현체, QueryDSL 사용
3. **{Domain}PersistenceRepository**: JpaRepository 확장

### 예시
```kotlin
// 1. 인터페이스
interface ItemRepository {
fun findAll(): List<Item>
fun save(item: Item): Item
}

// 2. 구현체
@Repository
class ItemRepositoryImpl(
private val jpaQueryFactory: JPAQueryFactory,
private val itemPersistenceRepository: ItemPersistenceRepository,
) : ItemRepository {
override fun findAll(): List<Item> {
return jpaQueryFactory
.selectFrom(item)
.fetch()
}
}

// 3. JPA Repository
@Repository
interface ItemPersistenceRepository : JpaRepository<Item, Long>
```

## Service 작성 규칙

### Command Service
- `@Transactional` 어노테이션 필수
- 상태 변경 로직만 포함
- 검증 로직은 private 메서드로 분리

### Query Service
- `@Transactional` 불필요 (읽기 전용)
- 조회 로직만 포함
- 매핑 로직은 private 메서드로 분리

### 공통 규칙
- 한 메서드는 한 가지 일만
- 검증, 변환, 매핑 로직은 별도 private 메서드로 분리
- depth 2 이상 중첩 금지

## Controller 작성 규칙

```kotlin
@RestController
@RequestMapping("/items")
class ItemCommandController(
private val itemCommandService: ItemCommandService,
) {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun create(@RequestBody request: ItemCreateRequest): ItemResponse {
return itemCommandService.create(request)
}
}
```

### 규칙
- REST 원칙 준수
- `@ResponseStatus`로 상태 코드 명시
- 비즈니스 로직은 Service에 위임
- Command/Query 분리

## Exception 작성 규칙

### 도메인 예외
- 위치: `domain/{domain}/domain/exception/`
- 도메인 규칙 위반 시 발생

### 애플리케이션 예외
- 위치: `domain/{domain}/application/exception/`
- 애플리케이션 로직 실패 시 발생

### 기본 구조
```kotlin
class SomeException : BusinessBaseException(ErrorMessage.SOME_ERROR)
```

## Mapper 작성 규칙

```kotlin
object ItemMapper {
fun toEntity(response: ItemResponse): Item {
return Item(...)
}

fun toItemResponse(item: Item): ItemResponse {
return ItemResponse(...)
}
}
```

### 규칙
- object로 선언 (싱글톤)
- Entity ↔ DTO 변환 담당
- 명확한 네이밍: `toEntity`, `to{Dto}Response`

## 코딩 스타일

### Kotlin 스타일
- 들여쓰기: 4 spaces
- 파일 끝 EOF 필수
- 사용하지 않는 import 제거
- 명시적 타입 선언 (추론 가능한 경우 제외)
- **와일드카드 import(`import package.*`) 사용 금지 - 모든 클래스를 명시적으로 import**
- **제네릭 와일드카드(`List<*>`, `Map<*, *>`) 사용 금지 - 명시적 타입 파라미터 사용**

### Null Safety (중요)
- **최대한 nullable(`?`)을 사용하지 않고 코드를 작성**
- 정말 필요한 경우에만 nullable 타입 사용 (예: 데이터베이스의 nullable 컬럼)
- `@Nonnull`, `@Nullable` 같은 Java 어노테이션 사용 금지
- Kotlin의 null safety 기능을 활용 (`?.`, `?:`, `!!` 등)
- `!!` 연산자는 정말 확실한 경우에만 사용
- null 대신 기본값, 빈 컬렉션, Optional 패턴 등을 활용
- Repository 메서드는 가능한 한 Non-null 반환 (예외 발생 방식 선호)

### 주석
- 복잡한 비즈니스 로직에만 추가
- 코드로 설명 가능한 경우 주석 생략

## 기술 스택

### 프레임워크 및 라이브러리
- Spring Boot 3.5.5
- Kotlin 1.9.25
- JDK 21
- Spring Security
- Spring Data JPA
- QueryDSL 5.1.0
- MySQL
- Redis
- Feign Client (외부 API 통신)
- JWT (인증)

### 빌드 도구
- Gradle (Kotlin DSL)
- kapt (어노테이션 프로세싱)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package bssm.devcoop.occount.domain.transaction.application.dto.request

import java.time.LocalDateTime

data class ChargeRequest(
val userCode: String,
val transactionDate: LocalDateTime,
val beforePoint: Int,
val afterPoint: Int,
val managedEmail: String,
val chargeType: String,
val chargedPoint: Int,
val reason: String?,
val paymentId: String?,
val refundState: String?,
val refundDate: LocalDateTime?,
val refundRequesterId: String?
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package bssm.devcoop.occount.domain.transaction.application.dto.request

import java.time.LocalDateTime

data class PayRequest(
val userCode: String,
val transactionDate: LocalDateTime,
val beforePoint: Int,
val afterPoint: Int,
val managedEmail: String,
val payType: String,
val payedPoint: Int,
val itemScans: List<ItemScanRequest>
)

data class ItemScanRequest(
val itemId: Int,
val scannedAt: LocalDateTime
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package bssm.devcoop.occount.domain.transaction.application.dto.response

import java.time.LocalDateTime

sealed class TransactionResponse {
data class ChargeResponse(
val transactionId: Long?,
val userCode: String,
val transactionDate: LocalDateTime,
val beforePoint: Int,
val afterPoint: Int,
val managedEmail: String,
val chargeType: String,
val chargedPoint: Int,
val reason: String?,
val paymentId: String?,
val refundState: String?,
val refundDate: LocalDateTime?,
val refundRequesterId: String?
) : TransactionResponse()

data class PayResponse(
val transactionId: Long?,
val userCode: String,
val transactionDate: LocalDateTime,
val beforePoint: Int,
val afterPoint: Int,
val managedEmail: String,
val payType: String,
val payedPoint: Int,
val itemScans: List<ItemScanResponse>
) : TransactionResponse()
}

data class ItemScanResponse(
val scanId: Int?,
val itemId: Int,
val scannedAt: LocalDateTime
)

data class TransactionListResponse(
val transactions: List<TransactionResponse>
)

Loading