Skip to content

Feat/#12 이메일 찾기 SMS 인증 구현#16

Merged
jinnieusLab merged 11 commits intodevelopfrom
feat/#12
Jan 31, 2026
Merged

Feat/#12 이메일 찾기 SMS 인증 구현#16
jinnieusLab merged 11 commits intodevelopfrom
feat/#12

Conversation

@jinnieusLab
Copy link
Collaborator

@jinnieusLab jinnieusLab commented Jan 30, 2026

📌 관련 이슈

🚀 개요

이메일 찾기 기능을 위한 SMS 인증을 구현하였습니다.

📄 작업 내용

  • CoolSMS SDK 적용
  • SMS 인증 로직 구현 (SmsService): Redis를 활용한 인증번호 저장(3분 만료) 및 검증 구현

1. sms 전송 `POST /api/sms/sms-send`
{
  "phoneNumber": "01012345678"
}
  • sendSms: 미가입자(유저가 아닐 시)는 전송 차단 (USER_404_2), 유저일 시 전송
  • generateCode: 6자리 랜덤 인증 번호 생성
  • Redis(일회성 정보이기에 db에 저장 x)에 {phoneNum: verificationCode} 저장
  • 관련 환경 변수 업데이트 (application.yml, .env.example, 노션 .env 페이지)
  1. 이후 인증 번호 입력(확인) POST /api/users/sms-verify
{
  "phoneNumber": "01012345678",
  "verificationCode": "123456"
}
  • verifyCode: 요청 내 인증 번호와 Redis에 저장된 해당 번호(key)의 인증번호(value)가 일치하는지 여부 확인
  • isPhoneVerified: 검증 성공 시 해당 유저의 이메일 반환, 인증 성공 시 Redis 데이터 삭제(delete)로 재사용 방지
  • 응답: email 필드를 포함-> 프론트 이메일 찾기 결과로 활용 가능

  • 관련 환경 변수 업데이트 (application.yml, .env.example, 노션 .env 페이지)

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

image

✅ 체크리스트

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

🔍 리뷰 포인트 (Review Points) + 고려할 사항

  • 이전 PR 리뷰 후 추후 충돌 사항 수정 필요
  • 현재 서비스가 인터페이스 없이 작성되어 있는데 추후 기능이 무거워지는 걸 (ex. 유저 로직이 조직 관리 등에서 쓰인다면...) 대비해 미리 분리해야 할지?
  • 프론트와의 연결을 위해 각자 구현한 api 명세 필요할듯..
  • 문자 한 건 당 18원, 현재 12개까지는 무료로 가능

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

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 휴대폰 번호 기반 SMS 인증 기능을 추가했습니다. 사용자는 휴대폰 번호로 인증 코드를 받고 검증할 수 있습니다.
    • 새로운 SMS 발송 및 검증 API 엔드포인트를 제공합니다.
  • Chores

    • SMS 서비스 지원을 위한 의존성을 추가했습니다.
    • 필수 SMS 설정 값을 환경 변수로 추가했습니다.

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

@jinnieusLab jinnieusLab self-assigned this Jan 30, 2026
@jinnieusLab jinnieusLab added the ✨ Feature 새로운 기능 추가 label Jan 30, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 30, 2026

Walkthrough

사용자 SMS 인증을 통한 이메일 찾기 기능을 구현합니다. CoolSMS API를 통해 휴대폰 번호로 6자리 인증 코드를 전송하고 Redis에 일시 저장하며, 코드 검증 후 사용자 이메일을 반환하는 플로우를 추가합니다.

Changes

