Skip to content

Feat/#13#15

Merged
ojy0903 merged 17 commits intodevelopfrom
feat/#13
Jan 31, 2026
Merged

Feat/#13#15
ojy0903 merged 17 commits intodevelopfrom
feat/#13

Conversation

@ojy0903
Copy link
Collaborator

@ojy0903 ojy0903 commented Jan 30, 2026

📌 관련 이슈

🚀 개요

이번 PR에서 변경된 핵심 내용을 요약해주세요.

비밀번호를 재설정 하려는 계정에 대해 이메일 인증 진행 후 비밀번호 재설정하는 API 추가

📄 작업 내용

구체적인 작업 내용을 설명해주세요.

  • 기존의 최초 회원가입 시 이메일 발송 로직을 템플릿화
  • 비밀번호 재설정 시 이메일 인증 로직과 최초 회원가입 시의 이메일 인증 로직에서 검증 부분을 구분
  • 이전 비밀번호와 같은 값으로 재설정 시도 시 예외 처리

📸 스크린샷 / 테스트 결과 (선택)

결과물 확인을 위한 사진이나 테스트 로그를 첨부해주세요.

  1. 최초 회원가입 (이메일 : test@example.com, 비밀번호: password)
최초 회원가입
  1. 비밀번호 재설정을 위해 test@example.com 에 대해 인증 이메일 발송
비번 재설정 이메일
  1. 이메일 인증 진행 (이메일 인증은 기존 최초 회원가입 시 사용하던 /email-verify 로 진행)
비번 재설정 이메일 인증완료
  1. 비밀번호 newPassword 로 재설정 완료 및 새로운 비밀번호로 정상 로그인 확인
비번 재설정 완료 새로운 비번 로그인

===예외 처리===

  1. 이메일 인증 없이 비밀번호 재설정 요청 API 에 접근한 경우 예외처리
    1-1) redis 비어있는 상태 -> 진행된 이메일 인증 없음
redis 비어있음

1-2) 이메일 인증 없이 비밀번호 재설정 요청 시 예외처리
인증 없이 비번 재설정 시도

  1. 기존 비밀번호와 동일한 비밀번호로 재설정 시도 시 예외 처리
    2-1) 이메일 test@example.com, 비밀번호 password 로 회원가입한 상태
최초 회원가입

2-2) 동일한 "password" 로 비밀번호 재설정 시도 시 예외처리
이전과 동일 비번으로 재설정 시도 오류

✅ 체크리스트

  • [✅] 브랜치 전략(GitHub Flow)을 준수했나요?
  • [✅] 메서드 단위로 코드가 잘 쪼개져 있나요?
  • [✅] 테스트 통과 확인
  • [✅] 서버 실행 확인
  • [✅] API 동작 확인

🔍 리뷰 포인트 (Review Points)

리뷰어가 중점적으로 확인했으면 하는 부분을 적어주세요. (P1~P4 적용 가이드)

  • 이전에 설정해뒀던 비밀번호와 동일한 비밀번호로 재설정 시도를 막는 로직을 넣어봤는데 어떤가요?
  • 기존 최초 회원가입 시 이메일 전송하는 것과 비밀번호 재설정 시 이메일 전송하는 게 중복 부분이 많은 것 같아
    템플릿 느낌으로 메서드를 빼서 만들어봤는데 오류의 여지가 있을까요? (EmailService 내부 sendEmail(), sendEmailForPwd() 메서드)
  • 이메일 인증코드를 받아 검증하는 API 는 기존의 최초 회원가입 시에 사용하던 /email-verify API 를 사용하도록 했는데
    이대로 해도 괜찮을까요 아니면 따로 추가 API 를 만드는 게 나을까요?

💬 리뷰어 가이드 (P-Rules)
P1: 필수 반영 (Critical) - 버그 가능성, 컨벤션 위반. 해결 전 머지 불가.
P2: 적극 권장 (Recommended) - 더 나은 대안 제시. 가급적 반영 권장.
P3: 제안 (Suggestion) - 아이디어 공유. 반영 여부는 드라이버 자율.
P4: 단순 확인/칭찬 (Nit) - 사소한 오타, 칭찬 등 피드백.

