Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c7033a1
test: User 도메인 단위 테스트 추가
iohyeon Feb 3, 2026
a72ca5d
test: User 도메인 단위 테스트 추가
iohyeon Feb 3, 2026
34f3918
test: User 도메인 패스워드 단위 테스트 추가
iohyeon Feb 4, 2026
e83aa32
test: User 도메인 이름, 이메일, 생일 단위 테스트 추가
iohyeon Feb 4, 2026
9f57cd0
refactor: ErrorType을 interface로 분리하고 도메인별 에러코드 정의
iohyeon Feb 4, 2026
f272db9
refactor: Password VO에서 인코딩 책임 제거, 교차 검증을 PasswordPolicy로 분리
iohyeon Feb 4, 2026
7fa465b
refactor: User 도메인 VO에 도메인 전용 에러타입(UserErrorType) 적용
iohyeon Feb 4, 2026
3013f15
feat: 회원가입, 내 정보 조회, 비밀번호 수정 기능 구현
iohyeon Feb 4, 2026
f3f3130
test: User 도메인 단위 테스트 및 API E2E 테스트 추가
iohyeon Feb 4, 2026
8829cdf
refactor: Auth User 패키지 분리 및 Auth Test 생성
Feb 5, 2026
3712005
refactor: Domain에서 Spring Security 의존성 제거 (DIP 적용)
iohyeon Feb 5, 2026
8a07da3
chore: Example 샘플 코드 삭제
iohyeon Feb 5, 2026
13ea51b
fix: 비밀번호 변경 시 Detached Entity로 인한 DB 미반영 버그 수정
iohyeon Feb 5, 2026
f376c3a
test: AuthFacade, UserFacade 통합 테스트 추가
iohyeon Feb 5, 2026
d7addbf
test: E2E method 수정
iohyeon Feb 5, 2026
84dc3f9
test: Repository 통합 테스트
iohyeon Feb 5, 2026
ba17895
test: 새 비밀번호에 생년월일 포함 예외 추가
iohyeon Feb 5, 2026
67265cd
chore: Redis Testcontainers 설정 개선
iohyeon Feb 5, 2026
5dd5a9d
feat: claude.md 추가
iohyeon Feb 5, 2026
e04cc0c
feat: http auth, user 생성
iohyeon Feb 5, 2026
5deb1b7
refactor: 코드 품질을 위한 중복 stubbing 제거, PasswordPolicy 스타일 명확화, BirthDat…
iohyeon Feb 5, 2026
ada08d1
fix: CoreException에 cause 전달을 지원하여 예외 체인 유실 방지
iohyeon Feb 7, 2026
c1a7364
refactor: Lombok 제거 + this. 접두어 추가
iohyeon Feb 7, 2026
371402a
refactor: 테스트 메서드명 한글화
iohyeon Feb 7, 2026
f937190
refactor: 함수명 네이밍 수정
iohyeon Feb 7, 2026
3b0ba2e
docs: 주석 추가
iohyeon Feb 7, 2026
e53300a
fix: null 선검증 누락으로 인한 NPE 방지 및 CRUD 검증 단계 강화
iohyeon Feb 7, 2026
36da4ad
fix: BCryptPasswordEncryptor.java — null 방어 로직 추가
iohyeon Feb 7, 2026
e80650d
fix: loginIdValue 컬럼에 대한 unique 제약 설정
iohyeon Feb 7, 2026
690afc9
docs: claude.md 수정
iohyeon Feb 7, 2026
1ca4c56
fix: AuthV1Dto.java — toString() 재정의로 비밀번호 마스킹
iohyeon Feb 7, 2026
4479253
fix: 비밀번호 변경 시 검증 로직 추가 및 비밀번호 변경 성공 테스트 코드 추가
iohyeon Feb 7, 2026
5917066
fix: 민감 정보 분리
iohyeon Feb 7, 2026
6bf19b0
refactor: vo 패키지 분리 , Gender 추가
iohyeon Feb 7, 2026
417e4f3
refactor: Gender 제거 및 @AuthUser 인증 공통화 도입
iohyeon Feb 7, 2026
edbbd67
refactor: Gender 제거
iohyeon Feb 7, 2026
44913e4
refactor: Gender 제거 및 @AuthUser 인증 공통화 도입
iohyeon Feb 7, 2026
ea8cbeb
fix: Password VO 과설계 제거 및 PasswordPolicy 인라인
iohyeon Feb 7, 2026
75032b4
fix: 불필요한 UserFacade 계층 제거
iohyeon Feb 7, 2026
80fb46f
fix: Email VO 정규식 간소화 및 UserName 길이 검증 누락 수정
iohyeon Feb 7, 2026
ffdfbae
fix: 생년월일 NPE 체크 추가
iohyeon Feb 7, 2026
faed7b9
test: user 서비스 단위테스트 추가
iohyeon Feb 7, 2026
46ab63e
refactor: auth 패키지를 user로 통합 및 V1 네이밍 제거
iohyeon Feb 7, 2026
292f902
docs: 기능 별 주석 추가
iohyeon Feb 8, 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
593 changes: 593 additions & 0 deletions .claude/CLAUDE.md

