Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c0dcfd8
remove: deprecated codeguide
hanyoung-kurly Feb 1, 2026
c49eec3
chore: gitignore 내용 추가
plan11plan Feb 3, 2026
aa462f9
docs: 온보딩 궁금증 정리
plan11plan Feb 3, 2026
d105efb
feat: 유저 모델 생성 기능 추가
plan11plan Feb 5, 2026
8dd32ec
feat: 유저 이름 생성 기능 추가
plan11plan Feb 5, 2026
1069a2d
feat: 생년월일 생성 기능 추가
plan11plan Feb 5, 2026
c8625fe
feat: 로그인Id 생성 기능 추가
plan11plan Feb 5, 2026
57e6ada
feat: 이름 마스킹 기능 추가
plan11plan Feb 5, 2026
0243f12
feat: 이메일 생성 기능 추가
plan11plan Feb 5, 2026
2398e81
feat: 패스워드 생성 및 생일 검증 기능 추가
plan11plan Feb 5, 2026
c130402
feat: 회원가입 기능 구현 및 통합 테스트 작성
plan11plan Feb 5, 2026
47c6353
feat: 회원가입 API 구현 및 E2E 테스트 작성
plan11plan Feb 5, 2026
8620b0d
docs: 기능 요구 사항 작성
plan11plan Feb 5, 2026
7472c4c
feat: 사용자 인증 서비스 구현
plan11plan Feb 5, 2026
249a25f
feat: 내 정보 조회 서비스 로직 구현
plan11plan Feb 5, 2026
dc35f18
feat: 내 정보 조회 API 구현
plan11plan Feb 5, 2026
b00608d
feat: 인증 서비스 단위 테스트 작성
plan11plan Feb 5, 2026
264e51a
feat: 내 정보 조회 통합 테스트 및 E2E 테스트 작성
plan11plan Feb 5, 2026
5f11aef
docs: 내 정보 조회 API HTTP 테스트 케이스 추가
plan11plan Feb 5, 2026
09bbbe1
feat: 비밀번호 변경 도메인 로직 구현
plan11plan Feb 5, 2026
ec973ba
feat: 비밀번호 변경 서비스 로직 구현
plan11plan Feb 5, 2026
46d8afe
feat: 비밀번호 변경 API 구현
plan11plan Feb 5, 2026
ec29637
test: 비밀번호 변경 단위 테스트 작성
plan11plan Feb 5, 2026
b6350d4
test: 비밀번호 변경 통합 테스트 및 E2E 테스트 작성
plan11plan Feb 5, 2026
641e21b
docs: 비밀번호 변경 API HTTP 테스트 케이스 추가
plan11plan Feb 5, 2026
521127d
test: 회원가입-내정보조회-비밀번호변경-내정보조회 시나리오 테스트 추가
plan11plan Feb 5, 2026
3b3ef73
feat: 패스워드 암호화 기능 추가
plan11plan Feb 6, 2026
fd18f15
fix: 비밀번호 변경 시 암호화 적용
plan11plan Feb 6, 2026
899e211
docs: 과제 진행 과정 작성
plan11plan Feb 6, 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.

6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,9 @@ out/

### Kotlin ###
.kotlin

### Claude Code ###
.claude/

### Documentation ###
docs/
105 changes: 71 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,77 @@
# Loopers Template (Spring + Java)
Loopers 에서 제공하는 스프링 자바 템플릿 프로젝트입니다.

