Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c0dcfd8
remove: deprecated codeguide
hanyoung-kurly Feb 1, 2026
158e427
fix : 예제 테스트 코드 오류 해결을 위한 testcontainers 버전 업
madirony Feb 3, 2026
071c2e7
feat : ADD CLAUDE.md
madirony Feb 3, 2026
43df9a1
test : Password 테스트 코드 추가
madirony Feb 4, 2026
0e7fe66
feat : Password VO 구현
madirony Feb 4, 2026
67339fb
chore : 암호화 의존성 추가
madirony Feb 4, 2026
0abd973
feat : BirthDate VO 구현
madirony Feb 4, 2026
f96b07f
feat : MemberId VO 구현
madirony Feb 4, 2026
0bb46d6
feat : Name VO 구현
madirony Feb 4, 2026
7c71610
feat : Email VO 구현
madirony Feb 4, 2026
bb6902f
refactor : Password VO가 BirthDate VO를 사용하도록 변경
madirony Feb 4, 2026
ec59da8
feat : PasswordEncoder 인터페이스 및 BCrypt 구현체 추가
madirony Feb 4, 2026
46a63e9
feat : Member 엔티티 구현
madirony Feb 4, 2026
f992c11
refactor : MemberRepository 패턴을 프로젝트 구조에 맞게 변경
madirony Feb 4, 2026
a1e8854
feat : 회원가입 서비스 구현
madirony Feb 4, 2026
c321acc
feat : 회원가입 API 구현
madirony Feb 4, 2026
c5483f8
feat: 내 정보 조회 API 구현
madirony Feb 5, 2026
556f03a
refactor: 비밀번호 변경 로직을 Password VO로 이동
madirony Feb 5, 2026
d70e9ad
feat: 비밀번호 수정 API 구현
madirony Feb 5, 2026
008db3c
refactor: 인증 객체 조회 방식 개선 (Servlet API 의존성 제거)
madirony Feb 5, 2026
4066297
refactor: MemberFacade 도입 및 MemberService의 순수 POJO 전환
madirony Feb 5, 2026
4fe877e
test: MemberTest에 updatePassword 성공 테스트 추가
madirony Feb 5, 2026
3310a3d
fix : 예제 테스트 코드 오류 해결을 위한 testcontainers 버전 업
madirony Feb 3, 2026
2a442e0
Merge pull request #48 from madirony/pr-only-commit
madirony Feb 8, 2026
5163a58
Merge branch 'Loopers-dev-lab:main' into volume-1
madirony Feb 10, 2026
6d0393d
refactor: 도메인 안정성 강화 및 CodeRabbit 리뷰 반영
madirony Feb 10, 2026
2e0fe9a
Merge remote-tracking branch 'origin/volume-1' into volume-1
madirony Feb 10, 2026
58431ec
refactor: Password.of() → validate()로 메서드 의도 명확화
madirony Feb 10, 2026
e23dab3
refactor: BirthDate.getValue() → getFormattedValue()로 메서드명 명확화
madirony Feb 10, 2026
39ce405
refactor: BirthDate에 @Getter 적용 및 수동 getValue() 제거
madirony Feb 10, 2026
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
45 changes: 0 additions & 45 deletions .codeguide/loopers-1-week.md

This file was deleted.

196 changes: 196 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# CLAUDE.md

이 파일은 Claude Code가 프로젝트 작업 시 참조하는 컨텍스트 문서입니다.

## 프로젝트 개요

**loopers-java-spring-template** - Loopers에서 제공하는 Spring + Java 멀티모듈 템플릿 프로젝트입니다.

- **Group**: `com.loopers`
- **Language**: Java 21
- **Build Tool**: Gradle 8.13 (Kotlin DSL)
- **Framework**: Spring Boot 3.4.4

## 기술 스택

### Core
| 기술 | 버전 |
|------|------|
| Java | 21 |
| Gradle | 8.13 |
| Spring Boot | 3.4.4 |
| Spring Cloud | 2024.0.1 |
| Spring Dependency Management | 1.1.7 |

### Database & Cache
| 기술 | 용도 |
|------|------|
| Spring Data JPA | ORM |
| QueryDSL (jakarta) | 타입 세이프 쿼리 |
| MySQL | RDBMS |
| Spring Data Redis | 캐시/세션 |
| Spring Kafka | 메시지 브로커 |

### 테스트
| 기술 | 버전 |
|------|------|
| Testcontainers | 2.0.2 |
| Spring MockK | 4.0.2 |
| Mockito | 5.14.0 |
| Instancio JUnit | 5.0.2 |
| JaCoCo | 테스트 커버리지 |