Large diffs are not rendered by default.

158 changes: 158 additions & 0 deletions .claude/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# AI 협업 개발 가이드

본 문서는 AI 도구(Claude Code)와의 효과적인 협업을 위한 개발 원칙을 정의합니다.

## 1. 협업 철학 - 증강 코딩 (Augmented Coding)

AI는 개발자의 역량을 증강하는 도구이며, 의사결정의 주체는 개발자입니다.

- **제안과 승인**: AI는 방향성과 대안을 제안하고, 개발자가 최종 승인
- **설계 주도권**: 아키텍처와 설계 결정은 개발자가 주도
- **중간 개입**: 반복 동작, 미요청 기능 구현, 임의 테스트 삭제 시 개발자가 개입

## 2. 개발 방법론 - TDD (Red → Green → Refactor)

모든 코드는 테스트 주도 개발 방식으로 작성합니다.

### 2.1 Red Phase
- 요구사항을 만족하는 실패 테스트 케이스 먼저 작성
- 3A 원칙 준수 (Arrange - Act - Assert)

### 2.2 Green Phase
- 테스트를 통과하는 최소한의 코드 작성
- 오버엔지니어링 금지

### 2.3 Refactor Phase
- 코드 품질 개선 및 불필요한 코드 제거
- 객체지향 원칙 준수
- 모든 테스트 통과 필수

## 3. 코드 품질 기준

### 3.1 금지 사항 (Never Do)
- Mock 남발: 실제 동작하지 않는 코드, 과도한 Mock 사용 금지
- null-safety 위반: Optional 활용 필수 (Java)
- 디버깅 코드: println, System.out 등 잔류 금지

### 3.2 권장 사항 (Recommendation)
- E2E 테스트로 실제 API 동작 검증
- 재사용 가능한 객체 설계
- 성능 최적화 대안 제시
- 완성된 API는 `http/**/*.http`에 문서화

### 3.3 우선순위 (Priority)
1. 실제 동작하는 해결책만 고려
2. null-safety, thread-safety 보장
3. 테스트 가능한 구조 설계
4. 기존 코드 패턴과 일관성 유지

## 4. 오류 수정 컨벤션

오류를 수정할 때는 반드시 아래 형식으로 **오류 수정 이력** 섹션에 기록한다.

| 항목 | 설명 |
|------|------|
| **AS-IS** | 수정 전 코드 |
| **TO-BE** | 수정 후 코드 |
| **왜 (Why)** | 왜 이 수정이 필요한지 |
| **동작 원리** | 내부적으로 어떻게 동작하는지, 이유 |
| **검증 테스트** | 수정이 올바른지 확인하는 테스트 |

## 5. Git 컨벤션

- **커밋 주체**: 개발자진행 방향가 직접 수행 (AI 임의 커밋 금지)
- **커밋 메시지**: Conventional Commits 형식 권장

## 6. AI 협업 스타일

본 프로젝트에서 AI와의 협업은 다음 방식을 지향합니다:

| 스타일 | 설명 |
|--------|------|
| **Planning-first** | 개발자가 먼저 설계하고, AI로 검증 및 대안 비교 |
| **Explanation-seeking** | 코드의 이유, 원리, 동작에 대한 설명 요구 |
| **Iterative-reasoning** | 문제를 분해하여 추론 → 질문 → 수정 반복 |

> AI는 답을 제공하는 것이 아닌, 사고를 돕는 도구로 활용합니다.

---

## 오류 수정 이력

오류 수정 시 AS-IS / TO-BE / 왜(Why) / 동작 원리를 반드시 기록한다.

---

### [#1] CoreException 예외 원인(cause) 유실 문제

**출처**: CodeRabbit 리뷰

#### AS-IS

```java
// CoreException — cause를 받는 생성자 없음
public CoreException(ErrorType errorType, String customMessage) {
super(customMessage != null ? customMessage : errorType.getMessage());
this.errorType = errorType;
this.customMessage = customMessage;
}

// BirthDate.parseDate — 원래 예외(e)를 전달하지 않음
catch (DateTimeParseException e) {
throw new CoreException(UserErrorType.INVALID_BIRTH_DATE,
"생년월일은 YYYY-MM-DD 형식이어야 합니다.");
}
```

#### TO-BE