## Getting Started
현재 프로젝트 안정성 및 유지보수성 등을 위해 아래와 같은 장치를 운용하고 있습니다. 이에 아래 명령어를 통해 프로젝트의 기반을 설치해주세요.
### Environment
`local` 프로필로 동작할 수 있도록, 필요 인프라를 `docker-compose` 로 제공합니다.
```shell
docker-compose -f ./docker/infra-compose.yml up
# Round-1
### 시작 전 목표
```
- 나의 의도를 테스트 코드로 작성한다.
- TDD 방식으로 AI와 함께 기능 구현해본다.
- TDD로 요구사항을 먼저 정리하는 장점을 느껴본다.
- 작게 쪼개고 점진적으로 설계하는 과정을 느껴본다.
- 리팩토링이 가능하다는것을 느껴본다.
```
### Monitoring
`local` 환경에서 모니터링을 할 수 있도록, `docker-compose` 를 통해 `prometheus` 와 `grafana` 를 제공합니다.

애플리케이션 실행 이후, **http://localhost:3000** 로 접속해, admin/admin 계정으로 로그인하여 확인하실 수 있습니다.
```shell
docker-compose -f ./docker/monitoring-compose.yml up
### 시작 후 목표
- 요구사항을 AI 실행 프롬프트로 구조화하는 방법 알아보기
```
내 현재 학습 초점은 '설계는 내가, 구현은 AI가'라는 명확한 역할 분리다.
TDD 방식으로 AI에게 코딩을 위임하면서도 설계 결정권은 내가 가져가는 방식이다.
여기서 질문이 생겼는데, 기능 요구사항을 받았을 때, 어떤 변환 과정을 거쳐 AI가 정확히 구현할 수 있는 프롬프트 형태로 만들어지는가?'

## About Multi-Module Project
본 프로젝트는 멀티 모듈 프로젝트로 구성되어 있습니다. 각 모듈의 위계 및 역할을 분명히 하고, 아래와 같은 규칙을 적용합니다.
이 변환 과정을 알아보는 것을 목표로 했다.
```

- apps : 각 모듈은 실행가능한 **SpringBootApplication** 을 의미합니다.
- modules : 특정 구현이나 도메인에 의존적이지 않고, reusable 한 configuration 을 원칙으로 합니다.
- supports : logging, monitoring 과 같이 부가적인 기능을 지원하는 add-on 모듈입니다.
### 내가 한 시도

```
Root
├── apps ( spring-applications )
│ ├── 📦 commerce-api
│ ├── 📦 commerce-batch
│ └── 📦 commerce-streamer
├── modules ( reusable-configurations )
│ ├── 📦 jpa
│ ├── 📦 redis
│ └── 📦 kafka
└── supports ( add-ons )
├── 📦 jackson
├── 📦 monitoring
└── 📦 logging
```
시도 1: AI가 설계하고, AI가 결정
- 방식: 기능 요구사항을 그대로 던져주고 TDD(Red-Green-Refactor)로 개발하라고 했다.
- 문제점: 이 과정에서 내 설계도, 내 의도도 없다는 걸 자각했다. AI가 알아서 다 했을 뿐이었다.

시도 2: AI가 설계하고, 내가 결정
- 방식: AI가 설계 보고서를 작성하면 내가 읽고 결정하는 위치에 서기로 했다.
- 문제점:
- AI가 너무 복잡한 설계를 제시했다.
- 그 문서를 읽는 데 시간이 많이 들었고, 전부 읽을 수도 없었다.
- 내 의도가 담긴 설계라고 전혀 느껴지지 않았다.

시도 3: 내가 설계하고, AI의 피드백을 받기
- 방식: 기능 요구사항으로부터 객체와 메시지 정의, 검증 및 예외 케이스를 내가 직접 문서로 정리했다.
- 좋았던 점:
- 오직 내 설계 틀 안에서 AI가 고려해줘서, 유용한 피드백을 받았다.
- 레이어별 특징, 해야 할 것, 하지 말아야 할 것을 먼저 정의해야 한다는 걸 배웠다.
- 내가 놓친 엣지 케이스나 잠재적 문제를 해상도 높게 알려주고, 판단을 요구해줬다.
- 문제점:
- 회원가입 기능을 도메인부터 API까지 전부 수도코드로 문서화하고 있었는데, 이러다 보니 생각이 들었다.
- "수도코드로 문서화할 바에, 그냥 내가 코드로 먼저 작성하고 이 스타일대로 나머지 기능을 구현하라고 하는 게 더 내 의도가 담긴 코드 아닌가?"
- 요구사항 규칙뿐만 아니라 개발 규칙들을 명확히 하는 것이 부족했다.
- 내 의도를 설명하면서 개발 스타일을 알려줄 때, 지금처럼 문서로 전달하는 게 맞는지, 아니면 내가 기능 구현 하나를 예시로 만들어주고 이 스타일을 참고해서 개발하라고 해야 할지 고민이 들었다.

