-
Notifications
You must be signed in to change notification settings - Fork 44
[volume-1] 회원가입/내 정보 조회/비밀번호 변경 기능 작업 #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: seonminKim1122
Are you sure you want to change the base?
Changes from all commits
f7acbe6
70c7861
2a0de07
2c02199
79a0dbd
42cceaa
798b687
a94f74e
b291a81
dc15642
b38b691
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,3 +38,6 @@ out/ | |
|
|
||
| ### Kotlin ### | ||
| .kotlin | ||
|
|
||
| ### CLAUDE ### | ||
| .claude | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| ## 개발 규칙 | ||
| ### 진행 Workflow - 증강 코딩 | ||
| - **대원칙** : 방향성 및 주요 의사 결정은 개발자에게 제안만 할 수 있으며, 최종 승인된 사항을 기반으로 작업을 수행. | ||
| - **중간 결과 보고** : AI 가 반복적인 동작을 하거나, 요청하지 않은 기능을 구현, 테스트 삭제를 임의로 진행할 경우 개발자가 개입. | ||
| - **설계 주도권 유지** : AI 가 임의판단을 하지 않고, 방향성에 대한 제안 등을 진행할 수 있으나 개발자의 승인을 받은 후 수행. | ||
|
|
||
| ### 개발 Workflow - TDD (Red > Green > Refactor) | ||
| - 모든 테스트는 3A 원칙으로 작성할 것 (Arrange - Act - Assert) | ||
| #### 1. Red Phase : 실패하는 테스트 먼저 작성 | ||
| - 요구사항을 만족하는 기능 테스트 케이스 작성 | ||
| - 테스트 예시 | ||
| #### 2. Green Phase : 테스트를 통과하는 코드 작성 | ||
| - Red Phase 의 테스트가 모두 통과할 수 있는 코드 작성 | ||
| - 오버엔지니어링 금지 | ||
| #### 3. Refactor Phase : 불필요한 코드 제거 및 품질 개선 | ||
| - 불필요한 private 함수 지양, 객체지향적 코드 작성 | ||
| - unused import 제거 | ||
| - 성능 최적화 | ||
| - 모든 테스트 케이스가 통과해야 함 | ||
|
|
||
| ## 주의사항 | ||
| ### 1. Never Do | ||
| - 실제 동작하지 않는 코드, 불필요한 Mock 데이터를 이요한 구현을 하지 말 것 | ||
| - null-safety 하지 않게 코드 작성하지 말 것 (Java 의 경우, Optional 을 활용할 것) | ||
| - println 코드 남기지 말 것 | ||
| - 당장 필요하지 않은 코드는 만들지 말 것 | ||
|
|
||
| ### 2. Recommendation | ||
| - 실제 API 를 호출해 확인하는 E2E 테스트 코드 작성 | ||
| - 재사용 가능한 객체 설계 | ||
| - 성능 최적화에 대한 대안 및 제안 | ||
| - 개발 완료된 API 의 경우, `.http/**.http` 에 분류해 작성 | ||
|
|
||
| ### 3. Priority | ||
| 1. 실제 동작하는 해결책만 고려 | ||
| 2. null-safety, thread-safety 고려 | ||
| 3. 테스트 가능한 구조로 설계 | ||
| 4. 기존 코드 패턴 분석 후 일관성 유지 | ||
|
|
||
| ## 소통규칙 | ||
| - 개발자의 의사를 확인해야 할 질문이 필요한 경우 사운드를 내서 알람을 준다 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package com.loopers.domain.user; | ||
|
|
||
| import com.loopers.support.error.CoreException; | ||
| import com.loopers.support.error.ErrorType; | ||
| import jakarta.persistence.Embeddable; | ||
| import lombok.AccessLevel; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.Objects; | ||
|
|
||
| @Embeddable | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class BirthDate { | ||
|
|
||
| private LocalDate value; | ||
|
|
||
| private BirthDate(LocalDate value) { | ||
| this.value = value; | ||
| } | ||
|
|
||
| public static BirthDate from(LocalDate value) { | ||
| if (value == null) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 비어있을 수 없습니다."); | ||
| } | ||
|
|
||
| if (value.isAfter(LocalDate.now())) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 오늘 이전 날짜만 가능합니다."); | ||
| } | ||
|
|
||
| return new BirthDate(value); | ||
| } | ||
|
|
||
| public LocalDate asLocalDate() { | ||
| return value; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(Object o) { | ||
| if (o == null || getClass() != o.getClass()) return false; | ||
| BirthDate birthDate = (BirthDate) o; | ||
| return Objects.equals(value, birthDate.value); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Objects.hashCode(value); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package com.loopers.domain.user; | ||
|
|
||
| import com.loopers.support.error.CoreException; | ||
| import com.loopers.support.error.ErrorType; | ||
| import jakarta.persistence.Embeddable; | ||
| import lombok.AccessLevel; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.util.Objects; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| @Embeddable | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| 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 value; | ||
|
|
||
| private Email(String value) { | ||
| this.value = value; | ||
| } | ||
|
|
||
| public static Email from(String value) { | ||
| if (value == null || value.isBlank()) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "이메일은 비어있을 수 없습니다."); | ||
| } | ||
|
|
||
| if (!EMAIL_PATTERN.matcher(value).matches()) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "이메일 형식이 올바르지 않습니다."); | ||
| } | ||
|
|
||
| return new Email(value); | ||
| } | ||
|
|
||
| public String asString() { | ||
| return value; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(Object o) { | ||
| if (o == null || getClass() != o.getClass()) return false; | ||
| Email email = (Email) o; | ||
| return Objects.equals(value, email.value); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Objects.hashCode(value); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package com.loopers.domain.user; | ||
|
|
||
| import com.loopers.support.error.CoreException; | ||
| import com.loopers.support.error.ErrorType; | ||
| import jakarta.persistence.Embeddable; | ||
| import lombok.AccessLevel; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.util.Objects; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| @Embeddable | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class LoginId { | ||
|
|
||
| private static final Pattern LOGIN_ID_PATTERN = Pattern.compile("^[a-zA-Z0-9]{5,20}$"); | ||
|
|
||
| private String value; | ||
|
|
||
| private LoginId(String value) { | ||
| this.value = value; | ||
| } | ||
|
|
||
| public static LoginId from(String value) { | ||
| if (value == null || value.isBlank()) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "로그인ID는 비어있을 수 없습니다."); | ||
| } | ||
|
|
||
| if (!LOGIN_ID_PATTERN.matcher(value).matches()) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "로그인ID는 5~20자의 영문과 숫자로만 이루어져야 합니다."); | ||
| } | ||
|
|
||
| return new LoginId(value); | ||
| } | ||
|
|
||
| public String asString() { | ||
| return value; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(Object o) { | ||
| if (o == null || getClass() != o.getClass()) return false; | ||
| LoginId loginId = (LoginId) o; | ||
| return Objects.equals(value, loginId.value); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Objects.hashCode(value); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| package com.loopers.domain.user; | ||
|
|
||
| import com.loopers.support.error.CoreException; | ||
| import com.loopers.support.error.ErrorType; | ||
| import jakarta.persistence.Embeddable; | ||
| import lombok.AccessLevel; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.util.Objects; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| @Embeddable | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class Name { | ||
|
|
||
| private static final Pattern NAME_PATTERN = Pattern.compile("^[가-힣]{2,6}$"); | ||
|
|
||
| private String value; | ||
|
|
||
| private Name(String value) { | ||
| this.value = value; | ||
| } | ||
|
|
||
|
|
||
| public static Name from(String value) { | ||
| if (value == null || value.isBlank()) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "이름은 비어있을 수 없습니다."); | ||
| } | ||
|
|
||
| if (!NAME_PATTERN.matcher(value).matches()) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "이름은 한글 2~6자만 가능합니다."); | ||
| } | ||
|
|
||
| return new Name(value); | ||
| } | ||
|
|
||
| public String asString() { | ||
| return value; | ||
| } | ||
|
|
||
| public String masked() { | ||
| return value.substring(0, value.length() - 1) + "*"; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(Object o) { | ||
| if (o == null || getClass() != o.getClass()) return false; | ||
| Name name = (Name) o; | ||
| return Objects.equals(value, name.value); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Objects.hashCode(value); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,43 @@ | ||||||||||||||||||||||
| package com.loopers.domain.user; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import com.loopers.support.error.CoreException; | ||||||||||||||||||||||
| import com.loopers.support.error.ErrorType; | ||||||||||||||||||||||
| import jakarta.persistence.Embeddable; | ||||||||||||||||||||||
| import lombok.AccessLevel; | ||||||||||||||||||||||
| import lombok.NoArgsConstructor; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import java.util.regex.Pattern; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| @Embeddable | ||||||||||||||||||||||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||||||||||||||||||||||
| public class Password { | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private static final Pattern PASSWORD_PATTERN = Pattern.compile("^[a-zA-Z0-9`~!@#$%^&*|'\";:\\\\₩?]{8,16}$"); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private String value; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private Password(String value) { | ||||||||||||||||||||||
| this.value = value; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public static Password of(String value, PasswordEncoder encoder) { | ||||||||||||||||||||||
| if (value == null || value.isBlank()) { | ||||||||||||||||||||||
| throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 비어있을 수 없습니다."); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (!PASSWORD_PATTERN.matcher(value).matches()) { | ||||||||||||||||||||||
| throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 8~16자의 영문대소문자, 숫자, 특수문자만 가능합니다."); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return new Password(encoder.encode(value)); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public String asString() { | ||||||||||||||||||||||
| return value; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Password는 저장 시 암호화 되므로 Password 간의 동일성/동등성은 PasswordEncoder 가 확인하는 것으로 한다. | ||||||||||||||||||||||
| public boolean matches(String raw, PasswordEncoder encoder) { | ||||||||||||||||||||||
| return encoder.matches(raw, value); | ||||||||||||||||||||||
|
Comment on lines
+39
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. matches 입력값 null/blank 처리 누락 🔧 제안 수정 public boolean matches(String raw, PasswordEncoder encoder) {
+ if (raw == null || raw.isBlank()) {
+ throw new CoreException(ErrorType.BAD_REQUEST, "비밀번호는 비어있을 수 없습니다.");
+ }
return encoder.matches(raw, value);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.loopers.domain.user; | ||
|
|
||
| public interface PasswordEncoder { | ||
|
|
||
| String encode(String raw); | ||
|
|
||
| boolean matches(String raw, String encoded); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PasswordEncoder null 방어가 필요하다
운영 관점에서 DI 설정 누락이나 테스트 스텁 미주입 시 NPE로 500이 발생해 장애 원인 파악이 어려워진다. 수정안으로
encoder에 대한 fail-fast 검사를 추가해 명확한 오류를 내도록 하라. 추가 테스트로encoder == null일 때 예외 타입/메시지를 검증하는 단위 테스트를 추가하라. As per coding guidelines: null 처리, 방어적 복사, 불변성, equals/hashCode/toString 구현 안정성을 점검한다.🔧 제안 수정
📝 Committable suggestion
🤖 Prompt for AI Agents