-
Notifications
You must be signed in to change notification settings - Fork 44
[volume-1] 회원가입/내 정보 조회/비밀번호 수정 기능 작성 #27
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: SukheeChoi
Are you sure you want to change the base?
Changes from all commits
3562127
f47a7d9
eeb137c
23fa221
533790c
e6ba4dd
24c7c17
4e2522f
a0bba79
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 Code ### | ||
| CLAUDE.md | ||
This file was deleted.
This file was deleted.
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,57 @@ | ||
| 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.LoginId; | ||
| import com.loopers.domain.member.vo.Password; | ||
| import com.loopers.support.error.CoreException; | ||
| import com.loopers.support.error.ErrorType; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Embedded; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.Table; | ||
|
|
||
| @Entity | ||
| @Table(name = "member") | ||
| public class Member extends BaseEntity { | ||
|
|
||
| @Embedded | ||
| private LoginId loginId; | ||
|
|
||
| @Embedded | ||
| private Password password; | ||
|
|
||
| @Column(nullable = false, length = 50) | ||
| private String name; | ||
|
|
||
| @Embedded | ||
| private BirthDate birthDate; | ||
|
|
||
| @Embedded | ||
| private Email email; | ||
|
|
||
| protected Member() {} | ||
|
|
||
| public Member(LoginId loginId, Password password, String name, | ||
| BirthDate birthDate, Email email) { | ||
| if (name == null || name.isBlank()) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "이름은 필수입니다."); | ||
| } | ||
| this.loginId = loginId; | ||
| this.password = password; | ||
| this.name = name; | ||
| this.birthDate = birthDate; | ||
| this.email = email; | ||
| } | ||
|
|
||
| public LoginId getLoginId() { return loginId; } | ||
| public Password getPassword() { return password; } | ||
| public String getName() { return name; } | ||
| public BirthDate getBirthDate() { return birthDate; } | ||
| public Email getEmail() { return email; } | ||
|
|
||
| public void changePassword(Password newPassword) { | ||
| this.password = newPassword; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.loopers.domain.member; | ||
|
|
||
| import com.loopers.domain.member.vo.LoginId; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| public interface MemberRepository { | ||
| Member save(Member member); | ||
| Optional<Member> findByLoginId(LoginId loginId); | ||
| boolean existsByLoginId(LoginId loginId); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| package com.loopers.domain.member; | ||
|
|
||
| import com.loopers.domain.member.vo.BirthDate; | ||
| import com.loopers.domain.member.vo.Email; | ||
| import com.loopers.domain.member.vo.LoginId; | ||
| import com.loopers.domain.member.vo.Password; | ||
| import com.loopers.support.error.CoreException; | ||
| import com.loopers.support.error.ErrorType; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.security.crypto.password.PasswordEncoder; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Service | ||
| @Transactional(readOnly = true) | ||
| public class MemberService { | ||
|
|
||
| private final MemberRepository memberRepository; | ||
| private final PasswordEncoder passwordEncoder; | ||
|
|
||
| @Transactional | ||
| public Member register(String loginId, String plainPassword, String name, | ||
| String birthDate, String email) { | ||
| LoginId loginIdVo = new LoginId(loginId); | ||
|
|
||
| if (memberRepository.existsByLoginId(loginIdVo)) { | ||
| throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 ID입니다."); | ||
| } | ||
|
|
||
| BirthDate birthDateVo = BirthDate.from(birthDate); | ||
| Password password = Password.create(plainPassword, birthDateVo.value(), passwordEncoder); | ||
| Email emailVo = new Email(email); | ||
|
|
||
| Member member = new Member(loginIdVo, password, name, birthDateVo, emailVo); | ||
| return memberRepository.save(member); | ||
| } | ||
|
|
||
| public Optional<Member> findByLoginId(String loginId) { | ||
| return memberRepository.findByLoginId(new LoginId(loginId)); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void changePassword(Member member, String currentPlain, String newPlain) { | ||
| if (!member.getPassword().matches(currentPlain, passwordEncoder)) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "현재 비밀번호가 일치하지 않습니다."); | ||
| } | ||
|
|
||
| if (member.getPassword().matches(newPlain, passwordEncoder)) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, | ||
| "새 비밀번호는 현재 비밀번호와 달라야 합니다."); | ||
| } | ||
|
|
||
| Password newPassword = Password.create( | ||
| newPlain, member.getBirthDate().value(), passwordEncoder); | ||
| member.changePassword(newPassword); | ||
| } | ||
|
Comment on lines
+45
to
+59
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. 🧩 Analysis chain🏁 Script executed: # 1. AuthMemberResolver의 구현 및 트랜잭션 설정 확인
find . -name "*AuthMemberResolver*" -type f
# 2. MemberService의 전체 구조 확인
cat -n apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java
# 3. MemberRepository의 메서드 확인
find . -name "*MemberRepository*" -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 2983 🏁 Script executed: # AuthMemberResolver 찾기 (패턴 확대)
find . -name "*.java" -type f | xargs grep -l "AuthMemberResolver" | head -5
# MemberRepositoryImpl 내용 확인
cat -n apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberRepositoryImpl.java
# changePassword를 호출하는 컨트롤러 찾기
rg -n "changePassword" --type java -A3 -B3
# Member 엔티티 확인 (detached 상태 판단을 위해)
find . -name "Member.java" -path "*/domain/member/*" -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 6479 🏁 Script executed: # AuthMemberResolver 파일 내용 확인
cat -n apps/commerce-api/src/main/java/com/loopers/support/auth/AuthMemberResolver.java
# Member 엔티티 전체 확인
cat -n apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java
# WebMvcConfig 확인 (AuthMemberResolver 등록)
cat -n apps/commerce-api/src/main/java/com/loopers/support/auth/WebMvcConfig.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 5606 🏁 Script executed: # 통합 테스트 또는 엔드투엔드 테스트 존재 여부 확인
find . -name "*MemberServiceTest*" -o -name "*MemberIntegrationTest*" | head -5
# MemberServiceTest 또는 유사 테스트 파일 검색
find . -path "*/test/*" -name "*Member*Service*.java" -type f
# 마이그레이션 또는 다른 서비스에서 detached entity 처리 패턴 확인
rg -n "findByLoginId" --type java -A3 -B1 | grep -A3 -B1 "Transactional"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 610 🏁 Script executed: # MemberServiceIntegrationTest 전체 내용 확인
cat -n ./apps/commerce-api/src/test/java/com/loopers/domain/member/MemberServiceIntegrationTest.java
# changePassword 테스트 케이스가 있는지 확인
rg -n "changePassword" ./apps/commerce-api/src/test/java/com/loopers/domain/member/ -A10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 5119 🏁 Script executed: # BaseEntity 확인 (ID 생성 방식, 버전 관리 등)
find . -name "BaseEntity.java" -type f | xargs cat -n
# MemberJpaRepository 확인 (Spring Data JPA 메서드 확인)
find . -name "MemberJpaRepository.java" -type f | xargs cat -nRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 2728
비밀번호 변경이 DB에 반영되지 않으면 사용자가 새 비밀번호로 로그인할 수 없는 심각한 장애로 이어진다. 해결 방안: 현재 트랜잭션 컨텍스트에서 엔티티를 재조회하여 영속 상태를 보장한다. 수정 코드 `@Transactional`
public void changePassword(Member member, String currentPlain, String newPlain) {
+ // 현재 트랜잭션 컨텍스트에서 엔티티를 다시 조회하여 영속 상태 보장
+ Member managedMember = memberRepository.findByLoginId(member.getLoginId())
+ .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "회원을 찾을 수 없습니다."));
+
- if (!member.getPassword().matches(currentPlain, passwordEncoder)) {
+ if (!managedMember.getPassword().matches(currentPlain, passwordEncoder)) {
throw new CoreException(ErrorType.BAD_REQUEST, "현재 비밀번호가 일치하지 않습니다.");
}
- if (member.getPassword().matches(newPlain, passwordEncoder)) {
+ if (managedMember.getPassword().matches(newPlain, passwordEncoder)) {
throw new CoreException(ErrorType.BAD_REQUEST,
"새 비밀번호는 현재 비밀번호와 달라야 합니다.");
}
Password newPassword = Password.create(
- newPlain, member.getBirthDate().value(), passwordEncoder);
- member.changePassword(newPassword);
+ newPlain, managedMember.getBirthDate().value(), passwordEncoder);
+ managedMember.changePassword(newPassword);
}추가로 🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package com.loopers.domain.member.policy; | ||
|
|
||
| import com.loopers.support.error.CoreException; | ||
| import com.loopers.support.error.ErrorType; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.List; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| public class PasswordPolicy { | ||
|
|
||
| private static final Pattern FORMAT_PATTERN = | ||
| Pattern.compile("^[A-Za-z0-9!@#$%^&*()_+=-]{8,16}$"); | ||
|
|
||
| public static void validate(String plain, LocalDate birthDate) { | ||
| validateFormat(plain); | ||
| validateNotContainsSubstrings(plain, | ||
| extractBirthDateStrings(birthDate), | ||
| "비밀번호에 생년월일을 포함할 수 없습니다."); | ||
| } | ||
|
|
||
| public static void validateFormat(String plain) { | ||
| if (plain == null || !FORMAT_PATTERN.matcher(plain).matches()) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, | ||
| "비밀번호는 8~16자의 영문, 숫자, 특수문자만 허용됩니다."); | ||
| } | ||
| } | ||
|
|
||
| public static void validateNotContainsSubstrings( | ||
| String plain, List<String> forbidden, String errorMessage) { | ||
| for (String s : forbidden) { | ||
| if (plain.contains(s)) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, errorMessage); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public static List<String> extractBirthDateStrings(LocalDate birthDate) { | ||
| return List.of( | ||
| birthDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")), | ||
| birthDate.format(DateTimeFormatter.ofPattern("yyMMdd")) | ||
| ); | ||
|
Comment on lines
+16
to
+43
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. birthDate null 처리 누락으로 500이 발생할 수 있다다 운영 관점: 수정안 예시다 public class PasswordPolicy {
public static void validate(String plain, LocalDate birthDate) {
+ if (birthDate == null) {
+ throw new CoreException(ErrorType.BAD_REQUEST,
+ "생년월일은 필수입니다.");
+ }
validateFormat(plain);
validateNotContainsSubstrings(plain,
extractBirthDateStrings(birthDate),
"비밀번호에 생년월일을 포함할 수 없습니다.");
}🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
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.
🧩 Analysis chain
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 236
🏁 Script executed:
find apps/commerce-api -type f -name "LoginId.java"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 158
🏁 Script executed:
fd -e sql -o -e yaml | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 201
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 3077
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 112
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1458
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
find apps/commerce-api -type f -name "*.sql"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
find apps/commerce-api -path "*/resources/db/*" -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 414
🏁 Script executed:
rg -A 20 "interface MemberRepository" apps/commerce-api/src/main/java/Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 653
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1176
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2724
🏁 Script executed:
rg -B 5 -A 20 "public Member register" apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1037
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 158
🏁 Script executed:
rg -n "DataIntegrityViolationException" apps/commerce-api/src/main/java/Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 315
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2716
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 741
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 174
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2623
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
register메서드의 동시성 이슈에 따른 예외 처리 개선이 필요하다.existsByLoginId체크와save사이의 race condition으로 인해 두 동시 요청이 동일한 loginId로 가입 시도 시 둘 다 체크를 통과한 후 하나의 요청은 DB 레벨의 유니크 제약에서DataIntegrityViolationException을 발생시킨다. 현재 코드에서 이 예외가 처리되지 않아 사용자에게 일관되지 않은 오류 응답이 전달된다.LoginId VO의 login_id 컬럼에 유니크 제약(
@Column(name = "login_id", nullable = false, unique = true, length = 20))이 설정되어 있어 데이터 무결성은 보장되지만,memberRepository.save(member)호출 시 발생하는DataIntegrityViolationException을 catch하여CoreException(ErrorType.CONFLICT, "이미 존재하는 ID입니다.")로 변환해야 한다. 이를 통해 사용자에게 일관된 오류 메시지를 제공할 수 있다.🤖 Prompt for AI Agents