Conversation
Walkthrough여러 클래스에서 로깅을 제거하고 불필요한 임포트를 정리했다. 미션 리뷰 스크롤 API는 메서드명 변경과 정렬 방향 파라미터(sortType) 추가로 시그니처가 바뀌었다. 미션 엔티티에 tip 필드를 추가하고 DTO에 반영했다. 리포지토리 쿼리를 단순화하고 서비스 로직을 이에 맞게 조정했다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant Controller as MissionController
participant Service as MissionService
participant Repo as MissionRepository
participant ReviewQ as ReviewQueryRepository
Client->>Controller: GET /missions/{missionId}/reviews?lastReviewId&limit&sortType
Controller->>Service: getMissionReviewsWithCursor(missionId, lastReviewId, limit, sortType)
Service->>ReviewQ: findReviewsWithCursor(missionId, lastReviewId, limit+1, sortType)
ReviewQ-->>Service: List<Review>
note right of Service: hasNext = size > limit<br/>nextId = last(review).id (있을 때)
Service-->>Controller: PageResult(reviews[0..limit-1], nextId, hasNext)
Controller-->>Client: 200 OK (ResponseBody)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. 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. Comment |
There was a problem hiding this comment.
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 (10)
src/main/java/avengers/lion/global/OAuth2SuccessHandler.java (1)
50-58: 카카오 이메일 null 케이스 미처리카카오 동의 범위에 따라 이메일이 제공되지 않을 수 있습니다. 이메일이 null이면 회원 생성/조회 로직이 실패합니다. 대체 키(예: kakao id)로 조회/매핑하거나, 추가 동의 요청/예외 처리 필요합니다.
예시:
- Member member = memberRepository.findByEmail(kakaoUserInfo.getEmail()) + String email = kakaoUserInfo.getEmail(); + if (email == null) { + // kakaoUserInfo.getId() 등을 기반으로 매핑하거나 명시적 예외 처리 + throw new IllegalStateException("카카오 이메일을 가져올 수 없습니다."); + } + Member member = memberRepository.findByEmail(email) .orElseGet(()-> memberRepository.save( Member.builder() - .email(kakaoUserInfo.getEmail()) + .email(email) .nickname(kakaoUserInfo.getNickname()) .profileImageUrl(kakaoUserInfo.getProfileImageUrl()) .role(MemberRole.ROLE_USER) .build() ));src/main/java/avengers/lion/mission/service/MetadataCacheService.java (2)
37-51: 예외 삼킴(swallowing)으로 장애 감지 불가 및 디버깅 어려움.조회 실패(네트워크/직렬화 오류)와 “캐시 미스(null)”가 동일한
null로 반환됩니다. 최소한 예외는 래핑해 상위로 전달하세요.적용 패치:
public VerificationMetadata getMetadata(String jobId) { try { String key = CACHE_PREFIX + jobId; Object cached = redisTemplate.opsForValue().get(key); if (cached instanceof VerificationMetadata metadata) { return metadata; } return null; } catch (Exception e) { - return null; + throw new RuntimeException("메타데이터 조회 중 오류가 발생했습니다.", e); } }
53-59: 빈 catch 블록: 실패 은폐로 정합성 이슈 유발.삭제 실패를 숨기면 정리(cleanup) 로직이 성공한 것으로 오인될 수 있습니다. 최소한 예외를 전달하세요.
패치:
public void removeMetadata(String jobId) { try { String key = CACHE_PREFIX + jobId; redisTemplate.delete(key); } catch (Exception e) { - } + throw new RuntimeException("메타데이터 삭제 중 오류가 발생했습니다.", e); + } }src/main/java/avengers/lion/mission/domain/Mission.java (2)
65-76: 생성자에서 새로운 tip 파라미터 누락.Mission 엔티티에 tip 필드가 추가되었지만, 기존 생성자에는 tip 파라미터가 포함되지 않았습니다. 이로 인해 생성자를 통해 Mission 객체를 생성할 때 tip 필드가 항상 null로 설정됩니다.
생성자 시그니처를 업데이트하여 tip 필드를 포함해주세요:
- public Mission(Long id, String placeName, String address, String description, PlaceCategory placeCategory, MissionStatus status, Double latitude, Double longitude, MissionBatches missionBatches, MissionTemplate sourceTemplate) { + public Mission(Long id, String placeName, String address, String description, PlaceCategory placeCategory, MissionStatus status, Double latitude, Double longitude, String tip, MissionBatches missionBatches, MissionTemplate sourceTemplate) { this.id = id; this.placeName = placeName; this.address = address; this.description = description; this.placeCategory = placeCategory; this.status = status; this.latitude = latitude; this.longitude = longitude; + this.tip = tip; this.missionBatches = missionBatches; this.sourceTemplate = sourceTemplate; }
78-92: createFromTemplate 메서드에서 tip 필드 처리 필요.
createFromTemplate정적 팩토리 메서드에서도 tip 필드에 대한 처리가 누락되었습니다. MissionTemplate에서 tip 정보를 가져오거나 기본값을 설정해야 합니다.MissionTemplate에서 tip을 가져오도록 수정하거나 적절한 기본값을 설정해주세요:
public static Mission createFromTemplate(MissionTemplate template, MissionBatches batch) { Place place = template.getPlace(); return new Mission( null, place.getName(), place.getAddress(), template.getDescription(), place.getCategory(), MissionStatus.ACTIVE, place.getLatitude(), place.getLongitude(), + template.getTip(), // 또는 적절한 기본값 batch, template ); }src/main/java/avengers/lion/mission/service/MissionVerifyService.java (5)
55-58: 전역 에러 정책 우회: RuntimeException 대신 BusinessException 사용컨트롤러 어드바이스/에러 코드 체계와의 일관성을 위해 도메인형 예외로 래핑하세요.
- log.error("Failed to generate upload URL for missionId: {}", missionId, e); - throw new RuntimeException("업로드 URL 생성 중 오류가 발생했습니다.", e); + log.error("Failed to generate upload URL for missionId: {}", missionId, e); + throw new BusinessException(ExceptionType.VERIFICATION_UPLOAD_URL_CREATE_FAILED, e);
64-76: imageUrl 신뢰는 SSRF/권한 우회 위험 — 도메인/키 일치 검증 추가 필요클라이언트가 제공한 URL을 그대로 FastAPI로 전달하면 내부망 접근(SSRF) 또는 타 계정 리소스 검증 우회가 가능합니다. https 스킴, 허용 도메인(Cloudinary), 그리고 uploadKey(publicId) 포함 여부를 서버에서 검증하세요. 가능하면 서버가 uploadKey로 안전한 canonical URL을 구성해 사용하세요.
최소 보강 예(검증 + 정제 URL 사용):
try { // 새로운 jobId 생성 String jobId = metadataCacheService.generateJobId(); // 메타데이터 캐시 저장 + final String rawImageUrl = request.imageUrl(); + final String uploadKey = request.uploadKey(); + validateImageUrlOrThrow(rawImageUrl, uploadKey); // 스킴/도메인/키 일치 검증 + final String sanitizedImageUrl = rawImageUrl; // 필요 시 uploadKey 기반 canonical URL로 대체 metadataCacheService.cacheMetadata( jobId, - request.imageUrl(), - request.uploadKey(), // publicId + sanitizedImageUrl, + uploadKey, // publicId missionId, memberId ); // FastAPI에 비동기 분석 요청 - callFastApiAsync(jobId, request.imageUrl()); + callFastApiAsync(jobId, sanitizedImageUrl); return new VerifyImageResponse(jobId);도우미 메서드(파일 하단에 추가):
private void validateImageUrlOrThrow(String imageUrl, String uploadKey) { try { if (imageUrl == null || uploadKey == null) { throw new BusinessException(ExceptionType.INVALID_IMAGE_URL); } var uri = java.net.URI.create(imageUrl); if (!"https".equalsIgnoreCase(uri.getScheme())) { throw new BusinessException(ExceptionType.INVALID_IMAGE_URL); } var host = uri.getHost(); if (host == null || !(host.endsWith("cloudinary.com") || host.endsWith("res.cloudinary.com"))) { throw new BusinessException(ExceptionType.INVALID_IMAGE_URL); } if (!imageUrl.contains(uploadKey)) { throw new BusinessException(ExceptionType.INVALID_IMAGE_URL); } } catch (IllegalArgumentException e) { throw new BusinessException(ExceptionType.INVALID_IMAGE_URL, e); } }Also applies to: 79-81
83-94: 예외 처리 일관성 + 보상 처리 순서현재 RuntimeException을 던져 전역 에러 매핑을 우회합니다. 또한 업로드 정리 실패는 삼키되, 최종 예외는 도메인형으로 변환하세요.
- throw new RuntimeException("인증 시작 중 오류가 발생했습니다.", e); + throw new BusinessException(ExceptionType.VERIFICATION_START_FAILED, e);
113-128: WebClient: 상태코드 처리/타임아웃/Content-Type 명시 누락비정상 상태코드(onStatus), 타임아웃, Content-Type을 명시하세요. 실패 시 cleanup 보장은 유지합니다.
webClient.post() .uri(fastApiUrl + "/analyze") + .contentType(org.springframework.http.MediaType.APPLICATION_JSON) .bodyValue(requestBody) - .retrieve() - .bodyToMono(String.class) + .retrieve() + .onStatus(org.springframework.http.HttpStatusCode::isError, resp -> + resp.bodyToMono(String.class).defaultIfEmpty("") + .map(body -> new RuntimeException("FastAPI non-2xx: " + resp.statusCode() + " body=" + body)) + ) + .bodyToMono(Void.class) + .timeout(java.time.Duration.ofSeconds(10)) .subscribe( - result -> log.info("FastAPI call succeeded for jobId: {}", jobId), + unused -> log.info("FastAPI call succeeded for jobId: {}", jobId), error -> { log.error("FastAPI call failed for jobId: {}", jobId, error); try { cleanupService.cleanupFailedVerification(jobId); } catch (Exception e) { log.error("Failed to cleanup failed verification: {}", jobId, e); } } );
64-65: 미션 존재·상태·권한 검증 추가 필요검증 누락으로 비활성 또는 권한 없는 미션에 대해 인증 절차를 진행하거나 DB에 저장될 수 있습니다.
- src/main/java/avengers/lion/mission/service/MissionVerifyService.java — generateUploadUrl(Long) (라인 ~42) 및 startVerification(...) (라인 ~64): 미션 존재 및 상태(MissionStatus.ACTIVE)와 멤버 권한 검증 추가(또는 missionService로 위임).
- src/main/java/avengers/lion/mission/controller/VerificationController.java — getUploadUrl / startVerification 엔드포인트(라인 47 / 59): 컨트롤러에서 선검증으로 처리할 경우 missionService 호출로 검증 보장.
- src/main/java/avengers/lion/mission/service/CallbackService.java — saveCompletedMission(...) (라인 156): 현재는 미션 존재만 확인하므로 미션 상태(활성 여부)도 확인해 비활성 미션에 대한 CompletedMission 저장을 차단.
🧹 Nitpick comments (13)
src/main/java/avengers/lion/global/OAuth2SuccessHandler.java (6)
73-75: 프론트엔드 콜백 URL 하드코딩 제거 필요환경별로 달라지는 값을 소스에 고정하면 배포/보안 리스크가 큽니다. 설정값(예: application.yml)으로 주입하고, 허용된 도메인 화이트리스트를 강제하세요. 또한 프로덕션은 HTTPS를 기본으로.
적용 예시:
- String frontendUrl = "http://localhost:5174/auth/callback?code=" + authCode; - getRedirectStrategy().sendRedirect(request, response, frontendUrl); + String baseRedirect = appProps.getFrontendCallbackUrl(); // ex) https://app.example.com/auth/callback + String frontendUrl = UriComponentsBuilder.fromHttpUrl(baseRedirect) + .queryParam("code", authCode) + .build(true) + .toUriString(); + getRedirectStrategy().sendRedirect(request, response, frontendUrl);구성 추가:
+@ConfigurationProperties("app.auth") +public class AppAuthProps { + private String frontendCallbackUrl; + // getter/setter +}
11-11: @transactional 어노테이션 패키지 교체 권장현재 jakarta.transaction.Transactional 사용. Spring의 선언적 트랜잭션 기능(롤백 규칙 등)을 온전히 활용하려면 org.springframework.transaction.annotation.Transactional로 교체하세요.
-import jakarta.transaction.Transactional; +import org.springframework.transaction.annotation.Transactional;Also applies to: 39-39
51-61: 동시 가입 경쟁 조건 가능성(이메일 unique 충돌)동시에 같은 이메일로 최초 로그인 시 findByEmail → save 사이 레이스가 발생할 수 있습니다. 이메일에 유니크 인덱스를 보장하고, DataIntegrityViolationException 캐치 후 재조회 리트라이를 권장합니다.
예시 패턴:
try { return memberRepository.findByEmail(email) .orElseGet(() -> memberRepository.save(...)); } catch (DataIntegrityViolationException e) { return memberRepository.findByEmail(email).orElseThrow(e); }
75-77: 인증 속성 정리 누락리다이렉트 후 세션 속성 정리를 보장하려면 clearAuthenticationAttributes 호출을 권장합니다.
- getRedirectStrategy().sendRedirect(request, response, frontendUrl); + clearAuthenticationAttributes(request); + getRedirectStrategy().sendRedirect(request, response, frontendUrl);
18-19: 불필요한 로깅 애노테이션 제거로그를 모두 제거했는데 @slf4j는 남아 있습니다. 사용하지 않으면 제거해 주세요.
-import lombok.extern.slf4j.Slf4j; ... -@Slf4jAlso applies to: 26-26
13-13: RedisTemplate 타입 단순화 제안문자열 키/값만 다루므로 StringRedisTemplate 사용이 안전하고 직관적입니다. 직렬화 설정 이슈도 줄어듭니다.
-import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; ... - private final RedisTemplate<String, String> redisTemplate; + private final StringRedisTemplate redisTemplate;Also applies to: 37-37, 70-70
src/main/java/avengers/lion/mission/service/MetadataCacheService.java (3)
18-20: TTL 하드코딩 제거: 환경별 튜닝 가능하도록 외부화.프로퍼티로 노출해 운영/스테이징에서 유연하게 조정하세요.
예시(선호안):
- application.yml:
verification.metadata.ttl-minutes: 30- 코드 변경:
- private static final Duration CACHE_TTL = Duration.ofMinutes(30); + @org.springframework.beans.factory.annotation.Value("${verification.metadata.ttl-minutes:30}") + private long cacheTtlMinutes;그리고 set 호출부에서:
- redisTemplate.opsForValue().set(key, metadata, CACHE_TTL); + redisTemplate.opsForValue().set(key, metadata, java.time.Duration.ofMinutes(cacheTtlMinutes));
25-35: 입력 값 방어로직 누락: key/필드 null 허용 시 런타임 오류 가능.
jobId,publicId,missionId,memberId에 대한 널/공백 검증 추가 권장.간단 예시:
public void cacheMetadata(String jobId, String imageUrl, String publicId, Long missionId, Long memberId) { try { + if (jobId == null || jobId.isBlank()) throw new IllegalArgumentException("jobId is required"); + if (publicId == null || publicId.isBlank()) throw new IllegalArgumentException("publicId is required"); + if (missionId == null || memberId == null) throw new IllegalArgumentException("missionId/memberId are required"); VerificationMetadata metadata = new VerificationMetadata(imageUrl, publicId, missionId, memberId); String key = CACHE_PREFIX + jobId; - redisTemplate.opsForValue().set(key, metadata, CACHE_TTL); + redisTemplate.opsForValue().set(key, metadata, java.time.Duration.ofMinutes(cacheTtlMinutes));
37-47: Optional 반환 고려 — 호출부 2곳 동시 수정 필요getMetadata를 Optional로 바꾸면 의도가 명확해지나 공개 API 변경이므로 아래 호출부들을 함께 업데이트해야 함.
- 정의: src/main/java/avengers/lion/mission/service/MetadataCacheService.java — getMetadata (line 37).
- 호출: src/main/java/avengers/lion/mission/service/VerificationCleanupService.java — lines 19–20 (대입 후 null 검사).
- 호출: src/main/java/avengers/lion/mission/service/CallbackService.java — lines 123–124 (대입 후 null 검사).
src/main/java/avengers/lion/mission/service/MissionVerifyService.java (4)
49-53: Cloudinary 업로드 preset 하드코딩 제거 권장보안/운영 편의상 preset은 설정으로 주입하는 것이 안전합니다.
적용 예(해당 범위 내 수정):
- return new UploadUrlResponse( - uploadInfo.uploadUrl(), - uploadInfo.publicId(), - "LetsGGu"// Unsigned upload preset (노출 안전) - ); + return new UploadUrlResponse( + uploadInfo.uploadUrl(), + uploadInfo.publicId(), + unsignedPreset // 설정 주입 + );추가 필드(파일 상단 필드 구역에 배치):
@Value("${cloudinary.upload.unsigned-preset}") private String unsignedPreset;
102-102: 중복 비동기: @async와 Reactor subscribe를 동시에 사용이미 Reactor에서 비동기로 동작하므로 @async는 제거하거나, 반대로 subscribe를 제거하고 Mono를 반환해 호출부에서 구독 중 하나만 사용하세요. 현재 형태는 디버깅/스레드 관리가 어려워집니다.
최소 변경(주석만 제거):
- @Async public void callFastApiAsync(String jobId, String imageUrl) {
107-111: 콜백 URL 문자열 결합 대신 안전한 빌더 사용기본 URL의 슬래시 유무에 따라 // 발생 가능. UriComponentsBuilder로 안전하게 조립하세요.
- Map<String, Object> requestBody = Map.of( - "job_id", jobId, - "image_url", imageUrl, - "callback_url", publicBaseUrl + "/api/v1/missions/" + jobId + "/callback" - ); + String callbackUrl = org.springframework.web.util.UriComponentsBuilder + .fromHttpUrl(publicBaseUrl) + .path("/api/v1/missions/{jobId}/callback") + .buildAndExpand(jobId) + .toUriString(); + Map<String, Object> requestBody = Map.of( + "job_id", jobId, + "image_url", imageUrl, + "callback_url", callbackUrl + );
99-101: 설명 주석 추가는 좋습니다만, 메서드 역할 명확화 한 줄 보완 제안“FastAPI 비동기 분석 요청을 송신하며 실패 시 정리(cleanup)까지 담당” 정도를 한 줄에 담으면 더 명확합니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
src/main/java/avengers/lion/auth/controller/AuthController.java(0 hunks)src/main/java/avengers/lion/global/OAuth2SuccessHandler.java(1 hunks)src/main/java/avengers/lion/mission/api/MissionApi.java(1 hunks)src/main/java/avengers/lion/mission/controller/MissionController.java(1 hunks)src/main/java/avengers/lion/mission/domain/Mission.java(1 hunks)src/main/java/avengers/lion/mission/dto/response/MissionResponse.java(2 hunks)src/main/java/avengers/lion/mission/repository/MissionRepository.java(1 hunks)src/main/java/avengers/lion/mission/service/CallbackService.java(0 hunks)src/main/java/avengers/lion/mission/service/MetadataCacheService.java(1 hunks)src/main/java/avengers/lion/mission/service/MissionService.java(5 hunks)src/main/java/avengers/lion/mission/service/MissionVerifyService.java(2 hunks)src/main/java/avengers/lion/mission/service/VerificationCleanupService.java(0 hunks)src/main/java/avengers/lion/mission/service/VerificationEventService.java(0 hunks)src/main/java/avengers/lion/review/repository/ReviewQueryRepositoryImpl.java(0 hunks)
💤 Files with no reviewable changes (5)
- src/main/java/avengers/lion/mission/service/CallbackService.java
- src/main/java/avengers/lion/auth/controller/AuthController.java
- src/main/java/avengers/lion/review/repository/ReviewQueryRepositoryImpl.java
- src/main/java/avengers/lion/mission/service/VerificationEventService.java
- src/main/java/avengers/lion/mission/service/VerificationCleanupService.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-13T13:24:54.608Z
Learnt from: Juhye0k
PR: Lets-Gu/backend#25
File: src/main/java/avengers/lion/mission/repository/MissionRepository.java:15-18
Timestamp: 2025-08-13T13:24:54.608Z
Learning: MissionRepository.findAllByMissionStatusWithCompletedMissions() 메서드는 의도적으로 모든 CompletedMission 데이터를 fetch-join으로 로드합니다. 이는 서비스 레이어에서 특정 회원의 미션 완료 여부를 판단하기 위해 전체 완료 기록이 필요하기 때문입니다.
Applied to files:
src/main/java/avengers/lion/mission/repository/MissionRepository.javasrc/main/java/avengers/lion/mission/service/MissionService.java
🧬 Code graph analysis (6)
src/main/java/avengers/lion/global/OAuth2SuccessHandler.java (3)
src/main/java/avengers/lion/auth/api/AuthApi.java (2)
Operation(14-25)Tag(11-37)src/main/java/avengers/lion/global/config/security/SecurityConfig.java (2)
Configuration(23-81)oauth2(60-64)src/main/java/avengers/lion/auth/service/AuthService.java (1)
Service(9-19)
src/main/java/avengers/lion/mission/service/MetadataCacheService.java (1)
src/main/java/avengers/lion/mission/service/VerificationCleanupService.java (1)
Slf4j(7-33)
src/main/java/avengers/lion/mission/domain/Mission.java (2)
src/main/java/avengers/lion/mission/domain/CompletedMission.java (2)
Entity(17-59)Builder(47-54)src/main/java/avengers/lion/mission/domain/MissionBatches.java (1)
Entity(15-59)
src/main/java/avengers/lion/mission/dto/response/MissionResponse.java (4)
src/main/java/avengers/lion/mission/dto/response/MissionReviewResponse.java (1)
MissionReviewResponse(8-20)src/main/java/avengers/lion/mission/dto/response/MissionPreReviewResponse.java (1)
MissionPreReviewResponse(8-12)src/main/java/avengers/lion/review/dto/response/UnWrittenReviewResponse.java (1)
Schema(7-28)src/main/java/avengers/lion/review/dto/response/WrittenReviewResponse.java (1)
Schema(9-36)
src/main/java/avengers/lion/mission/api/MissionApi.java (3)
src/main/java/avengers/lion/review/repository/ReviewQueryRepositoryImpl.java (2)
Override(23-45)Override(47-65)src/main/java/avengers/lion/review/repository/ReviewQueryRepository.java (1)
findAllReviewByMissionId(13-13)src/main/java/avengers/lion/review/api/ReviewApi.java (1)
Operation(105-140)
src/main/java/avengers/lion/mission/repository/MissionRepository.java (2)
src/main/java/avengers/lion/mission/repository/CompletedMissionRepository.java (2)
CompletedMissionRepository(10-14)Query(12-13)src/main/java/avengers/lion/review/repository/CompletedMissionQueryRepositoryImpl.java (2)
Slf4j(18-47)Override(25-46)
🔇 Additional comments (14)
src/main/java/avengers/lion/mission/service/MetadataCacheService.java (2)
62-67: VerificationMetadata에 java.io.Serializable 구현 추가 권고레코드가 Serializable을 구현하지 않아 Redis 저장(기본 JDK 직렬화 또는 RedisTemplate 설정 미적용 시)에서 NotSerializableException 또는 역직렬화 타입 불일치가 발생할 수 있습니다. 안전을 위해 레코드에 Serializable을 추가하세요. (위치: src/main/java/avengers/lion/mission/service/MetadataCacheService.java:62–67)
- public record VerificationMetadata( + public record VerificationMetadata( String imageUrl, // Cloudinary 이미지 URL String publicId, // Cloudinary public ID (삭제용) Long missionId, Long memberId - ) {} + ) implements java.io.Serializable {}검증 불가: 제공된 스크립트가 파일을 찾지 못해 레포 내 RedisTemplate/Serializer 설정을 확인하지 못했습니다. RedisTemplate/Serializer(예: GenericJackson2JsonRedisSerializer, JdkSerializationRedisSerializer 등) 설정 유무를 확인하고, 설정이 있다면 그에 맞춰 처리하세요.
16-16: 제네릭 구체화: RedisTemplate<String, VerificationMetadata> 사용 권장RedisTemplate<String, Object> 대신 제네릭을 구체화해 타입 안전성과 역직렬화 안정성 개선. RedisTemplate 빈 시그니처와 직렬화기 설정(예: Jackson2JsonRedisSerializer 등)이 일치하는지 수동 확인 필요.
- private final RedisTemplate<String, Object> redisTemplate; + private final RedisTemplate<String, VerificationMetadata> redisTemplate;src/main/java/avengers/lion/mission/controller/MissionController.java (1)
54-60: 메서드명 변경 확인.
getMissionReviewsScroll에서getMissionReviewsWithCursor로 메서드명이 변경되었고sortType파라미터가 추가되었습니다. 메서드 시그니처가 MissionApi 인터페이스와 일치하므로 변경사항이 올바릅니다.src/main/java/avengers/lion/mission/api/MissionApi.java (1)
140-145: API 메서드명 변경 및 파라미터 추가 확인.미션 리뷰 무한스크롤 API의 메서드명이
getMissionReviewsScroll에서getMissionReviewsWithCursor로 변경되었고,sortType파라미터가 추가되었습니다. 이는 API의 명확성을 높이고 정렬 옵션을 제공하는 개선사항입니다.src/main/java/avengers/lion/mission/repository/MissionRepository.java (2)
16-17: 쿼리 간소화 및 성능 최적화.기존의 복잡한 fetch join 쿼리에서 단순한 상태 기반 조회로 변경되었습니다. 이는 N+1 문제 해결을 위한 좋은 접근입니다.
19-20: 완료된 미션 ID 조회 메서드 추가.특정 회원의 완료된 미션 ID를
Set<Long>으로 반환하는 새로운 메서드가 추가되었습니다. 이는 서비스 레이어에서 미션 완료 상태를 효율적으로 확인할 수 있도록 하는 좋은 개선사항입니다.src/main/java/avengers/lion/mission/dto/response/MissionResponse.java (2)
7-7: MissionResponse에 tip 필드 추가.Mission 엔티티의 새로운 tip 필드가 MissionResponse DTO에도 올바르게 추가되었습니다. 필드 순서와 타입이 적절합니다.
18-18: 팩토리 메서드에서 tip 필드 매핑 확인.
mission.getTip()호출을 통해 Mission 엔티티의 tip 필드가 MissionResponse로 올바르게 매핑되고 있습니다.src/main/java/avengers/lion/mission/service/MissionService.java (4)
28-32: 서비스 클래스 주석 추가.클래스 레벨 주석이 추가되어 MissionService의 역할과 책임이 명확하게 설명되었습니다.
44-45: 리포지토리 메서드 호출 최적화.기존의 복잡한 fetch join 방식에서 단순한 상태 기반 조회와 별도의 완료된 미션 ID 조회로 분리되었습니다. 이는 N+1 문제를 해결하고 쿼리 성능을 최적화하는 좋은 접근입니다.
58-67: 프리뷰 페이징 로직 개선.미션 리뷰 프리뷰에서
hasNext판단 로직과nextId계산이 더욱 명확하게 개선되었습니다.reviews.getLast().getId()를 사용하여 마지막 리뷰의 ID를 가져오는 방식도 적절합니다.
85-85: 무한스크롤 페이징 로직 간소화.nextId 계산 로직이 한 줄로 간소화되어 가독성이 향상되었습니다. 조건부 할당 방식도 명확합니다.
src/main/java/avengers/lion/mission/domain/Mission.java (1)
47-48: tip 필드의 NULL 제약 검토 및 DB 마이그레이션 반영 필요
- src/main/java/avengers/lion/mission/domain/Mission.java (라인 47–48): @column(name = "tip") private String tip; — nullable 속성 미지정(기본 true → DB에서 NULL 허용).
- 사용처: src/main/java/avengers/lion/mission/dto/response/MissionResponse.java:18 에서 mission.getTip() 사용 확인.
- 저장소에서 "tip" 컬럼을 포함한 SQL 마이그레이션을 찾지 못함. 비즈니스상 NULL 허용 불가라면 엔티티에 @column(nullable = false) 추가하고, 기존 데이터 처리를 포함한 DB 마이그레이션(또는 기본값/데이터마이그레이션) 생성·적용할 것.
src/main/java/avengers/lion/mission/service/MissionVerifyService.java (1)
21-25: 모듈 목적/범위 주석 보강 좋습니다.서비스 역할이 명확해졌습니다.
| // Redis에 임시 코드-토큰 매핑 저장 (5분 TTL) | ||
| redisTemplate.opsForValue().set("auth_code:" + authCode, accessToken, 5, TimeUnit.MINUTES); | ||
| log.info("🔵 Redis에 임시 코드 저장 완료"); | ||
|
|
There was a problem hiding this comment.
DB 트랜잭션 커밋 전 Redis에 토큰 저장 — TOCTOU 위험
JPA 트랜잭션이 커밋되기 전에 액세스 토큰을 Redis에 저장합니다. 커밋 실패 시 존재하지 않는 사용자에 대한 토큰이 발급·노출될 수 있습니다. 트랜잭션 커밋 이후에 저장하도록 조정하세요.
수정 예:
- // 사용자 식별 정보를 이용해 토큰 발급
- String accessToken = tokenProvider.createAccessToken(member.getId(), member.getRole().name());
- // 임시 코드 생성 (UUID)
- String authCode = UUID.randomUUID().toString();
- // Redis에 임시 코드-토큰 매핑 저장 (5분 TTL)
- redisTemplate.opsForValue().set("auth_code:" + authCode, accessToken, 5, TimeUnit.MINUTES);
+ String accessToken = tokenProvider.createAccessToken(member.getId(), member.getRole().name());
+ String authCode = UUID.randomUUID().toString();
+ TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+ @Override public void afterCommit() {
+ redisTemplate.opsForValue()
+ .set("auth_code:" + authCode, accessToken, 5, TimeUnit.MINUTES);
+ }
+ });📝 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.
| // 사용자 식별 정보를 이용해 토큰 발급 | |
| String accessToken = tokenProvider.createAccessToken(member.getId(), member.getRole().name()); | |
| log.info("🔵 AccessToken 생성 완료 - Length: {}", accessToken.length()); | |
| // 임시 코드 생성 (UUID) | |
| String authCode = UUID.randomUUID().toString(); | |
| log.info("🔵 임시 코드 생성: {}", authCode); | |
| // Redis에 임시 코드-토큰 매핑 저장 (5분 TTL) | |
| redisTemplate.opsForValue().set("auth_code:" + authCode, accessToken, 5, TimeUnit.MINUTES); | |
| log.info("🔵 Redis에 임시 코드 저장 완료"); | |
| String accessToken = tokenProvider.createAccessToken(member.getId(), member.getRole().name()); | |
| String authCode = UUID.randomUUID().toString(); | |
| TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { | |
| @Override | |
| public void afterCommit() { | |
| redisTemplate.opsForValue() | |
| .set("auth_code:" + authCode, accessToken, 5, TimeUnit.MINUTES); | |
| } | |
| }); |
🤖 Prompt for AI Agents
In src/main/java/avengers/lion/global/OAuth2SuccessHandler.java around lines 63
to 71, the code stores the access token in Redis before the JPA transaction
commits which risks TOCTOU if the DB commit later fails; change the flow so the
Redis write happens only after successful transaction commit by either moving
the Redis set call into the service layer after the transaction completes or
register an afterCommit callback via
TransactionSynchronizationManager.registerSynchronization/TransactionSynchronizationAdapter
to perform redisTemplate.opsForValue().set("auth_code:"+authCode, accessToken,
5, TimeUnit.MINUTES) only in the afterCommit callback; ensure authCode is still
generated and propagated to the afterCommit handler and handle errors/logging
accordingly.
| redisTemplate.opsForValue().set("auth_code:" + authCode, accessToken, 5, TimeUnit.MINUTES); | ||
| log.info("🔵 Redis에 임시 코드 저장 완료"); | ||
|
|
||
|
|
||
| // 프론트엔드로 임시 코드와 함께 리다이렉트 | ||
| String frontendUrl = "http://localhost:5174/auth/callback?code=" + authCode; | ||
| getRedirectStrategy().sendRedirect(request, response, frontendUrl); |
There was a problem hiding this comment.
🧩 Analysis chain
임시 코드 교환 로직 점검 요청
auth_code 키로 저장된 토큰이 1회성으로 안전하게 교환되고 즉시 삭제되는지, TTL 5분이 문서/클라이언트와 일치하는지 확인 필요.
아래 스크립트로 교환/삭제 구현부를 찾아 확인해 주세요:
🏁 Script executed:
#!/bin/bash
# auth_code 조회·삭제 사용처 점검
rg -n -C3 -P 'auth_code:\s*\+|auth_code:' --type=java
rg -n -C2 -P 'opsForValue\(\)\.(get|getAndDelete|delete)\(' --type=javaLength of output: 4593
임시 auth_code 조회→삭제를 원자적으로 처리하도록 수정 필요
OAuth2SuccessHandler에서 auth_code를 5분 TTL로 저장하는 것(src/main/java/avengers/lion/global/OAuth2SuccessHandler.java:70)을 확인했고, AuthController.exchangeToken에서는 opsForValue().get(...)로 조회한 뒤 redisTemplate.delete(...)로 삭제(src/main/java/avengers/lion/auth/controller/AuthController.java:24,30)하고 있어 조회와 삭제가 분리되어 동시 요청 시 동일 코드가 중복 사용될 가능성이 있습니다.
- 조치: 조회+삭제를 원자적으로 처리하도록 변경 — 예: opsForValue().getAndDelete("auth_code:...") 사용(라이브러리 메서드 지원 여부 확인) 또는 Lua 스크립트/트랜잭션으로 GET+DEL 원자화. (수정 대상: src/main/java/avengers/lion/auth/controller/AuthController.java:24-30)
- 확인: TTL(5분)이 문서/프론트엔드 요구사항과 일치하는지 확인 및 필요 시 조정.
🤖 Prompt for AI Agents
In src/main/java/avengers/lion/auth/controller/AuthController.java around lines
24 to 30, the current flow separately calls redisTemplate.opsForValue().get(...)
then redisTemplate.delete(...), which can allow race conditions and double-use
of the same auth_code; change this to perform an atomic get-and-delete operation
— prefer using RedisOperations.opsForValue().getAndDelete(key) if available,
otherwise implement the atomicity via a Redis Lua script (GET+DEL) or a Redis
transaction that returns and removes the key in one atomic step; ensure error
handling remains and validate that the 5-minute TTL set elsewhere matches spec
and adjust if required.
|
🍓 |
What is this PR?🔍
Changes💻
getMissionReviewsScroll→getMissionReviewsWithCursor)Summary by CodeRabbit
New Features
Refactor