시도 4: 내가 설계하고 시범 구현을 작성하고, AI에게 이 방식대로 하라고 명령
- 방식: 회원가입 기능을 TDD(Red-Green-Refactor)로 도메인에서 API까지 직접 구현하고, 내 코드 스타일대로 다른 기능 개발을 맡겼다.
- 진행 과정:
- AI는 A부터 Z까지 전부 만들어냈다.
- 나는 왜 그렇게 설계했고 만들었는지 물어보는 식으로 AI의 설계 사고를 배우고 있었다.
- 이 내용들은 내가 설계를 진행했어도 AI에게 물어볼 내용들이었다.
- 내 의도가 담긴 코드라기보다는, 나는 그 의도를 이해해 나가고, 동의하지 않으면 내 설계를 담아내는 식으로 진행했다.
- 문제점:
- 한 번 구현하라고 할 때마다 오래 걸렸다. 처음부터 API까지 관련 모든 코드를 20분 동안 작업하고 있었다.
- 전부 한 큐에 완성시키기 때문에 양이 방대했다.
- 나는 추가된 main 코드 확인과 테스트 통과 여부만 확인하고 있었다.
- 이게 맞는 방식인지는 아직 잘 모르겠다.



### 과제 후 느낀점
- AI 협업에 대한 미해결 고민
- 아직 어떻게 AI를 파트너로 협업하는 것인지 깨닫지 못했다.
- 이번 과제는 사실 처음부터 완벽한 설계를 만들고 AI에게 개발하라고 하는 게 아니라, 기능 요구사항으로부터 개발자가 직접 TDD를 구현하면서 이 과정에서 궁금하거나, 막히거나, 노가다 과정을 AI에게 맡기는 것을 기대한 과제였던 걸까?
- 클로드 코드 사용 경험
- 이번에 클로드 코드를 사용해서 개발해보는 건 처음인데, 한 번의 명령으로 A부터 Z까지 기능 구현, 문서화, 테스트 코드 전부 구현해줘서 놀라웠다.
- 더 이상 구현은 중요하지 않다는 것을 이번에 체감했다.
- 대신 개발이 되는 환경을 잘 이해하는 것, 계층 책임이나 객체 책임 및 검증 스코프를 잘 정의하는 것, 이런 것들이 더 중요한 것 같은 느낌을 받았다.
- 클로드 스킬에 관심이 생겼다.



---

## 📋 기능 요구 사항

기능 요구 사항 정리 [ToDoList.md](./ToDoList.md)
42 changes: 42 additions & 0 deletions ToDoList.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 기능 요구 사항


## 1. 회원가입

### 요구사항
- **필요 정보 : { 로그인 ID, 비밀번호, 이름, 생년월일, 이메일 }**
- 이미 가입된 로그인 ID 로는 가입이 불가능함
- 각 정보는 포맷에 맞는 검증 필요 (이름, 이메일, 생년월일)
- 비밀번호는 암호화해 저장하며, 아래와 같은 규칙을 따름
```
1. 8~16자의 영문 대소문자, 숫자, 특수문자만 가능합니다.
2. 생년월일은 비밀번호 내에 포함될 수 없습니다.
```
> 이후, 유저 정보가 필요한 모든 요청은 아래 헤더를 통해 요청
> * X-Loopers-LoginId : 로그인 ID
> * X-Loopers-LoginPw : 비밀번호


---

## 2. 내 정보 조회
- **반환 정보 : { 로그인 ID, 이름, 생년월일, 이메일 }**
- 로그인 ID 는 영문과 숫자만 허용
- 이름은 마지막 글자를 마스킹해 반환

> 마스킹 문자는 `*` 로 통일
>

---

## 3. 비밀번호 수정
- **필요 정보 : { 기존 비밀번호, 새 비밀번호 }**
- 비밀 번호 RULE 을 따르되, 현재 비밀번호는 사용할 수 없습니다.

> **비밀번호 RULE**
> * 영문 대/소문자, 숫자, 특수문자 사용 가능
> * 생년월일 사용 불가


---

3 changes: 3 additions & 0 deletions apps/commerce-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.properties["springDocOpenApiVersion"]}")