Cohort / File(s) Summary
환경 및 의존성 설정
.env.example, build.gradle, src/main/resources/application.yml, .github/workflows/ci.yml
SMS API 신임정보(API_KEY, SECRET_KEY, SENDER_NUMBER)를 환경 변수로 추가. CoolSMS 라이브러리(net.nurigo:sdk:4.3.0)와 spring-dotenv 의존성 추가. GitHub Actions CI에 환경 변수 전달.
DTO 계층
src/main/java/com/whereyouad/WhereYouAd/domains/user/application/dto/request/SmsRequest.java, src/main/java/com/whereyouad/WhereYouAd/domains/user/application/dto/response/SmsResponse.java
SMS 전송 요청(phoneNumber) 및 검증 요청(phoneNumber, verificationCode) DTO 추가. 응답 DTO로 SmsSentResponse(message, phoneNumber, expireIn)와 SmsVerifiedResponse(isVerified, verificationMessage, email) 정의. 전화번호 형식 검증(@Pattern: 10-11자리) 구현.
비즈니스 로직 계층
src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/SmsService.java
SMS 발송(전화번호 검증 → 6자리 코드 생성 → CoolSMS 호출 → Redis 저장 180초), 코드 검증(Redis 비교 및 삭제), 전화번호 검증 완료 시 이메일 반환 로직 구현. Nurigo DefaultMessageService 초기화 및 RedisUtil, UserRepository 의존성 주입.
Repository 및 Exception
src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/repository/UserRepository.java, src/main/java/com/whereyouad/WhereYouAd/domains/user/exception/code/UserErrorCode.java
전화번호 기반 사용자 조회(findUserByPhoneNumber, existsByPhoneNumber) 메서드 추가. 새로운 에러 코드 3개 추가(USER_SMS_NOT_VERIFIED: 401, USER_NOT_FOUND_BY_PHONE: 404, SMS_SEND_FAILED: 500).
Controller 및 문서화
src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/UserController.java, src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/docs/UserControllerDocs.java
POST /sms-send 및 POST /sms-verify 엔드포인트 추가. SmsService 의존성 주입. API 문서화를 UserControllerDocs에 반영.

Sequence Diagram

sequenceDiagram
    participant Client
    participant UserController
    participant SmsService
    participant UserRepository
    participant Redis as Redis<br/>(Code Storage)
    participant CoolSMS as CoolSMS API
    
    Note over Client,CoolSMS: SMS 인증 흐름
    
    Client->>UserController: POST /sms-send<br/>{phoneNumber}
    UserController->>SmsService: sendSms(phoneNumber)
    SmsService->>UserRepository: findUserByPhoneNumber(phoneNumber)
    alt 사용자 없음
        SmsService-->>UserController: USER_NOT_FOUND_BY_PHONE 예외
        UserController-->>Client: 404 Error
    else 사용자 존재
        SmsService->>SmsService: generateCode() → 6자리
        SmsService->>CoolSMS: 인증코드 SMS 발송<br/>(API credentials 사용)
        alt SMS 발송 실패
            SmsService-->>UserController: SMS_SEND_FAILED 예외
            UserController-->>Client: 500 Error
        else SMS 발송 성공
            SmsService->>Redis: 코드 저장<br/>(TTL: 180초)
            SmsService-->>UserController: SmsSentResponse<br/>(message, phoneNumber, expireIn)
            UserController-->>Client: 200 OK
        end
    end
    
    Note over Client,CoolSMS: 코드 검증 흐름
    
    Client->>UserController: POST /sms-verify<br/>{phoneNumber, verificationCode}
    UserController->>SmsService: isPhoneVerified(phoneNumber, verificationCode)
    SmsService->>Redis: 저장된 코드 조회
    alt 코드 불일치 또는 만료
        SmsService-->>UserController: USER_SMS_NOT_VERIFIED 예외
        UserController-->>Client: 401 Error
    else 코드 일치
        SmsService->>Redis: 코드 삭제
        SmsService->>UserRepository: findUserByPhoneNumber(phoneNumber)
        SmsService-->>UserController: 사용자 이메일 반환
        UserController-->>Client: 200 OK<br/>SmsVerifiedResponse<br/>(isVerified: true, email)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45분