Summary by CodeRabbit

  • 새 기능

    • 이메일 인증 기반 비밀번호 재설정 흐름 추가(재설정 요청 및 확인)
    • 비밀번호 재설정 이메일 전송 전용 경로 추가
  • 버그 수정 / 유효성 검사

    • 새 비밀번호가 이전 비밀번호와 동일한 경우 차단
  • 개선

    • 이메일 인증 처리 및 에러 응답 정비
    • 인증 코드 유효시간 단축(3분)

✏️ Tip: You can customize this high-level summary in your review settings.



# Conflicts:
#	src/main/java/com/whereyouad/WhereYouAd/domains/user/exception/code/UserErrorCode.java
#	src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/entity/User.java
@ojy0903 ojy0903 self-assigned this Jan 30, 2026
@ojy0903 ojy0903 added the ✨ Feature 새로운 기능 추가 label Jan 30, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 30, 2026

Walkthrough

비밀번호 재설정 기능 추가: 이메일 인증 코드 발송(기존 로직 확장) 및 인증 후 비밀번호 변경 엔드포인트/서비스/엔티티/예외 코드·핸들러 추가. 이메일 템플릿 통합 및 Redis 만료 시간 단축(5분→3분). 예외 타입을 UserHandler로 통일.


Changes

Cohort / File(s) Summary
비밀번호 재설정 요청 DTO
src/main/.../user/application/dto/request/PwdResetRequest.java
새 레코드 PwdResetRequest 추가: email, password 필드에 @NotBlank, @Email 등 검증 어노테이션 적용.
이메일 서비스 확장
src/main/.../user/domain/service/EmailService.java
sendEmailForPwd(String) 추가 및 공통 템플릿 emailSendTemplate(...) 도입. Redis TTL 300s→180s 변경. 에러 타입을 UserHandler로 교체.
사용자 서비스 확장
src/main/.../user/domain/service/UserService.java
passwordReset(String email, String password) 추가: Redis 인증 검증, 사용자 조회, 기존 비밀번호 비교, 암호화 및 엔티티 업데이트(Dirty Checking), Redis 키 삭제. 예외는 UserHandler.
엔티티 변경
src/main/.../user/persistence/entity/User.java
resetPassword(String) 메서드 추가(비밀번호 필드 갱신).
컨트롤러 엔드포인트 추가
src/main/.../user/presentation/UserController.java
POST /api/users/password-reset/request 및 POST /api/users/password-reset/confirm 엔드포인트 추가. 각각 이메일 발송 및 비밀번호 변경 처리로 서비스 호출.
API 문서 업데이트
src/main/.../user/presentation/docs/UserControllerDocs.java
신규 엔드포인트 문서화 추가 및 기존 API 응답 에러 코드 번호 재정렬(해당 코드 참조 수정).
예외 처리 및 코드 변경
src/main/.../user/exception/handler/UserHandler.java, src/main/.../user/exception/code/UserErrorCode.java
UserHandler 예외 클래스를 신규 추가. 에러 코드 USER_PASSWORD_SAME_AS_OLD 추가. 기존에 사용하던 UserSignUpException 사용처를 UserHandler로 교체(Provider, CustomOAuth2UserService 등).

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UserController
    participant UserRepository
    participant EmailService
    participant Redis
    participant MailProvider as Mail Provider

    User->>UserController: POST /api/users/password-reset/request (email)
    UserController->>UserRepository: existsByEmail(email)
    alt 사용자 없음
        UserRepository-->>UserController: false
        UserController-->>User: USER_NOT_FOUND (400)
    else 사용자 있음
        UserRepository-->>UserController: true
        UserController->>EmailService: sendEmailForPwd(email)
        EmailService->>EmailService: 인증 코드 생성 (6자리)
        EmailService->>Redis: SET email:code (TTL 180s)
        EmailService->>MailProvider: 발송 요청 (비밀번호 재설정 템플릿)
        MailProvider-->>EmailService: 발송 완료
        EmailService-->>UserController: EmailSentResponse (180초)
        UserController-->>User: 성공 응답
    end