// security
implementation("org.springframework.security:spring-security-crypto")

// querydsl
annotationProcessor("com.querydsl:querydsl-apt::jakarta")
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.loopers.domain;

import com.loopers.infrastructure.PasswordEncoder;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AuthenticationService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

public UserModel authenticate(String loginIdValue, String rawPassword) {
LoginId loginId = new LoginId(loginIdValue);

UserModel user = userRepository.find(loginId)
.orElseThrow(() -> new CoreException(ErrorType.UNAUTHORIZED, "로그인 ID 또는 비밀번호가 일치하지 않습니다."));

if (!passwordEncoder.matches(rawPassword, user.getPassword().getValue())) {
throw new CoreException(ErrorType.UNAUTHORIZED, "로그인 ID 또는 비밀번호가 일치하지 않습니다.");
}

return user;
}
}
41 changes: 41 additions & 0 deletions apps/commerce-api/src/main/java/com/loopers/domain/BirthDate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.loopers.domain;

import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import jakarta.persistence.Embeddable;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import lombok.EqualsAndHashCode;

@Embeddable
@EqualsAndHashCode
public class BirthDate {

private static final DateTimeFormatter DATE_STRING_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");

private LocalDate birthDate;

protected BirthDate() {}

public BirthDate(LocalDate birthDate) {
validate(birthDate);
this.birthDate = birthDate;
}

private void validate(LocalDate birthDate) {
if (birthDate == null) {
throw new CoreException(ErrorType.BAD_REQUEST,"생년월일은 필수 입력값입니다.");
}
if (birthDate.isAfter(LocalDate.now())) {
throw new CoreException(ErrorType.BAD_REQUEST,"생년월일은 과거 날짜여야 합니다.");
}
}

public String toDateString() {
return birthDate.format(DATE_STRING_FORMATTER);
}

public LocalDate getDate() {
return birthDate;
}
}
34 changes: 34 additions & 0 deletions apps/commerce-api/src/main/java/com/loopers/domain/Email.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.loopers.domain;

import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import jakarta.persistence.Embeddable;
import java.util.regex.Pattern;
import lombok.EqualsAndHashCode;
import lombok.Getter;

@Embeddable
@Getter
@EqualsAndHashCode
public class Email {
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9+_.-]+@([A-Za-z0-9-]+\\.)+[A-Za-z]{2,}$");

private String mail;

protected Email() {}

public Email(String mail) {
validateEmail(mail);
this.mail = mail;
}

private void validateEmail(String mail) {
if (mail == null || mail.isBlank()) {
throw new CoreException(ErrorType.BAD_REQUEST, "이메일은 비어있을 수 없습니다.");
}

if (!EMAIL_PATTERN.matcher(mail).matches()) {
throw new CoreException(ErrorType.BAD_REQUEST, "이메일 형식이 올바르지 않습니다.");
}
}
}
41 changes: 41 additions & 0 deletions apps/commerce-api/src/main/java/com/loopers/domain/LoginId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.loopers.domain;

import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
import jakarta.persistence.Embeddable;
import java.util.regex.Pattern;
import lombok.EqualsAndHashCode;

@Embeddable
@EqualsAndHashCode
public class LoginId {
private static final int MIN_LENGTH = 4;
private static final int MAX_LENGTH = 12;

private static final Pattern ALPHANUMERIC_PATTERN = Pattern.compile("^[a-zA-Z0-9]*$");

private String value;

protected LoginId() {}

public LoginId(String value) {
validate(value);
this.value = value;
}

private void validate(String value) {
if (value == null || value.isBlank()) {
throw new CoreException(ErrorType.BAD_REQUEST, "로그인 ID는 비어있을 수 없습니다.");
}
if (value.length() < MIN_LENGTH || value.length() > MAX_LENGTH) {
throw new CoreException(ErrorType.BAD_REQUEST, "로그인 ID는 4자에서 12자 사이여야 합니다.");
}
if (!ALPHANUMERIC_PATTERN.matcher(value).matches()) {
throw new CoreException(ErrorType.BAD_REQUEST, "로그인 ID는 영문과 숫자만 허용됩니다.");
}
}

public String getValue() {
return value;
}
}
Loading