복잡도 요인:

  • 외부 API(CoolSMS) 통합으로 인한 오류 처리 검증 필요
  • Redis 저장/조회 로직의 동시성 고려 확인
  • 전화번호 검증 로직과 사용자 존재 여부 확인의 정합성
  • 다양한 파일(DTO, Service, Controller, Repository, ErrorCode 등)에 걸친 변경으로 일관성 확인 필요

주의 깊게 살펴볼 부분:

  1. Redis TTL 설정 - SmsService에서 180초 만료 설정이 정상인지, 동시성 이슈는 없는지 확인 필요
  2. 전화번호 유효성 - @Pattern 정규식이 모든 유효한 한국 휴대폰 번호를 포함하는지 검증 필요
    예: 010-1234-5678 형식은 하이픈이 없어야 한다는 조건 재확인
  3. 에러 처리 누락 여부 - CoolSMS 호출 실패 시 Stack trace 노출 방지 확인
  4. Repository 쿼리 - findUserByPhoneNumber의 쿼리가 인덱스를 활용하도록 설계되었는지 확인 (향후 성능 문제 예방)

칭찬할 점:

명확한 DTO 설계 - SmsSendRequest와SmsVerifyRequest를 구분하여 각 API의 책임을 명확히 함
검증 어노테이션 활용 - @NotBlank, @Pattern으로 입력값 검증을 선언적으로 처리
에러 코드 체계화 - 세 가지 SMS 관련 에러를 상태 코드(401, 404, 500)로 구분하여 관리
의존성 주입 활용 - SmsService가 RedisUtil, UserRepository, API 신임정보를 주입받아 테스트 용이성 확보

Possibly related PRs

  • Feat/#10 #11 - UserController와 UserErrorCode를 함께 수정하며, 사용자 검증 흐름(이메일 검증 vs SMS 검증)의 일관성을 검토할 필요가 있음.

Suggested reviewers

  • ojy0903
  • kingmingyu
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 SMS 인증 기능 구현이라는 핵심 변화를 명확하게 반영하고 있으며, 이슈 번호 #12와 함께 구체적인 기능을 설명합니다.
Description check ✅ Passed PR 설명이 필수 섹션을 모두 포함하고 있으며, 구체적인 작업 내용, API 엔드포인트, 환경 변수 업데이트 및 체크리스트를 상세히 설명하고 있습니다.
Linked Issues check ✅ Passed PR의 모든 구현 사항이 이슈 #12의 요구사항을 충족합니다: SMS 인증 로직 구현(SmsService), coolSMS 통합, 이메일 찾기 API 구현이 완료되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 SMS 인증 기능 구현이라는 이슈 #12의 범위 내에 있으며, 관련 없는 기능이나 불필요한 리팩토링은 포함되지 않았습니다.

✏️ 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/#12

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: 6

🤖 Fix all issues with AI agents
In @.env.example:
- Around line 31-36: The SMS env entries contain extra spaces around the equals
signs which can cause parsers to read variable names like "SMS_API_KEY " instead
of "SMS_API_KEY"; remove spaces so each line uses the exact key=value form for
SMS_API_KEY, SMS_SECRET_KEY, and SMS_SENDER_NUMBER and ensure the file ends with
a single newline character; verify keys are exactly "SMS_API_KEY",
"SMS_SECRET_KEY", and "SMS_SENDER_NUMBER" with no trailing spaces.

In `@build.gradle`:
- Around line 62-67: Replace the incompatible dotenv and update the SMS SDK:
change the dependency "me.paulschwarz:spring-dotenv:4.0.0" to the Spring Boot 3
compatible artifact "me.paulschwarz:springboot3-dotenv:5.1.0" and bump
"net.nurigo:sdk:4.3.0" to "net.nurigo:sdk:4.3.2" in the build.gradle
dependencies so the project uses the recommended springboot3-dotenv for Spring
Boot 3.5 and the latest Nurigo SDK.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/application/dto/request/SmsRequest.java`:
- Around line 7-15: SmsVerifyRequest currently lacks the same format constraints
as SmsSendRequest, allowing phone strings like "010-1234-5678" and
variable-length codes which breaks Redis key matching; update the
SmsVerifyRequest record to apply the same `@Pattern`(regexp = "^\\d{10,11}$",
message = "...") annotation to phoneNumber as used in SmsSendRequest and add a
`@Pattern`(regexp = "^\\d{6}$", message = "인증코드는 6자리 숫자만 입력해주세요.") (and keep
`@NotBlank`) on verificationCode so both request DTOs use identical phone
validation and enforce a 6-digit code.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/SmsService.java`:
- Around line 38-58: Before sending SMS in SmsService.sendSms, check the phone
is registered and enforce rate limiting: call
UserRepository.existsByPhoneNumber(phoneNumber) and throw
UserHandler(UserErrorCode.PHONE_NOT_FOUND) if false; then enforce a short
TTL-based rate limit using redisUtil (e.g., set/get a "sms:sent:{phoneNumber}"
key or use redisUtil.getData to check an existing flag) and if a recent send
exists throw UserHandler(UserErrorCode.TOO_MANY_REQUESTS); only after those
checks proceed to generateCode(), redisUtil.setDataExpire(phoneNumber,
verificationCode, 180) and call defaultMessageService.sendOne(new
SingleMessageSendingRequest(message)). Ensure error paths use the same
UserErrorCode semantics and do not call sendOne if checks fail.
- Around line 47-55: The Redis-stored verification code is set before sending
the SMS (redisUtil.setDataExpire), leaving a valid code if send fails; change
the flow in SmsService so you call defaultMessageService.sendOne(new
SingleMessageSendingRequest(message)) first and only on success persist the code
with redisUtil.setDataExpire(phoneNumber, verificationCode, 180); alternatively,
if you must keep the current order, catch exceptions from
defaultMessageService.sendOne and remove the stored key (use redisUtil.delete or
equivalent) inside the catch before throwing
UserHandler(UserErrorCode.SMS_SEND_FAILED) to ensure no orphaned codes remain.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/repository/UserRepository.java`:
- Around line 14-15: 쿼리의 네임드 파라미터 불일치로 바인딩 오류가 발생합니다; UserRepository의 메서드
findUserByPhoneNumber에서 `@Query가` :phoneNum을 사용하고 있으니 `@Param`("phoneNum")으로 이름을
맞추거나 쿼리 파라미터를 :phoneNumber로 변경해 두 식별자(쿼리의 :phoneNum과 `@Param의` 값)가 일치하도록 수정하세요.
🧹 Nitpick comments (5)
src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/SmsService.java (3)

60-65: 보안 강화: Random 대신 SecureRandom 사용을 권장합니다.

java.util.Random은 예측 가능한 난수를 생성하므로, 인증 코드와 같이 보안이 중요한 경우에는 SecureRandom을 사용하는 것이 좋습니다. 공격자가 시드 값을 추측하면 다음 인증 코드를 예측할 수 있는 이론적 위험이 있습니다.

🔒 SecureRandom 적용 예시
+import java.security.SecureRandom;
+
 // 랜덤 인증 번호 생성
 public String generateCode() {
-    Random random = new Random();
+    SecureRandom random = new SecureRandom();
     int code = 100000 + random.nextInt(900000); // 6자리 인증 번호
     return Integer.toString(code);
 }

19-21: 불필요한 @Transactional 어노테이션입니다.

이 서비스는 JPA 엔티티 수정 없이 Redis 작업과 외부 API 호출만 수행합니다. 클래스 레벨의 @Transactional은 불필요한 트랜잭션 오버헤드를 발생시킵니다.

만약 isPhoneVerified에서 userRepository.findUserByPhoneNumber가 읽기 전용이라면, 해당 메서드에만 @Transactional(readOnly = true)를 적용하는 것이 더 적절합니다.

♻️ 수정 제안
 `@Service`
-@Transactional
 public class SmsService {
     // ...

+    `@Transactional`(readOnly = true)
     public String isPhoneVerified(String phoneNumber, String insertedNum) {
         // ...
     }
 }

79-87: 메서드 이름이 반환 타입과 맞지 않습니다.

isPhoneVerified라는 이름은 boolean을 반환할 것 같지만 실제로는 String(email)을 반환합니다. 코드 가독성을 위해 메서드 이름을 변경하는 것을 권장합니다.

💡 네이밍 개선 제안
-    public String isPhoneVerified(String phoneNumber, String insertedNum) {
+    public String findEmailByVerifiedPhone(String phoneNumber, String insertedNum) {
         if (verifyCode(phoneNumber, insertedNum)) {
             User user = userRepository.findUserByPhoneNumber(phoneNumber)
                     .orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND));
             return user.getEmail();
         } else
             throw new UserHandler(UserErrorCode.USER_SMS_NOT_VERIFIED);
     }
src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/UserController.java (1)

53-65: SMS 관련 엔드포인트 구현이 깔끔합니다!

@Valid를 통한 입력 검증과 서비스 위임 패턴이 잘 적용되어 있습니다. SmsRequest에 정의된 @Pattern 검증으로 전화번호 형식도 잘 처리됩니다.

한 가지 작은 개선점으로, Line 64에서 SmsVerifiedResponse 생성을 서비스 레이어로 이동하면 다른 엔드포인트들과 일관성이 높아집니다. 현재 sendSms는 서비스에서 응답 객체를 생성하지만, verifySms는 컨트롤러에서 생성하고 있습니다.

💡 일관성 개선 제안

SmsService.java에서 응답 생성:

public SmsResponse.SmsVerifiedResponse verifyAndFindEmail(String phoneNumber, String insertedNum) {
    if (verifyCode(phoneNumber, insertedNum)) {
        User user = userRepository.findUserByPhoneNumber(phoneNumber)
                .orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND));
        return new SmsResponse.SmsVerifiedResponse(true, "이메일 찾기 성공", user.getEmail());
    }
    throw new UserHandler(UserErrorCode.USER_SMS_NOT_VERIFIED);
}

UserController.java:

 `@PostMapping`("/sms-verify")
 public ResponseEntity<DataResponse<SmsResponse.SmsVerifiedResponse>> verifySms(
         `@RequestBody` `@Valid` SmsRequest.SmsVerifyRequest request) {
-    String email = smsService.isPhoneVerified(request.phoneNumber(), request.verificationCode());
-    return ResponseEntity.ok(DataResponse.from(new SmsResponse.SmsVerifiedResponse(true, "이메일 찾기 성공", email)));
+    SmsResponse.SmsVerifiedResponse response = smsService.verifyAndFindEmail(request.phoneNumber(), request.verificationCode());
+    return ResponseEntity.ok(DataResponse.from(response));
 }
src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/docs/UserControllerDocs.java (1)

40-54: API 문서에 구체적인 에러 코드를 추가하면 좋겠습니다.

이메일 관련 API들은 "400_2", "400_3", "400_4" 등 구체적인 에러 코드와 설명을 제공하지만, SMS 관련 API들은 단순히 "400"만 명시되어 있습니다. 클라이언트 개발자가 에러 상황을 구분하기 어려울 수 있습니다.

📝 Swagger 문서 개선 제안
 `@Operation`(summary = "이메일 찾기 - SMS 인증 번호 전송 API", description = "입력받은 전화번호로 인증 번호를 전송합니다.")
 `@ApiResponses`({
         `@ApiResponse`(responseCode = "200", description = "성공"),
-        `@ApiResponse`(responseCode = "400", description = "실패")
+        `@ApiResponse`(responseCode = "404", description = "등록되지 않은 전화번호"),
+        `@ApiResponse`(responseCode = "500", description = "SMS 전송 실패")
 })
 public ResponseEntity<DataResponse<SmsResponse.SmsSentResponse>> sendSms(...);

 `@Operation`(summary = "이메일 찾기 - SMS 인증 번호 확인 API", description = "전화번호와 인증 코드를 받아 맞는지 검증합니다.")
 `@ApiResponses`({
         `@ApiResponse`(responseCode = "200", description = "성공"),
-        `@ApiResponse`(responseCode = "400", description = "실패")
+        `@ApiResponse`(responseCode = "401", description = "인증 코드 불일치"),
+        `@ApiResponse`(responseCode = "404", description = "등록되지 않은 전화번호")
 })
 public ResponseEntity<DataResponse<SmsResponse.SmsVerifiedResponse>> verifySms(...);

Comment on lines +7 to +15
public record SmsSendRequest(
@NotBlank(message = "전화번호는 필수입니다.")
@Pattern(regexp = "^\\d{10,11}$", message = "전화번호는 하이픈 없이 10~11자리 숫자만 입력해주세요.") String phoneNumber) {
}

public record SmsVerifyRequest(
@NotBlank(message = "전화번호는 필수입니다.") String phoneNumber,
@NotBlank(message = "인증코드는 필수입니다.") String verificationCode) {
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

검증 요청에도 전화번호/인증코드 형식 검증을 맞춰주세요

발송 요청은 10~11자리만 허용하지만 검증 요청은 제약이 없어 '010-1234-5678' 같은 값이 통과할 수 있습니다. 이렇게 되면 Redis 키가 달라져 항상 실패할 수 있어요. 검증 요청에도 동일한 패턴과 6자리 코드 제약을 넣는 걸 권장합니다.

✅ 수정 제안
     public record SmsVerifyRequest(
-                    `@NotBlank`(message = "전화번호는 필수입니다.") String phoneNumber,
-                    `@NotBlank`(message = "인증코드는 필수입니다.") String verificationCode) {
+                    `@NotBlank`(message = "전화번호는 필수입니다.")
+                    `@Pattern`(regexp = "^\\d{10,11}$", message = "전화번호는 하이픈 없이 10~11자리 숫자만 입력해주세요.") String phoneNumber,
+                    `@NotBlank`(message = "인증코드는 필수입니다.")
+                    `@Pattern`(regexp = "^\\d{6}$", message = "인증코드는 6자리 숫자만 입력해주세요.") String verificationCode) {
     }
🤖 Prompt for AI Agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/application/dto/request/SmsRequest.java`
around lines 7 - 15, SmsVerifyRequest currently lacks the same format
constraints as SmsSendRequest, allowing phone strings like "010-1234-5678" and
variable-length codes which breaks Redis key matching; update the
SmsVerifyRequest record to apply the same `@Pattern`(regexp = "^\\d{10,11}$",
message = "...") annotation to phoneNumber as used in SmsSendRequest and add a
`@Pattern`(regexp = "^\\d{6}$", message = "인증코드는 6자리 숫자만 입력해주세요.") (and keep
`@NotBlank`) on verificationCode so both request DTOs use identical phone
validation and enforce a 6-digit code.

Comment on lines +47 to +55
// redis 저장
redisUtil.setDataExpire(phoneNumber, verificationCode, 180); // 유효 기간 3분

try {
this.defaultMessageService.sendOne(new SingleMessageSendingRequest(message));
} catch (Exception e) {
e.printStackTrace();
throw new UserHandler(UserErrorCode.SMS_SEND_FAILED);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

SMS 전송 실패 시 Redis 데이터가 남아있는 문제가 있습니다.

현재 코드는 Redis에 인증 코드를 저장한 후 SMS를 전송합니다. 만약 SMS 전송이 실패하면 Redis에 저장된 데이터는 그대로 남아있게 되어, 사용자가 받지 못한 인증 코드가 3분간 유효한 상태로 남습니다.

예를 들어:

  1. 사용자 A가 인증 요청 → Redis에 코드 저장 → SMS 전송 실패
  2. 공격자가 해당 번호로 무작위 코드 대입 시도 가능
🛡️ 수정 제안: SMS 전송 성공 후 Redis 저장
     public SmsResponse.SmsSentResponse sendSms(String phoneNumber) {
         Message message = new Message();
         String verificationCode = generateCode();

         message.setFrom(senderNumber); // 발신 번호
         message.setTo(phoneNumber);
         message.setText("[Where You Ad] 이메일 찾기\n 휴대폰 인증 번호는 [" + verificationCode + "] 입니다.");

-        // redis 저장
-        redisUtil.setDataExpire(phoneNumber, verificationCode, 180); // 유효 기간 3분
-
         try {
             this.defaultMessageService.sendOne(new SingleMessageSendingRequest(message));
+            // SMS 전송 성공 후 redis 저장
+            redisUtil.setDataExpire(phoneNumber, verificationCode, 180); // 유효 기간 3분
         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("SMS 전송 실패 - phoneNumber: {}", phoneNumber, e);
             throw new UserHandler(UserErrorCode.SMS_SEND_FAILED);
         }

         return new SmsResponse.SmsSentResponse("인증번호가 전송되었습니다.", phoneNumber, 180L);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// redis 저장
redisUtil.setDataExpire(phoneNumber, verificationCode, 180); // 유효 기간 3분
try {
this.defaultMessageService.sendOne(new SingleMessageSendingRequest(message));
} catch (Exception e) {
e.printStackTrace();
throw new UserHandler(UserErrorCode.SMS_SEND_FAILED);
}
try {
this.defaultMessageService.sendOne(new SingleMessageSendingRequest(message));
// SMS 전송 성공 후 redis 저장
redisUtil.setDataExpire(phoneNumber, verificationCode, 180); // 유효 기간 3분
} catch (Exception e) {
log.error("SMS 전송 실패 - phoneNumber: {}", phoneNumber, e);
throw new UserHandler(UserErrorCode.SMS_SEND_FAILED);
}
🤖 Prompt for AI Agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/SmsService.java`
around lines 47 - 55, The Redis-stored verification code is set before sending
the SMS (redisUtil.setDataExpire), leaving a valid code if send fails; change
the flow in SmsService so you call defaultMessageService.sendOne(new
SingleMessageSendingRequest(message)) first and only on success persist the code
with redisUtil.setDataExpire(phoneNumber, verificationCode, 180); alternatively,
if you must keep the current order, catch exceptions from
defaultMessageService.sendOne and remove the stored key (use redisUtil.delete or
equivalent) inside the catch before throwing
UserHandler(UserErrorCode.SMS_SEND_FAILED) to ensure no orphaned codes remain.

@WhereYouAd WhereYouAd deleted a comment from coderabbitai bot Jan 30, 2026
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: 1

🤖 Fix all issues with AI agents
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/SmsService.java`:
- Around line 84-92: The isPhoneVerified method throws UserHandler with
UserErrorCode.USER_NOT_FOUND when userRepository.findUserByPhoneNumber fails;
change that to throw new UserHandler(UserErrorCode.USER_NOT_FOUND_BY_PHONE) so
the error matches phone-based lookup. Locate isPhoneVerified (which calls
verifyCode and userRepository.findUserByPhoneNumber) and replace the
UserErrorCode.USER_NOT_FOUND constant with UserErrorCode.USER_NOT_FOUND_BY_PHONE
in the exception construction.
🧹 Nitpick comments (2)
src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/SmsService.java (2)

57-59: e.printStackTrace() 대신 로거를 사용해주세요.

e.printStackTrace()는 표준 에러 스트림으로 출력되어 로그 관리가 어렵습니다. SLF4J 로거를 사용하면 로그 레벨 조정, 파일 저장, 로그 모니터링 등이 가능해집니다.

♻️ 수정 제안

클래스 상단에 로거 선언 추가:

import lombok.extern.slf4j.Slf4j;

`@Slf4j`
`@Service`
`@Transactional`
public class SmsService {

catch 블록 수정:

         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("SMS 전송 실패 - phoneNumber: {}", phoneNumber, e);
             throw new UserHandler(UserErrorCode.SMS_SEND_FAILED);
         }

65-70: 보안 강화를 위해 SecureRandom 사용을 권장드립니다.

Random은 예측 가능한 의사 난수를 생성합니다. 인증 코드는 보안에 민감하므로 SecureRandom을 사용하면 더 안전합니다.

참고로 EmailService.createCode()도 동일한 패턴을 사용 중인데, 추후 공통 유틸로 추출하는 것도 좋을 것 같습니다.

♻️ 수정 제안
+import java.security.SecureRandom;
-import java.util.Random;

     // 랜덤 인증 번호 생성
     public String generateCode() {
-        Random random = new Random();
+        SecureRandom random = new SecureRandom();
         int code = 100000 + random.nextInt(900000); // 6자리 인증 번호
         return Integer.toString(code);
     }

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.

P4: 고생하셨습니다! 이메일 찾기 기능 잘 구현하신 것 같습니다! 코드 보면서 생각난건데 혹시 회원가입 시에 저희가 전화번호 인증은 따로 안받는데 회원가입 시에 전화번호를 잘못 입력한 경우에 대해서는 당장 고칠 부분은 아니지만 생각 좀 해봐야 할 것 같습니다!

@ApiResponse(responseCode = "400_2", description = "이메일 중복 회원 존재")
})
public ResponseEntity<DataResponse<SignUpResponse>> signUp(@RequestBody @Valid SignUpRequest request);
@Operation(summary = "단순 회원가입 API", description = "이메일, 비밀번호, 이름, 전화번호를 받아 회원가입을 진행합니다(먼저 이메일 인증이 진행되어야 회원가입 가능)")
Copy link
Collaborator

