Skip to content

Conversation

@minjee2758
Copy link
Collaborator

@minjee2758 minjee2758 commented Sep 5, 2025

PR 생성 시 아래 항목을 채워주세요.

제목 예시: feat : Pull request template 작성

(작성 후 이 안내 문구는 삭제해주세요)


작업 내용

  • 어떤 기능(또는 수정 사항)을 구현했는지 간략하게 설명해주세요.
  • 예) "회원가입 API에 이메일 중복 검사 기능 추가"

변경 사항

  • 구현한 주요 로직, 클래스, 메서드 등을 bullet 형식으로 기술해주세요.
  • 예)
    • UserService.createUser() 메서드 추가
    • @Email 유효성 검증 적용

트러블 슈팅

  • 구현 중 마주한 문제와 해결 방법을 기술해주세요.
  • 예)
    • 문제: @Transactional이 적용되지 않음
    • 해결: 메서드 호출 방식 변경 (this.AopProxyUtils. 사용)

해결해야 할 문제

  • 기능은 동작하지만 리팩토링이나 논의가 필요한 부분을 적어주세요.
  • 예)D
    • UserController에서 비즈니스 로직 일부 처리 → 서비스로 이전 고려 필요

참고 사항

  • 기타 공유하고 싶은 정보나 참고한 문서(링크 등)가 있다면 작성해주세요.

코드 리뷰 전 확인 체크리스트

  • 불필요한 콘솔 로그, 주석 제거
  • 커밋 메시지 컨벤션 준수 (type : )
  • 기능 정상 동작 확인

Summary by CodeRabbit

  • New Features
    • 닉네임 중복 검사 및 오류 응답 추가
    • 사용자 언어가 비어있을 경우 기본 언어 자동 설정
    • 프로필 수정 요청에 multipart/form-data 지원
    • 회원 일일 풀이 기록에 날짜별 문제 ID 목록과 고유 풀이 수 제공
    • 이메일 인증 성공/실패 시 프론트엔드 리다이렉트 URL 설정 추가
  • Documentation
    • 회원의 일일 풀이 기록 API에 대한 OpenAPI 설명 보강
  • Chores
    • 애플리케이션 프로퍼티 정리 및 이메일 인증 리다이렉트 관련 설정 추가

@coderabbitai
Copy link

coderabbitai bot commented Sep 5, 2025

Walkthrough

사용자 정보 조회/수정 로직이 변경되어 기본 언어 초기화와 닉네임 중복 검사 추가. 일별 정답 통계 DTO가 날짜/문제수/문제ID집합으로 확장되고, 해당 조회 쿼리가 GroupBy 기반 변환으로 수정. 닉네임 목록 조회용 리포지토리 메서드 추가. 컨트롤러에서 multipart/form-data 수신 및 문서화 주석 보강. 검증 리다이렉트 속성 추가.

Changes

Cohort / File(s) Summary
User 서비스/도메인 변경
src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java, src/main/java/org/ezcode/codetest/domain/user/service/UserDomainService.java, src/main/java/org/ezcode/codetest/domain/user/exception/code/UserExceptionCode.java, src/main/java/org/ezcode/codetest/domain/user/model/entity/User.java
getUserInfo 트랜잭션을 쓰기 가능으로 변경하고 사용자 언어가 null일 때 ID 1L 언어로 초기화. modifyUserInfo에 닉네임 중복 검사 추가 및 중복시 예외 상수(ALREADY_EXIST_NICKNAME) 사용. User에 setLanguage 추가. UserDomainService에 getUserNicknames, existsByNickname 추가.
닉네임 조회 리포지토리 계층
src/main/java/org/ezcode/codetest/domain/user/repository/UserRepository.java, .../infrastructure/persistence/repository/user/UserJpaRepository.java, .../infrastructure/persistence/repository/user/UserRepositoryImpl.java
닉네임 목록 조회 메서드 도입: 인터페이스(getUserNicknames), JPA 쿼리(findAllNicknames), 구현에서 위임.
일별 정답 집계 확장
src/main/java/org/ezcode/codetest/domain/submission/dto/DailyCorrectCount.java, .../infrastructure/persistence/repository/submission/query/UserProblemResultQueryRepositoryImpl.java
DailyCorrectCount를 (date, long count, Set problemIds)로 변경하고 기존 java.sql.Date 매핑 생성자 제거. 쿼리를 GroupBy.transform으로 변경하여 날짜별 distinct 문제 수와 문제ID Set을 투영.
컨트롤러 주석/콘텐츠 타입
src/main/java/org/ezcode/codetest/presentation/usermanagement/UserController.java
사용자 정보 수정 엔드포인트를 multipart/form-data 소비로 변경. 일별 풀이 현황 엔드포인트에 OpenAPI @operation 주석 추가.
설정 파일
src/main/resources/application.properties
이메일 검증 성공/실패 리다이렉트 URL 속성 추가(주석 포함). hikari.validation-timeout 라인 재배치.
사소한 임포트 정리
src/main/java/org/ezcode/codetest/domain/submission/service/SubmissionDomainService.java, src/main/java/org/ezcode/codetest/presentation/usermanagement/UserVerifyController.java
불필요/추가 임포트 정리(동작 변화 없음).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as 사용자
  participant C as UserController
  participant S as UserService
  participant D as UserDomainService
  participant R as UserRepository

  rect rgb(245,248,255)
  note over U,C: 프로필 수정 요청 (multipart/form-data)
  U->>C: PUT /users (form-data)
  C->>S: modifyUserInfo(request)
  alt 닉네임 변경 요청
    S->>D: existsByNickname(nickname)
    D->>R: existsByNickname(nickname)
    R-->>D: boolean
    D-->>S: boolean
    opt 중복일 때
      S-->>C: throw ALREADY_EXIST_NICKNAME
    end
  end
  S-->>C: 수정 결과 반환
  end