Loading
sequenceDiagram
    actor User
    participant UserController
    participant UserService
    participant EmailService as EmailService (Redis 검증)
    participant Redis
    participant UserRepository
    participant PasswordEncoder

    User->>UserController: POST /api/users/password-reset/confirm (email, password)
    UserController->>UserService: passwordReset(email, password)
    UserService->>EmailService: verifyEmailCode(email)
    EmailService->>Redis: GET email:code
    alt 인증 코드 없음/불일치
        EmailService-->>UserService: USER_EMAIL_AUTH_INVALID
        UserService-->>UserController: 예외 전달
        UserController-->>User: 400 응답
    else 인증 성공
        EmailService-->>UserService: 인증 확인
        UserService->>UserRepository: findByEmail(email)
        alt 사용자 없음
            UserRepository-->>UserService: 빈 응답
            UserService-->>UserController: USER_NOT_FOUND
            UserController-->>User: 400 응답
        else 사용자 있음
            UserRepository-->>UserService: User 엔티티
            UserService->>PasswordEncoder: encode(newPassword)
            PasswordEncoder-->>UserService: encodedPassword
            alt 새 비밀번호 == 기존(해시 비교 필요)
                UserService-->>UserController: USER_PASSWORD_SAME_AS_OLD
                UserController-->>User: 400 응답
            else 변경 허용
                UserService->>User: user.resetPassword(encodedPassword)
                UserService->>Redis: DEL email:code
                UserService-->>UserController: 성공
                UserController-->>User: 성공 응답
            end
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning PR 제목이 이슈 번호만 포함하고 있어 변경사항의 핵심을 파악하기 어렵습니다. 제목을 'Feat/#13: 이메일 기반 비밀번호 재설정 기능 추가' 같이 구체적인 기능 설명을 포함하도록 수정해주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed PR 설명이 템플릿의 모든 필수 섹션을 포함하고 있으며, 변경 내용, 테스트 결과, 체크리스트가 완벽하게 작성되었습니다.
Linked Issues check ✅ Passed 비밀번호 재설정 이메일 인증 기능과 비밀번호 변경 로직 구현으로 이슈 #13의 모든 요구사항을 충족합니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 비밀번호 재설정 기능 구현과 직접적으로 관련되어 있으며, 범위를 벗어난 변경사항이 없습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#13

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java (2)

73-76: ⚠️ Potential issue | 🟡 Minor

비밀번호 재설정인데 제목이 “회원가입 인증번호”로 나가고 있어요.
사용자 입장에서는 “가입” 안내가 뜨면 혼란스럽습니다. 예: 이미 가입한 사용자가 비밀번호 재설정 메일을 받았는데 ‘회원가입’이라고 표시됨. 흐름별 제목을 분리하는 게 좋습니다.

✉️ 개선 예시 (제목 파라미터화)
-    private EmailSentResponse emailSendTemplate(String toEmail) {
+    private EmailSentResponse emailSendTemplate(String toEmail, String subject) {
@@
-        message.setSubject("whereyouad 회원가입 인증번호");
+        message.setSubject(subject);
@@
-        return emailSendTemplate(toEmail);
+        return emailSendTemplate(toEmail, "whereyouad 회원가입 인증번호");
@@
-        return emailSendTemplate(toEmail); //정상적으로 이메일 발송
+        return emailSendTemplate(toEmail, "whereyouad 비밀번호 재설정 인증번호"); //정상적으로 이메일 발송

58-59: ⚠️ Potential issue | 🟠 Major

인증코드 생성에 SecureRandom을 사용해주세요.

현재 Math.random()을 사용하고 있는데, 이는 선형 합동 생성기(LCG) 기반이라 예측 가능합니다. 회원가입과 비밀번호 재설정이라는 보안에 민감한 작업에 사용되는 만큼 암호학적으로 안전한 난수 생성이 필요합니다.

🔐 개선 예시 (SecureRandom 사용)
+import java.security.SecureRandom;
@@
+    private static final SecureRandom SECURE_RANDOM = new SecureRandom();
@@
     private String createCode() {
-        return String.valueOf((int)(Math.random() * (900000)) + 100000);
+        return String.format("%06d", SECURE_RANDOM.nextInt(1_000_000));
     }
🤖 Fix all issues with AI agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java`:
- Around line 40-46: The sendEmailForPwd method leaks account existence by
throwing UserException when userRepository.existsByEmail(toEmail) is false;
change it so it always returns the same EmailSentResponse regardless of
existence: call emailSendTemplate(toEmail) only when exists, but when not exists
do not throw—return the identical success response and optionally log the
non-existent attempt internally. Update sendEmailForPwd to remove the
UserException path, keep userRepository.existsByEmail(toEmail) check to decide
whether to send the email, and ensure the public behavior and response shape
remain identical in both branches.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java`:
- Around line 61-69: The passwordReset method currently relies on the same Redis
key ("VERIFIED:"+email) used for signup verification; separate the concerns by
checking a dedicated password-reset verification key instead (e.g.,
"PASSWORD_RESET:"+email or "VERIFIED:PWD_RESET:"+email). Update
UserService.passwordReset to read redisUtil.getData for the new password-reset
key and throw UserException(UserErrorCode.USER_EMAIL_NOT_VERIFIED) when that key
is missing/invalid; also update EmailService where password-reset emails are
generated (currently setting the signup key around line 109) to set the new
password-reset Redis key with appropriate TTL instead of the signup key. Ensure
all references to "VERIFIED:"+email remain for signup flows only and document
the new key name in the email-reset logic.

Comment on lines +40 to +46
//비밀번호 재설정을 위한 인증코드 이메일 발송 로직 (이미 회원가입 된 상태에서 비밀번호 재설정)
public EmailSentResponse sendEmailForPwd(String toEmail) {
if (userRepository.existsByEmail(toEmail)) { //이미 회원가입 되어있는 것이 확인되면
return emailSendTemplate(toEmail); //정상적으로 이메일 발송
} else { //만약 회원가입 되어있지 않다면
throw new UserException(UserErrorCode.USER_NOT_FOUND); //예외발생
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

계정 존재 여부가 노출될 수 있어요.
현재는 미가입 이메일에 대해 예외를 반환해 계정 열거(Enumeration)가 가능합니다. 예: 공격자가 이메일 리스트를 넣어보고 USER_NOT_FOUND로 가입 여부를 추측할 수 있어요. 존재 여부는 확인하되, 응답은 동일하게 주는 방식이 안전합니다.

🔒 개선 예시 (응답 통일, 미가입은 메일 미발송)
 public EmailSentResponse sendEmailForPwd(String toEmail) {
-    if (userRepository.existsByEmail(toEmail)) { //이미 회원가입 되어있는 것이 확인되면
-        return emailSendTemplate(toEmail); //정상적으로 이메일 발송
-    } else { //만약 회원가입 되어있지 않다면
-        throw new UserException(UserErrorCode.USER_NOT_FOUND); //예외발생
-    }
+    if (!userRepository.existsByEmail(toEmail)) {
+        // 존재 여부는 체크하되, 응답은 동일하게 반환 (계정 열거 방지)
+        return new EmailSentResponse("인증코드를 이메일로 전송했습니다.", toEmail, 300L);
+    }
+    return emailSendTemplate(toEmail);
 }
🤖 Prompt for AI Agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java`
around lines 40 - 46, The sendEmailForPwd method leaks account existence by
throwing UserException when userRepository.existsByEmail(toEmail) is false;
change it so it always returns the same EmailSentResponse regardless of
existence: call emailSendTemplate(toEmail) only when exists, but when not exists
do not throw—return the identical success response and optionally log the
non-existent attempt internally. Update sendEmailForPwd to remove the
UserException path, keep userRepository.existsByEmail(toEmail) check to decide
whether to send the email, and ensure the public behavior and response shape
remain identical in both branches.

Comment on lines +61 to +69
//이미 회원가입 된 회원의 비밀번호 재설정 메서드
public void passwordReset(String email, String password) {
//이메일 인증이 되어있는지 확인
String isEmailVerified = redisUtil.getData("VERIFIED:" + email);

//인증이 안되었다면,
if (isEmailVerified == null || !isEmailVerified.equals("TRUE")) {
throw new UserException(UserErrorCode.USER_EMAIL_NOT_VERIFIED); //예외 발생
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "VERIFIED:" src/main/java/com/whereyouad/WhereYouAd/domains/user -g '*.java' -B 2 -A 2

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 2574


🏁 Script executed:

# UserService.java 파일 전체 읽기 (크기 확인)
wc -l src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 160


🏁 Script executed:

# 비밀번호 재설정 관련 메서드 전체 확인 (61-95 줄 범위)
sed -n '55,95p' src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1255


🏁 Script executed:

# 회원가입/이메일 인증 로직에서 VERIFIED 키 설정하는 부분 찾기
rg -n "VERIFIED:" src/main/java/com/whereyouad/WhereYouAd/domains -g '*.java' -B 3 -A 3

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 3508


비밀번호 재설정용 인증 키를 분리하세요 (보안 문제)

현재 코드의 심각한 보안 문제를 발견했습니다. VERIFIED:{email} 키가 회원가입과 비밀번호 재설정 두 가지 용도로 공유되고 있습니다.

공격 시나리오:

  1. 공격자가 피해자 이메일로 회원가입 시작 → 이메일 인증 완료 → VERIFIED:user@example.com 설정 (1시간 TTL)
  2. 같은 시간 내에 passwordReset(user@example.com, "새_악의적_비밀번호") 호출
  3. 별도 비밀번호 재설정 이메일 인증 없이 비밀번호 변경 성공

수정 방법:
비밀번호 재설정용 접두어를 분리하세요. 예를 들어:

  • 이메일 인증: VERIFIED:{email}
  • 비밀번호 재설정: VERIFIED:PWD_RESET:{email} 또는 PASSWORD_RESET:{email}

EmailService에서 이메일 인증 설정 부분(line 109)은 회원가입 인증용으로만 유지하고, 비밀번호 재설정이 필요할 때는 별도의 Redis 키를 사용해야 합니다.

🔧 변경 예시
// UserService.java - passwordReset 메서드
- String isEmailVerified = redisUtil.getData("VERIFIED:" + email);
+ String isEmailVerified = redisUtil.getData("VERIFIED:PWD_RESET:" + email);
...
- redisUtil.deleteData("VERIFIED:" + email);
+ redisUtil.deleteData("VERIFIED:PWD_RESET:" + email);
🤖 Prompt for AI Agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java`
around lines 61 - 69, The passwordReset method currently relies on the same
Redis key ("VERIFIED:"+email) used for signup verification; separate the
concerns by checking a dedicated password-reset verification key instead (e.g.,
"PASSWORD_RESET:"+email or "VERIFIED:PWD_RESET:"+email). Update
UserService.passwordReset to read redisUtil.getData for the new password-reset
key and throw UserException(UserErrorCode.USER_EMAIL_NOT_VERIFIED) when that key
is missing/invalid; also update EmailService where password-reset emails are
generated (currently setting the signup key around line 109) to set the new
password-reset Redis key with appropriate TTL instead of the signup key. Ensure
all references to "VERIFIED:"+email remain for signup flows only and document
the new key name in the email-reset logic.

//무작위 인증코드 값 생성
private String createCode() {
return String.valueOf((int)(Math.random() * (900000)) + 100000);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: 저도 SMS 인증 서비스(SmsService) 만들 때 이런 인증 코드 생성 로직을 구현해놨는데, 이건 공통 로직이니 global.utils 패키지에 CodeGenerator 같은 유틸 클래스를 하나 만들어서 추후에 공통 메서드 처리 하는 것도 괜찮을 것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 지금 제 PR 이랑 지민님 PR 다하고 나서 하는것도 좋을 것 같아요! 일단 Resolve 안하고 남겨놓을게요

@jinnieusLab
Copy link
Collaborator

P4: 말씀해주신 비밀번호 재사용 로직과 이메일 인증 API 반복해서 사용하는 것 모두 좋은 것 같습니다!

Copy link
Collaborator

@kingmingyu kingmingyu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다! 회원가입 시 이메일 인증을 잘 이용해서 구현하신 것 같습니다! 저도 따로 구분해서 만들지 않아도 괜찮을 것 같습니다!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java (1)

74-81: ⚠️ Potential issue | 🟡 Minor

비밀번호 재설정 메일 제목이 회원가입으로 고정되어 있어요.
현재 제목이 “회원가입 인증번호”로 고정돼 재설정 메일에서도 동일하게 보입니다. type을 제목에도 반영해 주세요.

✉️ 수정 예시
- message.setSubject("whereyouad 회원가입 인증번호");
+ message.setSubject("whereyouad " + type + " 인증번호");

@ojy0903
Copy link
Collaborator Author

ojy0903 commented Jan 31, 2026

  1. UserController 에서 DataResponse.created -> DataResponse.from 으로 수정
  2. 인증 시간은 3분으로 통일, 회원가입과 비밀번호 재설정 이메일 문구 분리
  3. UserException -> exception/handler/UserHandler 로 통합

이렇게 수정 진행했습니다! 머지 진행하겠습니다

@ojy0903 ojy0903 merged commit 05faf45 into develop Jan 31, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 비밀번호 찾기 - 이메일 기반 회원 비밀번호 재설정

3 participants