Choose a reason for hiding this comment

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

P4: 저 혹시 들여쓰기 8칸으로 수정하신 이유가 있을까요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

엇 딱히 이유는 없습니다!! 수정하다보니 들여쓰기가 고쳐진 것 같습니다(?)...

try {
this.defaultMessageService.sendOne(new SingleMessageSendingRequest(message));
} catch (Exception e) {
e.printStackTrace();
Copy link
Collaborator

Choose a reason for hiding this comment

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

P4: log.error("SMS 발송 실패: {}", e.getMessage()) 이런식으로 에러 로그 찍는건 어떤가요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

넵! Slf4j 쓰는 게 낫겠네요

Copy link
Collaborator

@ojy0903 ojy0903 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다! 제 비밀번호 찾기 PR 에서 지민님 하신것처럼 UserException -> UserHandler 로 통일해서 수정했는데, 제 PR 머지하고 나면 충돌이 생길수도 있을거 같아요. 코드 구조는 동일해서 만약 conflict 난다면 지민님이 하신 코드로 반영하면 될거 같아요. 전체적으로 로직은 깔끔해서 이대로 진행해도 될듯합니다!!

@ojy0903
Copy link
Collaborator

ojy0903 commented Jan 31, 2026

conflict 는 import 문이랑 제가 작성한 Docs 관련 차이인거 같아서 제 비밀번호 찾기 PR 내용 추가만 해주시면 될거 같아요!
그리고 API 명세는 저희 노션 API 명세 페이지에 제가 노션 템플릿 예시로 찾아서 넣어놔 봤는데 확인해 보면 좋을 거 같아요. 다른 템플릿 있으면 다른거로 해도 됩니다!

@jinnieusLab jinnieusLab merged commit b15a7c7 into develop Jan 31, 2026
2 checks passed
@jinnieusLab jinnieusLab deleted the feat/#12 branch January 31, 2026 14:58
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