Loading
sequenceDiagram
  autonumber
  actor U as 사용자
  participant C as UserController
  participant Q as UserProblemResultQueryRepositoryImpl
  participant DB as DB

  U->>C: GET /users/{id}/daily-solved
  C->>Q: findDailyCorrectCount(userId)
  Q->>DB: SELECT DATE(modified_at), COUNT(DISTINCT problem_id), SET(problem_id)... WHERE is_correct=true AND user_id=?
  DB-->>Q: rows (date, problem_id)
  Q-->>C: List<DailyCorrectCount(date, count, Set<problemIds>)>
  C-->>U: 응답 (날짜/문제ID목록/개수)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • Kimminu7
  • NCookies
  • thezz9
  • chat26666

Poem

깡총-깡총, 폼 데이터 안고 뛰네 🐇
닉네임 겹치면 멈춰! “이미 있어요” 말하네.
날짜별 풀이는 주머니에 쏙, 문제 ID까지 챙겨왔지.
기본 언어 한 스푼, 설정엔 길 표지판 둘.
오늘도 코드밭에 새싹이 돋았네. 🌱

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/user-problem-number

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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/org/ezcode/codetest/application/usermanagement/user/service/UserService.java (1)

114-117: 버그: 새 이미지 업로드 후 그 이미지를 삭제하는 역참조 오류

oldImageUrl을 저장해놓고 삭제 시에 user.getProfileImageUrl()(이미 새 URL)로 삭제하고 있어, 방금 업로드한 이미지를 지웁니다. oldImageUrl로 삭제해야 합니다.

-            if (oldImageUrl!=null) {
-                s3Uploader.delete(user.getProfileImageUrl(), "profile");
-            }
+            if (oldImageUrl != null) {
+                s3Uploader.delete(oldImageUrl, "profile");
+            }

추가로 하드코딩된 "profile" 대신 S3Directory.PROFILE.getDir() 사용을 권장합니다.

🧹 Nitpick comments (13)
src/main/java/org/ezcode/codetest/domain/submission/service/SubmissionDomainService.java (1)

3-3: 불필요한 import(LocalDate)로 보입니다.

이 파일 내에서 LocalDate는 사용되지 않습니다. 사용 계획이 없다면 제거해 경고를 줄이는 것을 권장합니다.

-import java.time.LocalDate;
src/main/java/org/ezcode/codetest/domain/user/repository/UserRepository.java (1)

25-25: getUserNicknames 메서드 제거 또는 구체적 쿼리로 대체

  • UserDomainService에서만 호출되며 컨트롤러·애플리케이션 서비스 등 외부 사용처가 없습니다. 실제 사용 케이스가 없다면 메서드를 삭제하고, 중복 확인 시 existsByNickname, 자동완성 시 findNicknamesByPrefix(prefix, pageable) 같은 범위 제한 쿼리로 대체하세요.
src/main/java/org/ezcode/codetest/domain/user/exception/code/UserExceptionCode.java (1)

18-18: HTTP 상태를 409 CONFLICT로 조정 제안

중복 리소스에 대한 의미론적으로는 400보다 409가 더 적합합니다. 서비스 계약 영향이 없으면 변경을 고려해 주세요.

-    ALREADY_EXIST_NICKNAME(false, HttpStatus.BAD_REQUEST, "이미 존재하는 닉네임입니다");
+    ALREADY_EXIST_NICKNAME(false, HttpStatus.CONFLICT, "이미 존재하는 닉네임입니다");
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/user/UserJpaRepository.java (1)