### 문서화 & 모니터링
| 기술 | 버전/용도 |
|------|----------|
| SpringDoc OpenAPI | 2.7.0 |
| Micrometer + Prometheus | 메트릭 수집 |
| Micrometer Tracing (Brave) | 분산 추적 |
| Logback Slack Appender | 1.6.1 |

### 유틸리티
- Lombok
- Jackson (JSR310, Kotlin Module)

## 모듈 구조

```
Root
├── apps (실행 가능한 SpringBootApplication)
│ ├── commerce-api # REST API 서버 (Web + OpenAPI)
│ ├── commerce-batch # Spring Batch 애플리케이션
│ └── commerce-streamer # Kafka 기반 스트리밍 애플리케이션
├── modules (재사용 가능한 Configuration)
│ ├── jpa # JPA + QueryDSL + MySQL 설정
│ ├── redis # Redis 설정
│ └── kafka # Kafka 설정
└── supports (부가 기능 Add-on)
├── jackson # Jackson 직렬화 설정
├── logging # 로깅 + Slack Appender
└── monitoring # Actuator + Prometheus 메트릭
```

### 모듈 의존성

| App | 의존 모듈 |
|-----|----------|
| commerce-api | jpa, redis, jackson, logging, monitoring |
| commerce-batch | jpa, redis, jackson, logging, monitoring |
| commerce-streamer | jpa, redis, kafka, jackson, logging, monitoring |

## 빌드 & 실행

### 빌드
```bash
./gradlew build
```

### 테스트
```bash
./gradlew test
```

테스트 설정:
- Timezone: `Asia/Seoul`
- Profile: `test`
- 병렬 실행: 비활성화 (`maxParallelForks = 1`)

### 로컬 환경 실행
```bash
# 인프라 (MySQL, Redis, Kafka 등)
docker-compose -f ./docker/infra-compose.yml up

# 모니터링 (Prometheus, Grafana)
docker-compose -f ./docker/monitoring-compose.yml up
```

Grafana: http://localhost:3000 (admin/admin)

## 컨벤션

### 모듈 규칙
- `apps`: BootJar 활성화, 일반 Jar 비활성화
- `modules`, `supports`: 일반 Jar 활성화, BootJar 비활성화
- `modules`, `supports`는 `java-library` 플러그인 사용
- 테스트 픽스처는 `java-test-fixtures` 플러그인으로 관리

### 버전 관리
- 프로젝트 버전 미지정 시 Git short hash 사용
- 의존성 버전은 `gradle.properties`에서 중앙 관리

## 주요 디렉토리

```
/docker # Docker Compose 파일
/http # HTTP 요청 테스트 파일
/gradle # Gradle Wrapper
```

## 개발 컨벤션 (Strict Rules)

### Entity & Domain
- **Entity**: `@Setter` 사용 금지. 변경 로직은 도메인 메서드(예: `updatePassword()`)로 구현.
- **Lombok**: `@Getter`, `@NoArgsConstructor(access = AccessLevel.PROTECTED)` 기본 사용.
- **BaseEntity**: 모든 Entity는 `com.loopers.domain.BaseEntity`를 상속받아 생성/수정 시간을 관리.
- **Validation**: 생성자 시점에 `CoreException`을 사용하여 유효성 검증 수행.

### API & Exception
- **Response**: 모든 Controller 응답은 `com.loopers.interfaces.api.ApiResponse<T>`로 감싸서 반환.
- **Exception**: 예외 발생 시 `com.loopers.support.error.CoreException`과 `ErrorType`을 사용. Java 표준 예외(`IllegalArgumentException` 등) 사용 지양.

### Coding Style
- **Null Safety**: `Optional`을 적극 활용. `null`을 직접 반환하거나 파라미터로 받지 않음.
- **DI**: 생성자 주입(`@RequiredArgsConstructor`) 사용. Field Injection(`@Autowired`) 금지.

## 테스트 전략 (TDD Workflow)

**대원칙: Red(실패) -> Green(구현) -> Refactor(개선)** 순서를 반드시 준수한다.

### 1. 단위 테스트 (Unit Test)
- **대상**: Domain Entity, POJO
- **도구**: JUnit5, AssertJ
- **특징**: Spring Context 로딩 금지. 순수 자바 코드로 검증.
- **위치**: `apps/commerce-api/src/test/java/com/loopers/domain/**`

### 2. 통합 테스트 (Integration Test)
- **대상**: Service, Repository
- **도구**: `@SpringBootTest`, Testcontainers (MySQL)
- **규칙**: `com.loopers.utils.DatabaseCleanUp`을 사용하여 매 테스트 종료 후 데이터 초기화.
- **위치**: `apps/commerce-api/src/test/java/com/loopers/application/**`

### 3. E2E 테스트 (API Test)
- **대상**: Controller (HTTP 요청/응답)
- **도구**: `TestRestTemplate`
- **검증**: 실제 HTTP Status Code와 `ApiResponse` 본문 검증.

## Round 1 Quest 요구사항 (Current Context)

### 1. 회원가입
- **필수 정보**: ID, 비밀번호, 이름, 생년월일, 이메일
- **ID 규칙**: 영문/숫자 조합 10자 이내. 중복 불가.
- **비밀번호 규칙**:
- 8~16자
- 영문 대소문자, 숫자, 특수문자 필수 포함
- 생년월일 포함 불가
- 암호화하여 저장 필수
- **유효성 검사**: 이메일 형식, 생년월일(`yyyy-MM-dd`) 형식 검증.

### 2. 내 정보 조회
- **마스킹**: 이름의 마지막 글자를 `*`로 마스킹하여 반환 (예: `홍길동` -> `홍길*`).

### 3. 비밀번호 수정
- 현재 비밀번호 확인 후 새 비밀번호로 변경.
- 기존 비밀번호와 동일한 비밀번호 사용 불가.

## AI 페르소나 및 행동 지침
- **언어**: 한국어 (기술 용어는 영어 병기 가능)
- **우선순위**:
1. 실제 실행 가능한 코드 제공
2. 테스트 코드 우선 작성 (TDD)
3. 기존 프로젝트 구조(Multi-module) 준수
- **금지사항**:
- `System.out.println` 사용 금지 (로깅은 `@Slf4j` 사용)
- 불필요한 주석이나 설명으로 답변 길게 하지 말 것.
- 존재하지 않는 라이브러리를 임의로 추가하지 말 것.
3 changes: 3 additions & 0 deletions apps/commerce-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ dependencies {

// web
implementation("org.springframework.boot:spring-boot-starter-web")

// security (for password encoding)
implementation("org.springframework.security:spring-security-crypto")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.properties["springDocOpenApiVersion"]}")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.loopers.application.member;

import com.loopers.domain.member.Member;
import com.loopers.domain.member.MemberRepository;
import com.loopers.domain.member.MemberService;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
@RequiredArgsConstructor
public class MemberFacade {

private final MemberService memberService;
private final MemberRepository memberRepository;

@Transactional
public Member signup(MemberService.SignupCommand command) {
return memberService.signup(command);
}

@Transactional
public void changePassword(Member member, String currentPassword, String newPassword) {
Member managedMember = memberRepository.findByMemberIdValue(member.getMemberId().getValue())
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "회원을 찾을 수 없습니다."));
memberService.changePassword(managedMember, currentPassword, newPassword);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.loopers.config;

import com.loopers.domain.member.MemberRepository;
import com.loopers.domain.member.MemberService;
import com.loopers.domain.member.PasswordEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DomainConfig {

@Bean
public MemberService memberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
return new MemberService(memberRepository, passwordEncoder);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.loopers.domain.member;

import com.loopers.domain.BaseEntity;
import com.loopers.domain.member.vo.BirthDate;
import com.loopers.domain.member.vo.Email;
import com.loopers.domain.member.vo.MemberId;
import com.loopers.domain.member.vo.Name;
import com.loopers.domain.member.vo.Password;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "member")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseEntity {

@Embedded
@AttributeOverride(name = "value", column = @Column(name = "member_id", nullable = false, unique = true))
private MemberId memberId;

@Embedded
@AttributeOverride(name = "value", column = @Column(name = "password", nullable = false))
private Password password;

@Embedded
@AttributeOverride(name = "value", column = @Column(name = "name", nullable = false))
private Name name;

@Embedded
@AttributeOverride(name = "value", column = @Column(name = "email", nullable = false))
private Email email;

@Embedded
@AttributeOverride(name = "value", column = @Column(name = "birth_date", nullable = false))
private BirthDate birthDate;

public Member(MemberId memberId, Password password, Name name, Email email, BirthDate birthDate) {
this.memberId = memberId;
this.password = password;
this.name = name;
this.email = email;
this.birthDate = birthDate;
}

public void updatePassword(String currentPassword, String newPassword, PasswordEncoder encoder) {
this.password = this.password.change(currentPassword, newPassword, this.birthDate, encoder);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.loopers.domain.member;

import java.util.Optional;

public interface MemberRepository {

Member save(Member member);

Optional<Member> findByMemberIdValue(String memberIdValue);

boolean existsByMemberIdValue(String memberIdValue);
}
Loading