```java
// CoreException — cause를 받는 3-파라미터 생성자 추가
public CoreException(ErrorType errorType, String customMessage, Throwable cause) {
super(customMessage != null ? customMessage : errorType.getMessage(), cause);
this.errorType = errorType;
this.customMessage = customMessage;
}

// BirthDate.parseDate — 원래 예외(e)를 cause로 전달
catch (DateTimeParseException e) {
throw new CoreException(UserErrorType.INVALID_BIRTH_DATE,
"생년월일은 YYYY-MM-DD 형식이어야 합니다.", e);
}
```

#### 왜 (Why)

`catch`에서 새로운 예외를 던질 때 원래 예외(`e`)를 넘기지 않으면 **예외 체인(Exception Chain)이 끊긴다.**
운영 환경에서 장애가 발생했을 때 로그에 `Caused by`가 남지 않아, 정확히 어떤 입력값이 왜 실패했는지 추적할 수 없다.

#### 동작 원리

Java의 모든 예외는 `Throwable.cause` 필드를 가진다. `super(message, cause)`로 원인을 연결하면 예외 체인이 형성된다.

```
// cause 없는 경우 — 원인 추적 불가
CoreException: 생년월일은 YYYY-MM-DD 형식이어야 합니다.
at BirthDate.parseDate(BirthDate.java:52)

// cause 있는 경우 — 원인 추적 가능
CoreException: 생년월일은 YYYY-MM-DD 형식이어야 합니다.
at BirthDate.parseDate(BirthDate.java:52)
Caused by: DateTimeParseException: Text '1994/11/15' could not be parsed
at java.time.format.DateTimeFormatter.parseResolved0(...)
```

`Caused by`가 있어야 "어떤 값이, 어떤 이유로 파싱에 실패했는지" 정확히 파악할 수 있다. 이는 운영 환경에서 장애 원인 파악 시간을 줄이는 데 직결된다.

#### 검증 테스트

```java
@DisplayName("잘못된 형식이면, 예외의 원인으로 DateTimeParseException을 포함한다.")
@Test
void preservesCauseWhenInvalidFormat() {
CoreException exception = assertThrows(CoreException.class, () -> {
new BirthDate("1994/11/15");
});
assertThat(exception.getCause()).isInstanceOf(DateTimeParseException.class);
}
```
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ out/

### Kotlin ###
.kotlin

### HTTP Client ###
http-client.private.env.json
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 (BCrypt)
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
Expand Up @@ -12,7 +12,7 @@ public class CommerceApiApplication {

@PostConstruct
public void started() {
// set timezone
// JVM 기본 타임존을 Asia/Seoul로 설정하여 DB 저장, 로그 출력 시 시간대 일관성을 보장한다.
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
}

Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.loopers.application.user;

import com.loopers.domain.user.User;

import java.time.LocalDate;

/**
* 사용자 정보 DTO (Application Layer)
*
* Entity를 외부 계층에 노출하지 않기 위한 변환용 DTO.
* maskedName은 개인정보 보호를 위해 이름의 마지막 글자를 마스킹한 값이다.
*/
public record UserInfo(
String loginId,
String name,
String maskedName,
LocalDate birthDate,
String email
) {
public static UserInfo from(User user) {
if (user == null) {
throw new IllegalArgumentException("User는 null일 수 없습니다.");
}
return new UserInfo(
user.getLoginId().getValue(),
user.getName().getValue(),
user.getName().getMaskedValue(),
user.getBirthDate().getValue(),
user.getEmail().getValue()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.loopers.config;

import com.loopers.support.auth.AuthUserResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

/**
* Spring MVC 설정
*
* {@link AuthUserResolver}를 ArgumentResolver로 등록하여
* {@code @AuthUser} 어노테이션 기반의 인증된 사용자 주입을 활성화한다.
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

private final AuthUserResolver authUserResolver;

public WebMvcConfig(AuthUserResolver authUserResolver) {
this.authUserResolver = authUserResolver;
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(this.authUserResolver);
}
}

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.loopers.domain.user;

/**
* 비밀번호 암호화 포트 (Domain Layer)
*
* Domain이 인프라(Spring Security)에 의존하지 않도록 추상화한 인터페이스.
* 실제 구현은 Infrastructure 계층의 어댑터가 담당한다.
*/
public interface PasswordEncryptor {

/**
* 평문 비밀번호를 암호화한다.
*
* @param rawPassword 평문 비밀번호
* @return 암호화된 비밀번호
*/
String encode(String rawPassword);

/**
* 평문 비밀번호와 암호화된 비밀번호가 일치하는지 확인한다.
*
* @param rawPassword 평문 비밀번호
* @param encodedPassword 암호화된 비밀번호
* @return 일치 여부
*/
boolean matches(String rawPassword, String encodedPassword);
}
Loading