28-29: 중복 제거 및 결과 폭 제한 고려

  • 닉네임 유니크 보장이 없으면 중복이 반환될 수 있습니다. distinct 권장.
  • 대량 데이터 시 전체 로드는 비용이 큽니다. 가능하면 prefix + pageable로 대체를 검토하세요(위 Repository 코멘트 참고).
-	@Query("select u.nickname from User u ")
+	@Query("select distinct u.nickname from User u")
 	List<String> findAllNicknames();
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/query/UserProblemResultQueryRepositoryImpl.java (1)

37-45: 검증 완료: 호출부에 ‘개수 전용’ 가정 호출부(예: getCount())가 없습니다.
메서드명·DTO 주석 정리는 가독성 향상용 선택 제안입니다.

src/main/java/org/ezcode/codetest/domain/user/model/entity/User.java (1)

220-222: 언어 설정 Setter에 null 가드 추가 제안

서비스에서 “기본 언어 할당”을 의도한다면 null 주입을 방지하는 편이 안전합니다.

아래처럼 간단 가드를 추가해 주세요:

 public void setLanguage(Language userLanguage) {
-    this.language = userLanguage;
+    if (userLanguage == null) {
+        throw new IllegalArgumentException("language must not be null");
+    }
+    this.language = userLanguage;
 }
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/user/UserRepositoryImpl.java (1)

56-59: 전량 닉네임 조회는 규모에 따라 부담 — 사용처 확인 및 페이징/대안 검토

  • 대규모 사용자 기준 메모리/네트워크 비용이 커질 수 있습니다. 중복 검사용이면 existsByNickname(...)이 더 적합합니다.
  • 추천/자동완성 용도라면 Pageable/접두사 검색/캐시 등을 검토해 주세요.

사용처 맥락을 알려주시면 거기에 맞춘 대안을 제안하겠습니다.

src/main/java/org/ezcode/codetest/domain/user/service/UserDomainService.java (1)

135-141: 읽기 전용 트랜잭션으로 명시해 불필요한 쓰기 트랜잭션 회피

클래스 레벨 @transactional의 기본값(쓰기 가능)이 상속됩니다. 단순 조회 메서드는 readOnly로 오버라이드해주는 편이 JPA/Hibernate 최적화와 의도 표현에 좋습니다.

-    public List<String> getUserNicknames() {
+    @Transactional(readOnly = true)
+    public List<String> getUserNicknames() {
         return userRepository.getUserNicknames();
-    }
+    }
 
-    public boolean existsByNickname(String nickname) {
+    @Transactional(readOnly = true)
+    public boolean existsByNickname(String nickname) {
         return userRepository.existsByNickname(nickname);
-    }
+    }
src/main/java/org/ezcode/codetest/presentation/usermanagement/UserController.java (2)

50-55: multipart 전환은 클라이언트 호환성에 영향 — JSON 버전 동시 제공 또는 명시적 produces 추가 권장

기존 JSON 클라이언트는 모두 깨집니다. 최소한 produces 명시, 가능하면 JSON 전용 매핑을 추가해 점진 전환하세요.

-    @PutMapping(value = "/users", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    @PutMapping(
+        value = "/users",
+        consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
+        produces = MediaType.APPLICATION_JSON_VALUE
+    )

또는 JSON 호환 엔드포인트 추가(예시):

@PutMapping(value="/users", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserInfoResponse> modifyUserInfoJson(
    @AuthenticationPrincipal AuthUser authUser,
    @Valid @RequestBody ModifyUserInfoRequest request
){
    return ResponseEntity.status(HttpStatus.OK).body(userService.modifyUserInfo(authUser, request, null));
}

94-94: OpenAPI 설명에 요청 스키마/파트 명시 추가 제안

“날짜/문제ID 리스트/개수” 응답 설명은 좋습니다. modifyUserInfo의 경우에도 request 파트가 JSON임을 명시하고 예시를 넣으면 소비자가 혼동하지 않습니다.

src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java (1)

3-3: 미사용 import 제거

LocalDate는 본 파일에서 사용되지 않습니다.

-import java.time.LocalDate;
src/main/java/org/ezcode/codetest/presentation/usermanagement/UserVerifyController.java (2)

39-44: 프로퍼티 주입은 @ConfigurationProperties로 묶는 것을 고려(선택)

app.redirect.verify.*를 전용 설정 클래스로 바인딩하면 테스트/유지보수가 수월합니다.


64-81: 예외 삼키지 말고 최소한의 로그 남기기 + 303(See Other) 고려

현재 모든 예외를 그대로 삼키고 302로 리다이렉트합니다. 민감정보를 남기지 않는 선에서 최소한의 로그를 남기고, GET 리다이렉트에는 303을 고려하세요.

-        try {
+        try {
             authService.verifyEmailCode(email, key);
-            return ResponseEntity.status(HttpStatus.FOUND)
+            return ResponseEntity.status(HttpStatus.SEE_OTHER)
                 .header(HttpHeaders.LOCATION, verifySuccessRedirect)
                 .build();
         } catch (Exception e) {
-            return ResponseEntity.status(HttpStatus.FOUND)
+            // 민감정보(key, email)는 로그에 남기지 않음
+            log.warn("이메일 인증 실패: {}", e.getClass().getSimpleName());
+            return ResponseEntity.status(HttpStatus.SEE_OTHER)
                 .header(HttpHeaders.LOCATION, verifyFailureRedirect)
                 .build();
         }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between dc0d318 and 36a7244.

⛔ Files ignored due to path filters (5)
  • .DS_Store is excluded by !**/.DS_Store
  • src/.DS_Store is excluded by !**/.DS_Store
  • src/main/.DS_Store is excluded by !**/.DS_Store
  • src/main/java/.DS_Store is excluded by !**/.DS_Store
  • src/main/java/org/.DS_Store is excluded by !**/.DS_Store
📒 Files selected for processing (14)
  • src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java (3 hunks)
  • src/main/java/org/ezcode/codetest/domain/submission/dto/DailyCorrectCount.java (1 hunks)
  • src/main/java/org/ezcode/codetest/domain/submission/service/SubmissionDomainService.java (2 hunks)
  • src/main/java/org/ezcode/codetest/domain/user/exception/code/UserExceptionCode.java (1 hunks)
  • src/main/java/org/ezcode/codetest/domain/user/model/entity/User.java (1 hunks)
  • src/main/java/org/ezcode/codetest/domain/user/repository/UserRepository.java (1 hunks)
  • src/main/java/org/ezcode/codetest/domain/user/service/MailService.java (1 hunks)
  • src/main/java/org/ezcode/codetest/domain/user/service/UserDomainService.java (1 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/query/UserProblemResultQueryRepositoryImpl.java (2 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/user/UserJpaRepository.java (1 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/user/UserRepositoryImpl.java (1 hunks)
  • src/main/java/org/ezcode/codetest/presentation/usermanagement/UserController.java (3 hunks)
  • src/main/java/org/ezcode/codetest/presentation/usermanagement/UserVerifyController.java (3 hunks)
  • src/main/resources/application.properties (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (8)
src/main/java/org/ezcode/codetest/domain/submission/service/SubmissionDomainService.java (1)

124-124: EOF 개행 추가 OK

POSIX 관점에서 바람직한 변경입니다.

src/main/java/org/ezcode/codetest/domain/submission/dto/DailyCorrectCount.java (1)

8-12: count와 problemIds 불변성 보장 필요 — 레코드 불일치 위험

  • 파일: src/main/java/org/ezcode/codetest/domain/submission/dto/DailyCorrectCount.java
  • 제안: 카논 생성자 추가해 problemIdsList.copyOf로 불변화하고, countproblemIds.size()로 일관되게 유도하세요.
  • 생성자 추가 후 실제 호출부(예: new DailyCorrectCount(…) 또는 매핑 프레임워크 사용 지점)가 존재하는지 찾지 못했습니다. 수동으로 검증해주세요.
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/query/UserProblemResultQueryRepositoryImpl.java (1)

10-10: GroupBy 도입 일관성 확인 — OK

transform 기반 그룹핑 전환과 맞물려 필요한 의존성 추가로 보이며 문제 없습니다.

src/main/java/org/ezcode/codetest/domain/user/service/UserDomainService.java (1)

96-104: 닉네임 컬럼에 DB 유니크 제약 적용 여부 확인 필요
– 현재 @Column(nullable = false, unique = true)는 JPA 레벨 정의일 뿐, 실제 DB 스키마에 unique 제약이 있는지 마이그레이션 파일(예: Liquibase/Flyway)에서 반드시 확인해야 합니다.
– 만약 스키마에 제약이 없다면, 닉네임 컬럼에 unique 제약을 추가하는 마이그레이션을 작성하고, 저장 시 DataIntegrityViolationException을 캐치해 재시도 또는 적절한 에러 변환 로직을 구현해야 합니다.

src/main/java/org/ezcode/codetest/presentation/usermanagement/UserController.java (1)

16-16: LGTM: MediaType import 추가 적절

멀티파트 소비 타입 지정에 필요합니다.

src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java (1)

60-72: 매직 넘버 1L 상수화 및 기본 언어 유효성 검증

  • src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java:69
    src/main/java/org/ezcode/codetest/application/usermanagement/auth/service/AuthService.java:86
    src/main/java/org/ezcode/codetest/domain/user/service/CustomOAuth2UserService.java:88
    에서 getLanguage(1L) 호출을 DEFAULT_LANGUAGE_ID 상수로 통일해 리팩터링
  • DEFAULT_LANGUAGE_ID(1L)가 가리키는 Language 레코드가 DB 마이그레이션 또는 시드 스크립트에서 반드시 존재하도록 확인하거나, 누락 시 마이그레이션/시드에 해당 레코드 추가 필요
src/main/resources/application.properties (1)

162-163: 히카리 validation-timeout 라인 변경 없음(LGTM)

재정렬 수준의 변경으로 보이며 기능적 영향은 없습니다.

src/main/java/org/ezcode/codetest/presentation/usermanagement/UserVerifyController.java (1)

3-4: 리다이렉트 구현을 위한 의존성 추가 적절

@Value, HttpHeaders 추가는 새 리다이렉트 엔드포인트에 필요합니다.

Comment on lines +95 to +100
if (request.nickname() != null && !request.nickname().equals(user.getNickname())) {
if (userDomainService.existsByNickname(request.nickname())) {
log.info("중복 닉네임");
throw new UserException(UserExceptionCode.ALREADY_EXIST_NICKNAME);
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

닉네임 중복 선검사만으로는 부족 — DB 유니크 제약과 예외 매핑 병행 필요

현재 선검사로는 경쟁 상태를 막지 못합니다. 유니크 제약 추가 후 DataIntegrityViolationExceptionALREADY_EXIST_NICKNAME으로 매핑하는 방식을 병행하세요. 또한 공백/대소문자 정규화도 고려 바랍니다.

정규화 예시:

-        if (request.nickname() != null && !request.nickname().equals(user.getNickname())) {
-            if (userDomainService.existsByNickname(request.nickname())) {
+        if (request.nickname() != null) {
+            String newNickname = request.nickname().trim();
+            if (!newNickname.equals(user.getNickname()) && userDomainService.existsByNickname(newNickname)) {
                 log.info("중복 닉네임");
                 throw new UserException(UserExceptionCode.ALREADY_EXIST_NICKNAME);
             }
         }
🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java
around lines 95-100, the current pre-check for duplicate nicknames is
insufficient for race conditions; add DB-level unique constraint on the nickname
column (entity and migration) and keep the pre-check, but also catch
DataIntegrityViolationException (or the specific persistence exception your
stack throws) where the user is saved and map it to new
UserException(UserExceptionCode.ALREADY_EXIST_NICKNAME). Additionally normalize
nicknames (trim and a consistent case, e.g., toLowerCase()) before checking and
persisting so checks and DB values are consistent. Ensure your
global/transactional save path wraps persistence exceptions to translate them to
the ALREADY_EXIST_NICKNAME business exception.

Comment on lines 14 to 19
public DailyCorrectCount(java.sql.Date date, Set<Long> problemIds) {
this(
date.toLocalDate(),
problemIds.size(),
new ArrayList<>(problemIds)
);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

가변 리스트 노출 및 null 안전성 개선

  • new ArrayList<>(problemIds)는 외부에서 변경 가능해 레코드 불변성이 깨질 수 있습니다.
  • null 입력 시 NPE 위험이 있습니다.
  • 순서를 보장하려면 정렬을 명시하세요(클라이언트 일관성 향상).
-    public DailyCorrectCount(java.sql.Date date, Set<Long> problemIds) {
-        this(
-            date.toLocalDate(),
-            problemIds.size(),
-            new ArrayList<>(problemIds)
-        );
-    }
+    public DailyCorrectCount(java.sql.Date date, Set<Long> problemIds) {
+        var ids = (problemIds == null)
+            ? java.util.List.<Long>of()
+            : problemIds.stream().sorted().toList(); // 정렬 + 불변
+        this(date.toLocalDate(), 0, ids); // count는 카논 생성자에서 재계산
+    }

또한, ArrayList import는 불필요해집니다.

-import java.util.ArrayList;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/domain/submission/dto/DailyCorrectCount.java
around lines 14-19, the constructor currently does new ArrayList<>(problemIds)
which exposes a mutable list, can NPE on null input, and doesn't guarantee
order; change it to defensively create an immutable, sorted list from problemIds
(treat null as empty), set the count from the safe collection (or 0 if null),
and remove the unused ArrayList import; in short: null-check problemIds, produce
a deterministically sorted copy, wrap as an unmodifiable/immutable List, and use
that list for the record field and size.

body += "<h3>" + "아래 버튼을 클릭하여 이메일 인증을 완료해 주세요" + "</h3>";
// 이메일 버튼
body += "<a href='" + redirectUrl + "/api/auth/verify?email="+ email + "&key=" + key + "' target='_blenk'>이메일 인증 확인</a>";
body += "<a href='" + redirectUrl + "/api/auth/verify-page?email="+ email + "&key=" + key + "' target='_blenk'>이메일 인증 확인</a>";
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

사용자 입력 redirectUrl을 링크 호스트로 사용 → 인증코드 유출/피싱 위험 + URL 인코딩/타겟 오타 수정 필요

클라이언트가 제공하는 redirectUrl을 그대로 메일 링크의 호스트로 사용하면 공격자가 자신의 도메인으로 email/key를 수집할 수 있습니다. 서버가 신뢰하는 백엔드 퍼블릭 베이스 URL을 설정에서 주입받아 사용하고, 쿼리 파라미터는 안전하게 인코딩하세요. 또한 target='_blenk' 오타를 '_blank'로 수정해야 합니다.

적용 제안(diff):

@@
-import org.springframework.beans.factory.annotation.Value;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.util.UriComponentsBuilder;
@@
 	@Value("${spring.mail.username}")
 	private String senderEmail;
 
+	// 메일 링크의 호스트는 사용자 입력이 아닌, 신뢰 가능한 설정값을 사용
+	@Value("${app.backend.public-base-url}")
+	private String backendPublicBaseUrl;
@@ public MimeMessage CreateButtonMail(Long userId, String email, String redirectUrl) {
-			// 이메일 버튼
-			body += "<a href='" + redirectUrl + "/api/auth/verify-page?email="+ email + "&key=" + key + "' target='_blenk'>이메일 인증 확인</a>";
+			// 안전한 URL 빌드 및 오타 수정
+			String href = UriComponentsBuilder.fromHttpUrl(backendPublicBaseUrl)
+				.path("/api/auth/verify-page")
+				.queryParam("email", email)
+				.queryParam("key", key)
+				.build()
+				.toUriString();
+			body += "<a href=\"" + href + "\" target=\"_blank\">이메일 인증 확인</a>";

비밀번호 메일에도 동일 오타가 있으므로 함께 수정 권장:

-			body += "<a href='"+redirectUrl+"/api/auth/find-password-verify?email="+ email + "&key=" + verificationCode + "' target='_blenk'>비밀번호 변경하기</a>";
+			body += "<a href='"+redirectUrl+"/api/auth/find-password-verify?email="+ email + "&key=" + verificationCode + "' target=\"_blank\">비밀번호 변경하기</a>";

검증용 스크립트:


🏁 Script executed:

#!/bin/bash
# redirectUrl 사용처 및 verify-page 링크 조립 위치 확인
rg -n -C2 '\bredirectUrl\b'
rg -n '/api/auth/verify-page'

Length of output: 7587


redirectUrl 직접 사용·쿼리 미인코딩·타겟 오타 수정

  • 클라이언트 입력 redirectUrl 대신 설정값(app.backend.public-base-url)으로 링크 호스트 고정
  • UriComponentsBuilder로 쿼리 파라미터(email, key) 안전 인코딩
  • target='_blenk'target='_blank' 오타 수정

적용 예시:

+ import org.springframework.web.util.UriComponentsBuilder;
@@ public MimeMessage CreateButtonMail(…)
- body += "<a href='" + redirectUrl + "/api/auth/verify-page?email="+ email + "&key=" + key + "' target='_blenk'>이메일 인증 확인</a>";
+ String href = UriComponentsBuilder
+     .fromHttpUrl(backendPublicBaseUrl)
+     .path("/api/auth/verify-page")
+     .queryParam("email", email)
+     .queryParam("key", key)
+     .build()
+     .toUriString();
+ body += "<a href=\"" + href + "\" target=\"_blank\">이메일 인증 확인</a>";

CreatePasswordMail에도 동일한 target 오타 수정 권장.

📝 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
body += "<a href='" + redirectUrl + "/api/auth/verify-page?email="+ email + "&key=" + key + "' target='_blenk'>이메일 인증 확인</a>";
// add at top of file
import org.springframework.web.util.UriComponentsBuilder;
public MimeMessage CreateButtonMail(…) {
// build a safe, encoded verification link using the configured base URL
String href = UriComponentsBuilder
.fromHttpUrl(backendPublicBaseUrl)
.path("/api/auth/verify-page")
.queryParam("email", email)
.queryParam("key", key)
.build()
.toUriString();
// use the new href and correct the target typo
body += "<a href=\"" + href + "\" target=\"_blank\">이메일 인증 확인</a>";
}
🤖 Prompt for AI Agents
In src/main/java/org/ezcode/codetest/domain/user/service/MailService.java around
line 54, stop using the client-supplied redirectUrl and instead build the
verification link using the configured app.backend.public-base-url as the host;
construct the URL with Spring's UriComponentsBuilder (set the base URL, path
/api/auth/verify-page, and add email and key as query parameters so they are
percent-encoded) and use the builtUriString() result for the href, and fix the
anchor target typo from '_blenk' to '_blank'; apply the same target '_blank' fix
to CreatePasswordMail as well.

Comment on lines +164 to +170
# ========================
# App Redirects (Email Verify)
# ========================
# 인증 성공 시 이동할 페이지 URL (예: 프론트의 성공 안내 페이지)
app.redirect.verify.success=${APP_REDIRECT_VERIFY_SUCCESS:https://your-frontend.example.com/verify/success}
# 인증 실패 시 이동할 페이지 URL (예: 프론트의 실패 안내 페이지)
app.redirect.verify.failure=${APP_REDIRECT_VERIFY_FAILURE:https://your-frontend.example.com/verify/failure}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

리다이렉트 속성 추가는 적절. 메일 링크용 백엔드 베이스 URL 속성도 함께 정의 권장

메일 링크 호스트를 신뢰 가능한 설정에서 주입받을 수 있도록 백엔드 퍼블릭 베이스 URL 속성을 추가하세요.

 # ========================
 # App Redirects (Email Verify)
 # ========================
+# 백엔드 퍼블릭 베이스 URL (메일 인증 링크 생성 시 사용)
+app.backend.public-base-url=${APP_BACKEND_PUBLIC_BASE_URL:https://api.your-backend.example.com}
+
 # 인증 성공 시 이동할 페이지 URL (예: 프론트의 성공 안내 페이지)
 app.redirect.verify.success=${APP_REDIRECT_VERIFY_SUCCESS:https://your-frontend.example.com/verify/success}
 # 인증 실패 시 이동할 페이지 URL (예: 프론트의 실패 안내 페이지)
 app.redirect.verify.failure=${APP_REDIRECT_VERIFY_FAILURE:https://your-frontend.example.com/verify/failure}
📝 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
# ========================
# App Redirects (Email Verify)
# ========================
# 인증 성공 시 이동할 페이지 URL (예: 프론트의 성공 안내 페이지)
app.redirect.verify.success=${APP_REDIRECT_VERIFY_SUCCESS:https://your-frontend.example.com/verify/success}
# 인증 실패 시 이동할 페이지 URL (예: 프론트의 실패 안내 페이지)
app.redirect.verify.failure=${APP_REDIRECT_VERIFY_FAILURE:https://your-frontend.example.com/verify/failure}
# ========================
# App Redirects (Email Verify)
# ========================
# 백엔드 퍼블릭 베이스 URL (메일 인증 링크 생성 시 사용)
app.backend.public-base-url=${APP_BACKEND_PUBLIC_BASE_URL:https://api.your-backend.example.com}
# 인증 성공 시 이동할 페이지 URL (예: 프론트의 성공 안내 페이지)
app.redirect.verify.success=${APP_REDIRECT_VERIFY_SUCCESS:https://your-frontend.example.com/verify/success}
# 인증 실패 시 이동할 페이지 URL (예: 프론트의 실패 안내 페이지)
app.redirect.verify.failure=${APP_REDIRECT_VERIFY_FAILURE:https://your-frontend.example.com/verify/failure}
🤖 Prompt for AI Agents
In src/main/resources/application.properties around lines 164 to 170, you
currently define frontend redirect URLs for email verification but lack a
configurable backend/public base URL for constructing mail links; add a new
property (e.g. app.public.base-url with an environment-backed default like
${APP_PUBLIC_BASE_URL:https://your-backend.example.com}) adjacent to the
redirect entries so the mailer can build absolute links using that value instead
of hardcoding hostnames; ensure the property key is documented with a brief
comment and referenced by the email-generation code to compose verification
links.

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

♻️ Duplicate comments (1)
src/main/java/org/ezcode/codetest/domain/submission/dto/DailyCorrectCount.java (1)

6-10: 레코드 불변식 및 null‑안전 보강 (count ↔ problemIds 정합성 보장)

외부에서 임의의 값으로 count를 주입하면 problemIds.size()와 불일치 가능성이 있습니다. 캡슐화를 위해 컴팩트 캐논 생성자로 방어적 복사와 정합성 보장을 해주세요.

 public record DailyCorrectCount(
     LocalDate date,
     long count,
     Set<Long> problemIds
 ) {
+    public DailyCorrectCount {
+        java.util.Objects.requireNonNull(date, "date must not be null");
+        // null‑safe + 불변 복사
+        problemIds = (problemIds == null) ? java.util.Set.of() : java.util.Set.copyOf(problemIds);
+        // 불변식: count는 problemIds의 크기와 동일
+        count = problemIds.size();
+    }
 }
🧹 Nitpick comments (1)
src/main/java/org/ezcode/codetest/domain/submission/dto/DailyCorrectCount.java (1)

9-9: 응답 결정성(순서) 필요 시: 정렬 보장 고려

클라이언트가 problemIds의 순서에 의존한다면 Set의 순서는 비결정적입니다. 필요 시 정렬된 불변 Set으로 치환하세요.

-        problemIds = (problemIds == null) ? java.util.Set.of() : java.util.Set.copyOf(problemIds);
+        problemIds = (problemIds == null)
+            ? java.util.Set.of()
+            : java.util.Collections.unmodifiableSet(new java.util.TreeSet<>(problemIds));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 36a7244 and 1258cfb.

📒 Files selected for processing (3)
  • src/main/java/org/ezcode/codetest/domain/submission/dto/DailyCorrectCount.java (1 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/query/UserProblemResultQueryRepositoryImpl.java (2 hunks)
  • src/main/java/org/ezcode/codetest/presentation/usermanagement/UserVerifyController.java (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/org/ezcode/codetest/presentation/usermanagement/UserVerifyController.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/query/UserProblemResultQueryRepositoryImpl.java (1)
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/impl/UserProblemResultRepositoryImpl.java (1)
  • Repository (16-53)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (2)
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/query/UserProblemResultQueryRepositoryImpl.java (2)

31-31: distinct 카운트 도입 👍

upr.problem.id.countDistinct()로 집합 기반 통계와 일관성이 맞춰집니다.


39-41: 정렬 보장 검토

orderBy(date.asc())transform(groupBy(...).list(...)) 이전에 있어 키 순서는 보통 보장됩니다. 다만 DB/드라이버에 따라 미묘한 차이가 있을 수 있으니, 통합 테스트로 날짜 오름차순 정렬을 한 번 검증해 주세요.

Comment on lines 7 to 10
LocalDate date,
int count
) {
public DailyCorrectCount(java.sql.Date date, int count) {
this(date.toLocalDate(), count);
}
}
long count,
Set<Long> problemIds
) { }
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

QueryDSL 생성자 프로젝션과 타입 불일치로 런타임 실패 위험 (LocalDate vs java.sql.Date)

현재 리포지토리 쿼리는 (java.sql.Date, Long, Set<Long>)를 넘기는데, 레코드는 (LocalDate, long, Set<Long>)만 존재합니다. QueryDSL Projections.constructor는 자동 변환을 하지 않으므로 런타임 InstantiationException이 납니다. 브릿지 생성자를 추가해 주세요.

적용 예시:

 public record DailyCorrectCount(
     LocalDate date,
     long count,
     Set<Long> problemIds
-) {  }
+) {
+    // QueryDSL(JPA) DATE() -> java.sql.Date 대응 브릿지 생성자
+    public DailyCorrectCount(java.sql.Date date, Long count, Set<Long> problemIds) {
+        this(date == null ? null : date.toLocalDate(),
+             count == null ? 0L : count.longValue(),
+             problemIds);
+    }
+}
📝 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
LocalDate date,
int count
) {
public DailyCorrectCount(java.sql.Date date, int count) {
this(date.toLocalDate(), count);
}
}
long count,
Set<Long> problemIds
) { }
public record DailyCorrectCount(
LocalDate date,
long count,
Set<Long> problemIds
) {
// QueryDSL(JPA) DATE() -> java.sql.Date 대응 브릿지 생성자
public DailyCorrectCount(java.sql.Date date, Long count, Set<Long> problemIds) {
this(
date == null ? null : date.toLocalDate(),
count == null ? 0L : count.longValue(),
problemIds
);
}
}
🤖 Prompt for AI Agents
In
src/main/java/org/ezcode/codetest/domain/submission/dto/DailyCorrectCount.java
around lines 7 to 10, QueryDSL is instantiating with (java.sql.Date, Long,
Set<Long>) but the class only has (LocalDate, long, Set<Long>), causing runtime
InstantiationException; add an additional "bridge" constructor that accepts
java.sql.Date (or java.util.Date), long/Long, and Set<Long> and converts the
date to LocalDate (e.g., date.toLocalDate() with null-safety) before delegating
to the existing constructor so QueryDSL constructor projection matches the
runtime types.

@minjee2758 minjee2758 merged commit dcbe2db into dev Sep 6, 2025
1 check passed
@minjee2758 minjee2758 deleted the refactor/user-problem-number branch September 6, 2025 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants