-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/#37-ocr 리뷰 관련 기능 구현 #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Walkthrough이 PR은 학생회 닉네임/프로필 이미지 변경 기능을 제거하고, 시간대별 추천 장소 엔드포인트를 폐기하며, OCR 기반 영수증 검증 기능을 추가하고, 제휴 신청 기능을 도입하는 대규모 도메인 간 리팩토링입니다. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 15
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
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/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java (1)
54-78:limit파라미터가 무시되고 하드코딩된 값 3이 사용됨.메서드 시그니처에서
limit파라미터를 받지만, 실제로는 Line 76에서.limit(3)으로 하드코딩되어 호출자가 전달한 값이 무시됩니다. 이는 API 계약을 위반하고 호출자에게 혼란을 줄 수 있습니다.두 가지 해결 방안이 있습니다:
- 항상 3개로 고정할 경우 파라미터를 제거
- 파라미터를 사용하되, 최대값을 제한
♻️ 옵션 1: 파라미터 제거 (항상 3개 고정 시)
- public List<String> fetchImages(String name, String address, int limit) { + public List<String> fetchImages(String name, String address) {♻️ 옵션 2: 파라미터 사용 (유연성 유지 시)
+ private static final int MAX_IMAGE_LIMIT = 10; + public List<String> fetchImages(String name, String address, int limit) { + int effectiveLimit = Math.min(Math.max(limit, 1), MAX_IMAGE_LIMIT); // ... List<String> imageUrls = photoRefs.stream() - .limit(3) + .limit(effectiveLimit) .map(this::buildPhotoUrl) .toList();src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java (1)
107-114: NullPointerException 위험
resolveTag메서드에서user.getSchool(),user.getCollege(),user.getMajor()가 null인 경우 NPE가 발생할 수 있습니다.PlaceService.resolveCouncils에서는 null 체크를 하지만, 이 메서드는 그러한 보호 없이 호출됩니다.🐛 null 안전성 확보
private String resolveTag(StudentCouncilPost post, User user) { CouncilType councilType = post.getWriter().getCouncilType(); return switch (councilType) { - case SCHOOL_COUNCIL -> user.getSchool().getSchoolName(); - case COLLEGE_COUNCIL -> user.getCollege().getCollegeName(); - case MAJOR_COUNCIL -> user.getMajor().getMajorName(); + case SCHOOL_COUNCIL -> user.getSchool() != null ? user.getSchool().getSchoolName() : ""; + case COLLEGE_COUNCIL -> user.getCollege() != null ? user.getCollege().getCollegeName() : ""; + case MAJOR_COUNCIL -> user.getMajor() != null ? user.getMajor().getMajorName() : ""; }; }
🤖 Fix all issues with AI agents
In
`@src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java`:
- Around line 286-308: Remove the `@EntityGraph`(attributePaths = {"places"})
annotation from StudentCouncilPostRepository because `@EntityGraph` is ignored for
native queries (nativeQuery = true) and the attribute name is incorrect for the
entity (the field is place, not places); either delete the annotation or convert
the native query to a JPQL query so `@EntityGraph` on StudentCouncilPostRepository
can be effective (or change attributePaths to "place" if you switch to JPQL).
In
`@src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java`:
- Around line 133-134: The lookup uses the wrong key: averageStarMap stores
averages by placeId but the code does averageStarMap.getOrDefault(post.getId(),
0.0). Replace the lookup key with the place ID for the post (e.g.,
post.getPlace().getId() or post.getPlaceId() depending on the Post API) so
averageStar = averageStarMap.getOrDefault(placeId, 0.0); ensure you reference
the same placeId accessor used elsewhere in PartnershipPlaceService.
In
`@src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java`:
- Around line 144-151: The current findByPlaceAndCouncil().orElseGet(() ->
partnershipSuggestionRepository.save(CouncilPartnershipSuggestion.create(place,
council))) pattern can race and create duplicate CouncilPartnershipSuggestion
rows; either add a DB unique constraint on (place_id, student_council_id) and
catch DataIntegrityViolationException around the save, or mirror the
UserPartnershipSuggestion approach by adding a
partnershipSuggestionRepository.existsByPlaceAndCouncil(place, council)
pre-check and only calling save when !exists (then fetch the entity), updating
the code around findByPlaceAndCouncil, CouncilPartnershipSuggestion.create, and
demand.increase() to use the chosen strategy to prevent duplicates.
In
`@src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java`:
- Around line 19-21: The ReviewRequest.star field in class ReviewRequest lacks
range validation; add `@DecimalMin`(value = "0.0", inclusive = true) and
`@DecimalMax`(value = "5.0", inclusive = true) annotations to the Double star
field (keeping the existing `@NotNull` and `@Schema`) and add the corresponding
imports (javax.validation.constraints.DecimalMin and DecimalMax) so out-of-range
values like -100.0 or 999.0 are rejected.
In
`@src/main/java/com/campus/campus/domain/review/application/exception/NotPartnershipReceiptException.java`:
- Around line 1-9: The NotPartnershipReceiptException class uses ErrorCode but
lacks its import; add an import for
com.campus.campus.domain.place.application.exception.ErrorCode at the top of the
file so NotPartnershipReceiptException (which extends ApplicationException) can
reference ErrorCode.NOT_PARTNERSHIP_RECEIPT_ERROR without compile errors.
In
`@src/main/java/com/campus/campus/domain/review/application/exception/ReceiptFileConvertException.java`:
- Around line 5-8: The class ReceiptFileConvertException is missing the import
for ErrorCode, causing a compile error; add the import for
com.campus.campus.domain.place.application.exception.ErrorCode at the top of
ReceiptFileConvertException so the reference to ErrorCode in the constructor
(super(ErrorCode.RECEIPT_FILE_CONVERT_ERROR)) resolves correctly and the class
compiles.
In
`@src/main/java/com/campus/campus/domain/review/application/exception/ReceiptImageFormatException.java`:
- Around line 1-9: ReceiptImageFormatException currently references
ErrorCode.RECEIPT_FILE_TYPE_ERROR but misses the import, causing a compile
error; add the missing import for ErrorCode (the same fix applied to
NotPartnershipReceiptException) at the top of ReceiptImageFormatException so the
class can compile and extend ApplicationException correctly.
In
`@src/main/java/com/campus/campus/domain/review/application/service/OcrService.java`:
- Around line 114-118: The date parsing using DateTimeFormatter.BASIC_ISO_DATE
in OcrService (the LocalDate paymentDate Optional chain that maps
receipt.paymentInfo() -> PaymentInfo::date -> TextField::text ->
LocalDate.parse(...)) can throw DateTimeParseException for formats like
yyyy-MM-dd or yyyy/MM/dd; add a private helper method (e.g., parseDate(String
text)) that returns null on invalid/blank input, normalizes the string to digits
only, attempts parse with BASIC_ISO_DATE inside a try/catch, logs a warning on
failure, and then replace the inline LocalDate.parse(...) call with a map to
this parseDate helper so invalid OCR date formats don't crash processing.
In
`@src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java`:
- Around line 286-289: The code comments mention setting the review's isVerified
flag to true but there's no implementation; update the Review entity (the local
variable post) to set isVerified to true (e.g., post.setIsVerified(true) or
post.markVerified()) and persist the change via the appropriate repository/save
method (e.g., reviewRepository.save(post)) before calling
getAverageOfStars(placeId) and placeMapper.toReviewPartnerResponse(post,
post.getPlace(), averageStar); ensure you reference ReviewService and the post
variable so the update and save occur in this method (or call a new
ReviewService.updateVerified(reviewId) helper if you prefer extracting the
logic).
In
`@src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.java`:
- Around line 15-22: ReviewImage currently uses `@SuperBuilder` but does not
extend any superclass and has dropped BaseEntity (losing audit fields); change
`@SuperBuilder` to Lombok's `@Builder` and make ReviewImage extend BaseEntity so
createdAt/updatedAt audit fields are restored; update constructors/annotations
as needed (e.g., keep `@NoArgsConstructor`(access = AccessLevel.PROTECTED), adjust
`@AllArgsConstructor` visibility) and ensure any builder/constructor signatures
reflect the inherited BaseEntity fields and relationships used by ReviewImage.
In
`@src/main/java/com/campus/campus/domain/review/infrastructure/ocr/ClovaOcrClient.java`:
- Around line 36-42: ClovaOcrClient currently calls
extractFormat(originalFilename) but ignores its return and hardcodes
image.put("format","png"); fix by capturing the returned format (e.g., String
format = extractFormat(originalFilename)) and use that value when building the
JSON body (replace the hardcoded "png" in image.put("format", ...)); update any
related variable names and validation if extractFormat can return null/empty to
fall back to a safe default.
In
`@src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java`:
- Around line 91-100: The upload method mixes `@RequestPart` MultipartFile file
with `@Valid` `@RequestBody` SavedPlaceInfo placeInfo which will fail for
multipart/form-data; change placeInfo to `@RequestPart` and have clients send the
JSON-encoded SavedPlaceInfo as a part (or accept a String part and deserialize
it) so both parts are handled as multipart. Also address the auth mismatch:
either make `@CurrentUserId` optional by using `@CurrentUserId`(required=false) on
the upload method if this endpoint must remain permitAll, or remove this path
from PermitUrlConfig so authentication is required; update the upload method
signature accordingly (method name: upload, parameter: `@CurrentUserId` Long
userId) to match the chosen behavior.
In
`@src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java`:
- Around line 137-157: getToken currently makes the Kakao token request but does
not handle HTTP errors or null responses; update getToken in KakaoOauthService
to validate the response and surface failures by (1) wrapping the RestClient
call in try/catch to catch client/transport errors (e.g.,
RestClientException/RuntimeException), (2) check the returned KakaoTokenResponse
for null or missing expected fields, and (3) throw a clear custom exception
(e.g., KakaoOauthException) with contextual details (include authorizationCode
or response/error body) similar to how unlink() handles errors; ensure to
reference KakaoTokenResponse, kakaoOauthProperty and KAUTH_BASE_URL when
logging/constructing the error so callers can handle failures deterministically.
In `@src/main/java/com/campus/campus/global/config/ClovaOcrConfig.java`:
- Around line 10-13: The clovaOcrRestTemplate bean currently returns a plain
RestTemplate with no timeouts; update clovaOcrRestTemplate to accept a
RestTemplateBuilder parameter and configure sensible timeouts (e.g.,
setConnectTimeout and setReadTimeout) before calling build() so external Clova
OCR calls won't block threads indefinitely; locate the clovaOcrRestTemplate
method in ClovaOcrConfig and replace the new RestTemplate() return with a
RestTemplate built via
RestTemplateBuilder.setConnectTimeout(...).setReadTimeout(...).build().
In `@src/main/java/com/campus/campus/global/config/PermitUrlConfig.java`:
- Around line 36-37: PermitUrlConfig currently lists "/reviews/receipt-ocr" as a
public URL but the controller handling that endpoint requires an authenticated
user via `@CurrentUserId` (required=true) and throws UnAuthorizedException; remove
"/reviews/receipt-ocr" from the PermitUrlConfig public URL array so Spring
Security will require authentication for that endpoint, aligning security
configuration with the controller's authentication requirement.
🟡 Minor comments (6)
src/main/java/com/campus/campus/domain/user/application/dto/response/ChangeProfileImageResponse.java-5-15 (1)
5-15: Line 9의 Schema 설명에서 괄호를 닫아주세요.Schema description에 닫는 괄호가 누락되었습니다:
수정 사항
- `@Schema`(description = "유저 닉네임(campusNickname 설정했으면 campusNickname, 아니면, nickname") + `@Schema`(description = "유저 닉네임(campusNickname 설정했으면 campusNickname, 아니면 nickname)")참고: 이 DTO는 UserMapper의
toChangeProfileImageResponse()메서드에서 사용 중이므로 제거할 수 없습니다.src/main/java/com/campus/campus/domain/user/application/dto/response/ChangeUserAcademicResponse.java-1-26 (1)
1-26: 학사 정보 변경 기능이 제거되었는데 Response DTO와 매퍼 메서드가 남아있습니다.
ChangeUserAcademicRequest삭제,UserService.updateUserAcademic()메서드 제거, 관련 엔드포인트 제거로 학사 정보 변경 기능이 완전히 제거되었지만, 이 Response DTO와UserMapper.toChangeUserAcademicResponse()매퍼 메서드는 그대로 남아있습니다. 이 매퍼 메서드는 코드베이스 어디에서도 호출되지 않으므로, 불필요한 유지보수 부담을 피하기 위해ChangeUserAcademicResponse.java와UserMapper의toChangeUserAcademicResponse()메서드를 함께 제거하기 바랍니다.src/main/java/com/campus/campus/domain/place/application/dto/response/PlaceDetailView.java-19-23 (1)
19-23: primitive 타입과 boxed 타입 혼용의 일관성을 개선하세요.
latitude/longitude는Double(boxed)을 사용하고,star/distance는double(primitive)을 사용하고 있습니다. 두 구현체(PlaceDetailResponse, PartnershipDetailResponse)가 모두 record로 정의되어 있으므로 생성 시 primitivedouble값이 반드시 필요하며, 현재 구조상 null 처리의 위험은 없습니다. 다만, 리뷰가 없을 때의 기본값(예: 0.0)이 의도적으로 설정되는지, 그리고 인터페이스 설계 측면에서 boxed 타입(Double)으로 통일하는 것이 더 명확할 수 있습니다.src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptResultDto.java-6-11 (1)
6-11: 필드명 오타 의심:totalPlace→totalPrice영수증 OCR 결과 DTO에서
totalPlace는totalPrice의 오타로 보입니다. PR 목표에서 영수증에서 금액과 날짜를 추출한다고 명시되어 있으므로, 해당 필드명을 확인해 주세요.🔧 수정 제안
public record ReceiptResultDto( String storeName, - String totalPlace, + String totalPrice, LocalDate paymentDate, List<ReceiptItemDto> items ) { }src/main/java/com/campus/campus/domain/review/infrastructure/ocr/ClovaOcrClient.java-42-42 (1)
42-42: 하드코딩된 테스트 값 제거 필요
"receipt_test2"는 테스트용 값으로 보입니다. 의미 있는 이름(예: 파일명 또는 UUID)으로 변경하거나, 필요 없다면 제거해 주세요.🔧 수정 제안
image.put("format", format); image.put("data", base64Image); -image.put("name", "receipt_test2"); +image.put("name", originalFilename != null ? originalFilename : "receipt");src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java-58-58 (1)
58-58: 메서드 이름 오타 수정 필요:findALl→findAll
findALlByPlace_PlaceId에서L이 대문자입니다. Spring Data JPA 쿼리 메서드 명명 규칙에 맞게findAllByPlace_PlaceId로 수정해야 합니다.🔧 수정 제안
- List<Review> findALlByPlace_PlaceId(long placeId); + List<Review> findAllByPlace_PlaceId(long placeId);
🧹 Nitpick comments (18)
src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java (1)
138-138: RestClient 인스턴스 재사용 고려매 요청마다
RestClient.create()를 호출하고 있습니다. 빈으로 주입받거나 필드로 재사용하면 약간의 성능 향상을 얻을 수 있습니다. 기존 코드(getUserInfo,unlink)도 동일한 패턴이므로, 추후 리팩토링 시 함께 개선하면 좋겠습니다.src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptItemDto.java (1)
3-7: LGTM!OCR 영수증 항목을 위한 간단하고 깔끔한 record 정의입니다.
다른 DTO들과의 일관성을 위해 Swagger 문서화 어노테이션 추가를 고려해 보세요:
📝 선택적 개선 제안
+import io.swagger.v3.oas.annotations.media.Schema; + public record ReceiptItemDto( + `@Schema`(description = "상품명") String name, + `@Schema`(description = "가격") String price ) { }src/main/java/com/campus/campus/domain/place/domain/entity/CouncilPartnershipSuggestion.java (2)
40-44:requestCount초기값 설정 방식 검토.
@SuperBuilder사용 시 빌더를 통해 엔티티를 생성하면requestCount가 기본값 0으로 초기화되지 않을 수 있습니다.create()팩토리 메서드만 사용한다면 문제없지만, 빌더 사용 시에도 일관된 초기화를 원한다면@Builder.Default추가를 고려하세요.💡 제안 (선택 사항)
+ `@Builder.Default` private int requestCount;
56-60: 파일 끝 불필요한 빈 줄 정리.파일 끝에 불필요한 빈 줄이 있습니다.
src/main/java/com/campus/campus/domain/place/domain/entity/UserPartnershipSuggestion.java (1)
40-45: 팩토리 메서드 구현이 적절합니다.
create정적 팩토리 메서드가 간결하게 구현되었습니다.@SuperBuilder가 선언되어 있으므로, 일관성을 위해 빌더 패턴 사용도 고려해볼 수 있습니다.♻️ 빌더 패턴 사용 대안
public static UserPartnershipSuggestion create(User user, Place place) { - UserPartnershipSuggestion s = new UserPartnershipSuggestion(); - s.user = user; - s.place = place; - return s; + return UserPartnershipSuggestion.builder() + .user(user) + .place(place) + .build(); }src/main/resources/application-local.yml (1)
74-77: Clova OCR invoke-url을 환경 변수로 관리하는 것을 권장합니다.
invoke-url에 하드코딩된 URL에는 API Gateway ID(oprno6zgu9)와 도메인 ID(49413)가 포함되어 있습니다. 이 값들은 환경별로 다를 수 있으며, 실수로 커밋될 경우 보안상 노출 위험이 있습니다.♻️ 환경 변수로 변경 제안
clova: ocr: - invoke-url: https://oprno6zgu9.apigw.ntruss.com/custom/v1/49413/b8f236bbccbb9b0008d8d0485d9028faba366671ac72ccab91d2df2bb9c96001/document/receipt + invoke-url: ${CLOVA_OCR_INVOKE_URL} secret-key: ${OCR_KEY}src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptOcrResponse.java (1)
5-10: 파일 끝 불필요한 공백 라인.파일 끝에 불필요한 빈 줄이 있습니다. 코드 일관성을 위해 제거하는 것이 좋습니다.
♻️ 제안된 수정
public record ReceiptOcrResponse( List<ImageResult> images ) { } - -src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewPartnerResponse.java (1)
3-9:star필드 타입 일관성 검토 권장코드베이스의 다른 response DTO들(
WriteReviewResponse,SearchPlaceInfoResponse등)에서는Doublewrapper 타입을 사용하고 있습니다. primitivedouble은 기본값이 0.0이므로 "평점 없음"과 "0점"을 구분할 수 없습니다.일관성을 위해
Double로 변경하는 것을 고려해 보세요.♻️ 제안하는 변경
public record ReviewPartnerResponse( String placeName, String placeCategory, String council, - double star, + Double star, String title ) { }src/main/java/com/campus/campus/domain/place/application/dto/response/PlaceDetailResponse.java (1)
7-23: 타입 일관성 검토 필요:star필드
PartnershipResponse에서는Double star(nullable)를 사용하지만, 이 DTO에서는double star(primitive)를 사용하고 있습니다. 리뷰가 없는 경우 0.0으로 기본값이 설정되는 것이 의도된 동작이라면 문제없지만, 프로젝트 전반의 타입 일관성을 위해 확인이 필요합니다.src/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PartnershipDetailResponse.java (1)
26-27:reviewSize필드 - 선택적 개선 제안
reviewSize가reviews.size()와 동일한 값이라면 중복입니다. 다만, 전체 리뷰 수(DB 기준)와 반환된 리뷰 수(예: 3건)가 다르다면 의미 있는 필드입니다. 필드명을totalReviewCount등으로 명확히 하는 것을 고려해 보세요.src/main/java/com/campus/campus/domain/review/infrastructure/ocr/ClovaOcrClient.java (1)
58-61: 외부 API 호출 - 에러 핸들링 권장
RestTemplate호출 시 네트워크 오류, 타임아웃, 4xx/5xx 응답에 대한 처리가 없습니다. 예외가 상위 레이어로 전파되어 적절히 처리되는지 확인하거나, 명시적인 에러 핸들링을 추가하는 것을 권장합니다.♻️ 에러 핸들링 추가 예시
+import org.springframework.web.client.RestClientException; +import com.campus.campus.domain.review.application.exception.OcrProcessingException; //호출 +try { ResponseEntity<String> response = restTemplate.postForEntity(invokeUrl, request, String.class); return response.getBody(); +} catch (RestClientException e) { + throw new OcrProcessingException("OCR API 호출 실패", e); +}src/main/java/com/campus/campus/domain/review/application/service/OcrService.java (2)
43-53: 파일 타입 검증 누락업로드된 파일이 이미지인지 검증하지 않고 있습니다. 악의적인 파일 업로드를 방지하기 위해 Content-Type 또는 파일 확장자 검증을 추가하는 것이 좋습니다.
♻️ 파일 타입 검증 추가 제안
public ReviewPartnerResponse processReceipt(MultipartFile file, Long userId, SavedPlaceInfo placeInfo) { + String contentType = file.getContentType(); + if (contentType == null || !contentType.startsWith("image/")) { + throw new ReceiptFileConvertException(); + } + Place place = placeService.findOrCreatePlace(placeInfo);
66-74: 일반 Exception 캐치 범위 축소 권장
JsonProcessingException을 명시적으로 캐치하면 예상치 못한 예외(예: NPE)가 숨겨지는 것을 방지할 수 있습니다.♻️ 명시적 예외 타입 사용
private ReceiptOcrResponse parse(String json) { try { log.debug("[OCR PARSE INPUT] {}", json); return objectMapper.readValue(json, ReceiptOcrResponse.class); - } catch (Exception e) { + } catch (JsonProcessingException e) { log.error("[OCR PARSE FAILED] raw={}", json, e); throw new ReceiptOcrFailedException(); } }src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java (1)
176-230: 중복 코드:getPartnershipDetailInternal과getPartnershipDetail로직이 거의 동일합니다.두 메서드가 동일한 로직을 수행하고 있습니다.
getPartnershipDetail에서getPartnershipDetailInternal을 호출하도록 리팩토링하면 중복을 제거할 수 있습니다.♻️ 중복 제거 제안
public PartnershipDetailResponse getPartnershipDetail( Long postId, Long userId, double userLat, double userLng ) { User user = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); StudentCouncilPost post = studentCouncilPostRepository.findById(postId) .orElseThrow(PostNotFoundException::new); - Place place = post.getPlace(); - - double distance = calculateDistance(place, userLat, userLng); - double averageStar = calculateAverageStar(place); - - return placeMapper.toPartnershipResponse( - user, - post, - place, - isLiked(place, user), - getImgUrls(post), - distance, - averageStar, - reviewService.getReviewSummaryList(place.getPlaceId()), - reviewService.getReviewCount(place.getPlaceId()) - ); + return getPartnershipDetailInternal(post, user, userLat, userLng); }src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (1)
292-306: 성능 이슈: 동일한 쿼리가 중복 실행됩니다.
getAverageOfStars와getReviewCount가 각각findALlByPlace_PlaceId를 호출합니다. 두 메서드가 함께 호출되는 경우(예:PartnershipPlaceService), 동일한 데이터를 두 번 조회하게 됩니다.또한
findALlByPlace_PlaceId의 오타(ALl→All)도 함께 수정이 필요합니다.♻️ 통합 메서드 제안
`@Transactional`(readOnly = true) public ReviewStats getReviewStats(Long placeId) { List<Review> reviews = reviewRepository.findAllByPlace_PlaceId(placeId); double averageStar = reviews.stream() .mapToDouble(Review::getStar) .average() .orElse(0.0); return new ReviewStats(averageStar, reviews.size()); } public record ReviewStats(double averageStar, int count) {}src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
267-289: N+1 쿼리 우려: 3개의 개별 DB 호출
resolveCouncils메서드에서 major, college, school 각각에 대해 별도 쿼리를 수행합니다. 현재 로직은 정확하지만, 단일 쿼리로 최적화할 수 있습니다.♻️ 선택적 최적화: IN 쿼리로 통합
Repository에 커스텀 메서드 추가:
`@Query`("SELECT sc FROM StudentCouncil sc WHERE " + "sc.major.majorId = :majorId OR " + "sc.college.collegeId = :collegeId OR " + "sc.school.schoolId = :schoolId") List<StudentCouncil> findByMajorIdOrCollegeIdOrSchoolId( `@Param`("majorId") Long majorId, `@Param`("collegeId") Long collegeId, `@Param`("schoolId") Long schoolId);src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java (1)
78-95: 사용되지 않는user파라미터
toPlaceDetailResponse메서드에서User user파라미터가 선언되어 있지만 메서드 내부에서 사용되지 않습니다.♻️ 사용하지 않는 파라미터 제거
- public PlaceDetailResponse toPlaceDetailResponse(User user, Place place, boolean isLiked, + public PlaceDetailResponse toPlaceDetailResponse(Place place, boolean isLiked, List<String> imgUrls, double distance, Double averageStar, List<SimpleReviewResponse> reviews, Integer size) {만약 향후 태그 또는 사용자 관련 정보가 추가될 예정이라면 주석으로 명시해주세요.
src/main/java/com/campus/campus/domain/place/presentation/PlaceController.java (1)
217-225: Request DTO로 Response DTO 사용
SavedPlaceInfo는 네이밍과 다른 곳에서의 사용 패턴을 보면 응답용 DTO로 보입니다. 요청 바디로 사용하는 것은 API 계층 분리 원칙에 맞지 않을 수 있습니다.전용 요청 DTO(
PartnershipSuggestionRequest등)를 만들어 요청/응답 DTO를 분리하는 것을 고려해주세요. 현재 동작에는 문제가 없지만, 향후 요청과 응답에 다른 필드가 필요할 때 유연성이 떨어질 수 있습니다.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (94)
.gitignoresrc/main/java/com/campus/campus/domain/council/application/dto/request/StudentCouncilChangeProfileImageRequest.javasrc/main/java/com/campus/campus/domain/council/application/dto/request/StudentCouncilNicknameRequest.javasrc/main/java/com/campus/campus/domain/council/application/dto/response/StudentCouncilChangeProfileImageResponse.javasrc/main/java/com/campus/campus/domain/council/application/dto/response/StudentCouncilLoginResponse.javasrc/main/java/com/campus/campus/domain/council/application/dto/response/StudentCouncilNicknameResponse.javasrc/main/java/com/campus/campus/domain/council/application/mapper/StudentCouncilLoginMapper.javasrc/main/java/com/campus/campus/domain/council/application/service/CouncilLoginService.javasrc/main/java/com/campus/campus/domain/council/application/service/CouncilService.javasrc/main/java/com/campus/campus/domain/council/domain/entity/StudentCouncil.javasrc/main/java/com/campus/campus/domain/council/domain/repository/StudentCouncilRepository.javasrc/main/java/com/campus/campus/domain/council/presentation/StudentCouncilController.javasrc/main/java/com/campus/campus/domain/council/presentation/StudentCouncilResponseCode.javasrc/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostForUserResponse.javasrc/main/java/com/campus/campus/domain/councilpost/application/dto/response/PostListItemResponse.javasrc/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.javasrc/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.javasrc/main/java/com/campus/campus/domain/councilpost/domain/entity/StudentCouncilPost.javasrc/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/PlaceDetailResponse.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/PlaceDetailView.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/RecommendNearByPlaceResponse.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/RecommendPartnershipPlaceResponse.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/RecommendPlaceByTimeResponse.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/SearchPartnershipInfoResponse.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/SearchPlaceInfoResponse.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PartnershipDetailResponse.javasrc/main/java/com/campus/campus/domain/place/application/exception/AlreadySuggestedPartnershipException.javasrc/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.javasrc/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.javasrc/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.javasrc/main/java/com/campus/campus/domain/place/application/service/PlaceService.javasrc/main/java/com/campus/campus/domain/place/application/service/RedisPlaceCacheService.javasrc/main/java/com/campus/campus/domain/place/domain/entity/CouncilPartnershipSuggestion.javasrc/main/java/com/campus/campus/domain/place/domain/entity/UserPartnershipSuggestion.javasrc/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.javasrc/main/java/com/campus/campus/domain/place/domain/repository/PartnershipSuggestionRepository.javasrc/main/java/com/campus/campus/domain/place/domain/repository/PlaceImagesRepository.javasrc/main/java/com/campus/campus/domain/place/domain/repository/PlaceRepository.javasrc/main/java/com/campus/campus/domain/place/domain/repository/UserPartnershipSuggestionRepository.javasrc/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.javasrc/main/java/com/campus/campus/domain/place/presentation/PlaceController.javasrc/main/java/com/campus/campus/domain/place/presentation/PlaceResponseCode.javasrc/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/PlaceStarAvgRow.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResponse.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResult.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ReviewPartnerResponse.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ReviewResponse.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/SimpleReviewResponse.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/WriteReviewResponse.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ImageResult.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ocr/PaymentInfo.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ocr/PriceInfo.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptItemDto.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptOcrItem.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptOcrResponse.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptResult.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptResultDto.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptWrapper.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ocr/StoreInfo.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ocr/SubResult.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ocr/TextField.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ocr/TotalPrice.javasrc/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.javasrc/main/java/com/campus/campus/domain/review/application/exception/NotPartnershipReceiptException.javasrc/main/java/com/campus/campus/domain/review/application/exception/ReceiptFileConvertException.javasrc/main/java/com/campus/campus/domain/review/application/exception/ReceiptImageFormatException.javasrc/main/java/com/campus/campus/domain/review/application/exception/ReceiptOcrFailedException.javasrc/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.javasrc/main/java/com/campus/campus/domain/review/application/service/OcrService.javasrc/main/java/com/campus/campus/domain/review/application/service/ReviewService.javasrc/main/java/com/campus/campus/domain/review/domain/entity/Review.javasrc/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.javasrc/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.javasrc/main/java/com/campus/campus/domain/review/infrastructure/ocr/ClovaOcrClient.javasrc/main/java/com/campus/campus/domain/review/presentation/ReviewController.javasrc/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.javasrc/main/java/com/campus/campus/domain/user/application/dto/request/ChangeProfileImageRequest.javasrc/main/java/com/campus/campus/domain/user/application/dto/request/ChangeUserAcademicRequest.javasrc/main/java/com/campus/campus/domain/user/application/dto/response/ChangeProfileImageResponse.javasrc/main/java/com/campus/campus/domain/user/application/dto/response/ChangeUserAcademicResponse.javasrc/main/java/com/campus/campus/domain/user/application/exception/AcademicInfoUpdateRestrictionException.javasrc/main/java/com/campus/campus/domain/user/application/exception/ErrorCode.javasrc/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.javasrc/main/java/com/campus/campus/domain/user/application/service/UserService.javasrc/main/java/com/campus/campus/domain/user/domain/entity/User.javasrc/main/java/com/campus/campus/domain/user/presentation/AuthController.javasrc/main/java/com/campus/campus/domain/user/presentation/UserController.javasrc/main/java/com/campus/campus/domain/user/presentation/UserResponseCode.javasrc/main/java/com/campus/campus/global/config/ClovaOcrConfig.javasrc/main/java/com/campus/campus/global/config/PermitUrlConfig.javasrc/main/resources/application-local.yml
💤 Files with no reviewable changes (23)
- src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostForUserResponse.java
- src/main/java/com/campus/campus/domain/council/application/dto/response/StudentCouncilNicknameResponse.java
- .gitignore
- src/main/java/com/campus/campus/domain/place/application/dto/response/RecommendPartnershipPlaceResponse.java
- src/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.java
- src/main/java/com/campus/campus/domain/place/application/dto/response/RecommendNearByPlaceResponse.java
- src/main/java/com/campus/campus/domain/council/application/dto/request/StudentCouncilChangeProfileImageRequest.java
- src/main/java/com/campus/campus/domain/place/application/service/RedisPlaceCacheService.java
- src/main/java/com/campus/campus/domain/council/application/service/CouncilService.java
- src/main/java/com/campus/campus/domain/council/application/dto/response/StudentCouncilChangeProfileImageResponse.java
- src/main/java/com/campus/campus/domain/user/presentation/UserController.java
- src/main/java/com/campus/campus/domain/council/application/dto/request/StudentCouncilNicknameRequest.java
- src/main/java/com/campus/campus/domain/place/application/dto/response/SearchPlaceInfoResponse.java
- src/main/java/com/campus/campus/domain/user/application/exception/AcademicInfoUpdateRestrictionException.java
- src/main/java/com/campus/campus/domain/place/application/dto/response/SearchPartnershipInfoResponse.java
- src/main/java/com/campus/campus/domain/council/presentation/StudentCouncilResponseCode.java
- src/main/java/com/campus/campus/domain/councilpost/domain/entity/StudentCouncilPost.java
- src/main/java/com/campus/campus/domain/user/application/dto/request/ChangeProfileImageRequest.java
- src/main/java/com/campus/campus/domain/review/application/dto/response/WriteReviewResponse.java
- src/main/java/com/campus/campus/domain/user/application/dto/request/ChangeUserAcademicRequest.java
- src/main/java/com/campus/campus/domain/user/application/service/UserService.java
- src/main/java/com/campus/campus/domain/council/presentation/StudentCouncilController.java
- src/main/java/com/campus/campus/domain/place/application/dto/response/RecommendPlaceByTimeResponse.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-18T09:42:14.128Z
Learnt from: 1winhyun
Repo: our-campUS/our-campUS-BE PR: 6
File: src/test/resources/application-test.yml:15-16
Timestamp: 2025-11-18T09:42:14.128Z
Learning: In the our-campUS-BE project, the test configuration file (src/test/resources/application-test.yml) uses hardcoded dummy values for OAuth and JWT secrets, as these are arbitrary test values and not real credentials.
Applied to files:
src/main/resources/application-local.yml
🧬 Code graph analysis (32)
src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ImageResult.java (7)
src/main/java/com/campus/campus/domain/user/application/dto/request/ChangeProfileImageRequest.java (1)
ChangeProfileImageRequest(5-9)src/main/java/com/campus/campus/domain/council/application/dto/response/StudentCouncilChangeProfileImageResponse.java (1)
StudentCouncilChangeProfileImageResponse(5-15)src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewRankingResponse.java (1)
ReviewRankingResponse(3-8)src/main/java/com/campus/campus/domain/place/application/dto/response/google/GooglePlaceDetailResponse.java (1)
GooglePlaceDetailResponse(5-12)src/main/java/com/campus/campus/domain/council/application/dto/request/StudentCouncilChangeProfileImageRequest.java (1)
StudentCouncilChangeProfileImageRequest(5-9)src/main/java/com/campus/campus/domain/stamp/application/dto/response/StampReviewResponse.java (1)
StampReviewResponse(7-17)src/main/java/com/campus/campus/domain/councilnotice/application/dto/request/NoticeRequest.java (1)
NoticeRequest(5-10)
src/main/java/com/campus/campus/domain/councilpost/application/dto/response/PostListItemResponse.java (2)
src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetLikedPostResponse.java (1)
JsonInclude(9-29)src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostForUserResponse.java (1)
Builder(13-39)
src/main/java/com/campus/campus/domain/user/application/dto/response/ChangeUserAcademicResponse.java (3)
src/main/java/com/campus/campus/domain/user/application/dto/request/ChangeUserAcademicRequest.java (1)
ChangeUserAcademicRequest(6-15)src/main/java/com/campus/campus/domain/user/application/mapper/UserMapper.java (1)
toChangeUserAcademicResponse(52-61)src/main/java/com/campus/campus/domain/user/application/service/UserService.java (1)
Transactional(93-119)
src/main/java/com/campus/campus/domain/review/application/exception/NotPartnershipReceiptException.java (3)
src/main/java/com/campus/campus/domain/review/application/exception/NotUserWriterException.java (2)
NotUserWriterException(6-8)NotUserWriterException(5-9)src/main/java/com/campus/campus/domain/review/application/exception/ReviewNotFoundException.java (1)
ReviewNotFoundException(5-9)src/main/java/com/campus/campus/domain/councilnotice/application/exception/NoticeNotFoundException.java (1)
NoticeNotFoundException(5-9)
src/main/java/com/campus/campus/domain/review/application/exception/ReceiptImageFormatException.java (9)
src/main/java/com/campus/campus/domain/councilnotice/application/exception/NoticeImageLimitExceededException.java (1)
NoticeImageLimitExceededException(5-9)src/main/java/com/campus/campus/domain/review/application/exception/ReviewNotFoundException.java (1)
ReviewNotFoundException(5-9)src/main/java/com/campus/campus/domain/councilpost/application/exception/ThumbnailRequiredException.java (1)
ThumbnailRequiredException(5-9)src/main/java/com/campus/campus/domain/councilpost/application/exception/PostImageLimitExceededException.java (1)
PostImageLimitExceededException(5-9)src/main/java/com/campus/campus/domain/council/application/exception/EmailAlreadyExistsException.java (1)
EmailAlreadyExistsException(5-9)src/main/java/com/campus/campus/domain/councilnotice/application/exception/NoticeOciImageDeleteFailedException.java (2)
NoticeOciImageDeleteFailedException(5-9)NoticeOciImageDeleteFailedException(6-8)src/main/java/com/campus/campus/domain/user/application/exception/AcademicInfoUpdateRestrictionException.java (1)
AcademicInfoUpdateRestrictionException(5-9)src/main/java/com/campus/campus/domain/review/application/exception/NotUserWriterException.java (1)
NotUserWriterException(6-8)src/main/java/com/campus/campus/domain/place/application/exception/PlaceCreationException.java (1)
PlaceCreationException(5-9)
src/main/java/com/campus/campus/domain/review/application/exception/ReceiptFileConvertException.java (9)
src/main/java/com/campus/campus/domain/review/application/exception/ReviewNotFoundException.java (1)
ReviewNotFoundException(5-9)src/main/java/com/campus/campus/domain/council/application/exception/EmailAlreadyExistsException.java (1)
EmailAlreadyExistsException(5-9)src/main/java/com/campus/campus/domain/review/application/exception/NotUserWriterException.java (2)
NotUserWriterException(6-8)NotUserWriterException(5-9)src/main/java/com/campus/campus/domain/user/application/exception/AcademicInfoUpdateRestrictionException.java (1)
AcademicInfoUpdateRestrictionException(5-9)src/main/java/com/campus/campus/domain/councilnotice/application/exception/NoticeImageLimitExceededException.java (1)
NoticeImageLimitExceededException(5-9)src/main/java/com/campus/campus/domain/place/application/exception/PlaceCreationException.java (1)
PlaceCreationException(5-9)src/main/java/com/campus/campus/domain/councilnotice/application/exception/NotNoticeWriterException.java (1)
NotNoticeWriterException(5-9)src/main/java/com/campus/campus/domain/councilnotice/application/exception/NoticeOciImageDeleteFailedException.java (1)
NoticeOciImageDeleteFailedException(5-9)src/main/java/com/campus/campus/domain/council/application/exception/NewPasswordIsCurrentPasswordException.java (1)
NewPasswordIsCurrentPasswordException(5-9)
src/main/java/com/campus/campus/domain/review/application/service/OcrService.java (2)
src/main/java/com/campus/campus/domain/review/application/exception/ReceiptFileConvertException.java (1)
ReceiptFileConvertException(5-9)src/main/java/com/campus/campus/domain/review/application/exception/ReceiptOcrFailedException.java (1)
ReceiptOcrFailedException(5-9)
src/main/java/com/campus/campus/domain/place/presentation/PlaceResponseCode.java (2)
src/main/java/com/campus/campus/domain/councilnotice/presentation/NoticeResponseCode.java (1)
Getter(10-23)src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostResponseCode.java (1)
Getter(10-27)
src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.java (1)
src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)
src/main/java/com/campus/campus/domain/council/application/service/CouncilLoginService.java (1)
src/main/java/com/campus/campus/domain/council/application/mapper/StudentCouncilMapper.java (4)
Component(18-80)toStudentCouncilLoginResponse(40-57)toStudentCouncilChangeProfileImageResponse(72-79)toStudentCouncilFindIdResponse(59-63)
src/main/java/com/campus/campus/domain/place/application/dto/response/PlaceDetailResponse.java (2)
src/main/java/com/campus/campus/domain/place/application/dto/response/SearchPlaceInfoResponse.java (1)
SearchPlaceInfoResponse(10-48)src/main/java/com/campus/campus/domain/place/application/dto/response/RecommendNearByPlaceResponse.java (1)
RecommendNearByPlaceResponse(10-39)
src/main/java/com/campus/campus/domain/review/application/dto/response/SimpleReviewResponse.java (4)
src/main/java/com/campus/campus/domain/review/application/dto/response/WriteReviewResponse.java (1)
Builder(7-18)src/main/java/com/campus/campus/domain/stamp/application/dto/response/StampReviewResponse.java (1)
StampReviewResponse(7-17)src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewRankingResponse.java (1)
ReviewRankingResponse(3-8)src/main/java/com/campus/campus/domain/stamp/application/dto/response/StampInfoResponse.java (1)
StampInfoResponse(7-14)
src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java (1)
src/main/java/com/campus/campus/domain/councilpost/domain/entity/StudentCouncilPost.java (1)
Entity(26-113)
src/main/java/com/campus/campus/domain/place/domain/repository/PlaceImagesRepository.java (2)
src/main/java/com/campus/campus/domain/place/domain/entity/PlaceImages.java (1)
Entity(18-40)src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java (1)
getPlaceImageUrls(259-267)
src/main/java/com/campus/campus/domain/place/application/exception/AlreadySuggestedPartnershipException.java (8)
src/main/java/com/campus/campus/domain/councilpost/application/exception/PartnershipDateRequiredException.java (2)
PartnershipDateRequiredException(5-9)PartnershipDateRequiredException(6-8)src/main/java/com/campus/campus/domain/council/application/exception/EmailAlreadyExistsException.java (1)
EmailAlreadyExistsException(5-9)src/main/java/com/campus/campus/domain/place/application/exception/PlaceCreationException.java (2)
PlaceCreationException(5-9)PlaceCreationException(6-8)src/main/java/com/campus/campus/domain/user/application/exception/NicknameAlreadyExistsException.java (1)
NicknameAlreadyExistsException(5-9)src/main/java/com/campus/campus/domain/user/application/exception/AcademicInfoUpdateRestrictionException.java (1)
AcademicInfoUpdateRestrictionException(5-9)src/main/java/com/campus/campus/domain/council/application/exception/CouncilSignupForbiddenException.java (1)
CouncilSignupForbiddenException(5-9)src/main/java/com/campus/campus/domain/council/application/exception/LoginIdAlreadyExistsException.java (1)
LoginIdAlreadyExistsException(5-9)src/main/java/com/campus/campus/domain/school/application/exception/SchoolMajorNotSameException.java (1)
SchoolMajorNotSameException(5-9)
src/main/java/com/campus/campus/domain/council/application/dto/response/StudentCouncilLoginResponse.java (3)
src/main/java/com/campus/campus/domain/council/application/dto/response/StudentCouncilNicknameResponse.java (1)
StudentCouncilNicknameResponse(5-12)src/main/java/com/campus/campus/domain/council/application/dto/response/StudentCouncilChangeProfileImageResponse.java (1)
StudentCouncilChangeProfileImageResponse(5-15)src/main/java/com/campus/campus/domain/council/application/mapper/StudentCouncilMapper.java (2)
toStudentCouncilChangeProfileImageResponse(72-79)Component(18-80)
src/main/java/com/campus/campus/domain/user/application/exception/ErrorCode.java (1)
src/main/java/com/campus/campus/domain/user/application/exception/AcademicInfoUpdateRestrictionException.java (2)
AcademicInfoUpdateRestrictionException(5-9)AcademicInfoUpdateRestrictionException(6-8)
src/main/java/com/campus/campus/domain/council/domain/repository/StudentCouncilRepository.java (2)
src/main/java/com/campus/campus/domain/school/domain/repository/MajorRepository.java (1)
MajorRepository(10-13)src/main/java/com/campus/campus/domain/school/domain/repository/CollegeRepository.java (1)
CollegeRepository(9-11)
src/main/java/com/campus/campus/domain/place/domain/entity/CouncilPartnershipSuggestion.java (2)
src/main/java/com/campus/campus/domain/place/domain/entity/UserPartnershipSuggestion.java (1)
Entity(20-47)src/main/java/com/campus/campus/domain/place/domain/entity/Place.java (1)
Entity(17-57)
src/main/java/com/campus/campus/domain/review/domain/entity/Review.java (4)
src/main/java/com/campus/campus/domain/council/domain/entity/StudentCouncil.java (1)
Entity(29-120)src/main/java/com/campus/campus/domain/user/domain/entity/User.java (1)
Entity(26-101)src/main/java/com/campus/campus/domain/review/application/dto/response/CursorPageReviewResponse.java (1)
Getter(8-16)src/main/java/com/campus/campus/domain/review/application/dto/response/WriteReviewResponse.java (1)
Builder(7-18)
src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.java (4)
src/main/java/com/campus/campus/domain/user/domain/entity/User.java (1)
Entity(26-101)src/main/java/com/campus/campus/domain/review/domain/entity/Review.java (1)
Entity(22-57)src/main/java/com/campus/campus/domain/councilpost/domain/entity/StudentCouncilPost.java (1)
Entity(26-106)src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImageRepository.java (1)
ReviewImageRepository(11-21)
src/main/java/com/campus/campus/domain/review/application/dto/response/PlaceStarAvgRow.java (7)
src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewRankingResponse.java (1)
ReviewRankingResponse(3-8)src/main/java/com/campus/campus/domain/place/application/dto/response/SearchPlaceInfoResponse.java (1)
SearchPlaceInfoResponse(10-48)src/main/java/com/campus/campus/domain/review/application/dto/response/WriteReviewResponse.java (1)
Builder(7-18)src/main/java/com/campus/campus/domain/stamp/application/dto/response/StampReviewResponse.java (1)
StampReviewResponse(7-17)src/main/java/com/campus/campus/domain/place/application/dto/response/RecommendPartnershipPlaceResponse.java (1)
RecommendPartnershipPlaceResponse(5-24)src/main/java/com/campus/campus/domain/place/application/dto/response/RecommendNearByPlaceResponse.java (1)
RecommendNearByPlaceResponse(10-39)src/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PartnershipResponse.java (1)
PartnershipResponse(6-24)
src/main/java/com/campus/campus/domain/place/domain/repository/UserPartnershipSuggestionRepository.java (2)
src/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.java (1)
LikedPlacesRepository(15-39)src/main/java/com/campus/campus/domain/stamp/domain/repository/RewardRepository.java (1)
RewardRepository(10-12)
src/main/java/com/campus/campus/domain/council/application/mapper/StudentCouncilLoginMapper.java (1)
src/main/java/com/campus/campus/domain/council/application/mapper/StudentCouncilMapper.java (2)
toStudentCouncilChangeProfileImageResponse(72-79)toStudentCouncilLoginResponse(40-57)
src/main/java/com/campus/campus/domain/place/application/dto/response/PlaceDetailView.java (5)
src/main/java/com/campus/campus/domain/place/domain/entity/Place.java (1)
Entity(17-57)src/main/java/com/campus/campus/domain/place/application/dto/response/SearchPlaceInfoResponse.java (1)
SearchPlaceInfoResponse(10-48)src/main/java/com/campus/campus/domain/place/application/dto/response/RecommendNearByPlaceResponse.java (1)
RecommendNearByPlaceResponse(10-39)src/main/java/com/campus/campus/domain/place/domain/entity/LikedPlace.java (1)
Entity(21-45)src/main/java/com/campus/campus/domain/place/domain/entity/PlaceImages.java (1)
Entity(18-40)
src/main/java/com/campus/campus/domain/review/infrastructure/ocr/ClovaOcrClient.java (2)
src/main/java/com/campus/campus/global/config/PermitUrlConfig.java (1)
Component(5-40)src/main/java/com/campus/campus/domain/review/application/exception/ReceiptImageFormatException.java (1)
ReceiptImageFormatException(5-9)
src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java (2)
src/main/java/com/campus/campus/domain/place/domain/entity/Place.java (1)
Entity(17-57)src/main/java/com/campus/campus/domain/place/domain/entity/Coordinate.java (1)
Embeddable(5-16)
src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java (1)
src/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.java (1)
LikedPlacesRepository(15-39)
src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResponse.java (2)
src/main/java/com/campus/campus/domain/review/application/dto/response/WriteReviewResponse.java (1)
Builder(7-18)src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewRankingResponse.java (1)
ReviewRankingResponse(3-8)
src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java (2)
src/main/java/com/campus/campus/domain/review/application/dto/response/WriteReviewResponse.java (1)
Builder(7-18)src/main/java/com/campus/campus/domain/stamp/application/mapper/StampMapper.java (1)
toStampReviewResponse(25-33)
src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java (2)
src/main/java/com/campus/campus/global/firebase/exception/ErrorCode.java (1)
Getter(10-20)src/main/java/com/campus/campus/global/common/exception/ErrorCodeInterface.java (1)
ErrorCodeInterface(5-11)
src/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.java (3)
src/main/java/com/campus/campus/domain/place/application/exception/GeoCoderException.java (1)
GeoCoderException(6-8)src/main/java/com/campus/campus/global/common/exception/ErrorCodeInterface.java (1)
ErrorCodeInterface(5-11)src/main/java/com/campus/campus/domain/notification/application/exception/ErrorCode.java (1)
Getter(10-21)
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| @EntityGraph(attributePaths = {"places"}) | ||
| @Query(value = """ | ||
| SELECT scp.* | ||
| FROM student_council_post scp | ||
| JOIN student_councils sc | ||
| ON scp.writer_id = sc.student_council_id | ||
| JOIN places p | ||
| ON scp.place_id = p.place_id | ||
| LEFT JOIN reviews r | ||
| ON r.place_id = p.place_id | ||
| AND r.created_at >= :from | ||
| WHERE scp.start_date_time <= :now | ||
| AND scp.end_date_time >= :now | ||
| AND ( | ||
| (sc.council_type =:majorType AND sc.major_id = :majorId) | ||
| OR (sc.council_type =:collegeType AND sc.college_id = :collegeId) | ||
| OR (sc.council_type =:schoolType AND sc.school_id = :schoolId) | ||
| ) | ||
| GROUP BY scp.id | ||
| ORDER BY COUNT(r.id) DESC | ||
| LIMIT 3; | ||
| """, nativeQuery = true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Find StudentCouncilPost entity file
find . -type f -name "*StudentCouncilPost*.java" | head -20Repository: our-campUS/our-campUS-BE
Length of output: 889
🏁 Script executed:
# Search for StudentCouncilPost class definition
rg "class StudentCouncilPost" -A 30Repository: our-campUS/our-campUS-BE
Length of output: 25188
🏁 Script executed:
# Check the repository file for the exact query and EntityGraph usage
rg -A 25 "@EntityGraph.*places" src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.javaRepository: our-campUS/our-campUS-BE
Length of output: 1032
🏁 Script executed:
rg -A 10 "findTop3RecommendedPartnershipPlaces" src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.javaRepository: our-campUS/our-campUS-BE
Length of output: 456
🏁 Script executed:
# Check StudentCouncil entity for council_type enum field
rg "council_type|councilType" src/main/java/com/campus/campus/domain/studentcouncil/domain/entity/StudentCouncil.java -A 2 -B 2Repository: our-campUS/our-campUS-BE
Length of output: 195
🏁 Script executed:
# Check what CouncilType enum looks like
find . -type f -name "*CouncilType*" -o -name "*counciltype*"Repository: our-campUS/our-campUS-BE
Length of output: 147
🏁 Script executed:
# Check CouncilType enum definition
cat -n src/main/java/com/campus/campus/domain/council/domain/entity/CouncilType.javaRepository: our-campUS/our-campUS-BE
Length of output: 1690
🏁 Script executed:
# Check StudentCouncil entity for the council_type field mapping
find . -type f -name "StudentCouncil.java" | xargs grep -l "council_type\|councilType" | head -1 | xargs cat -n | head -50Repository: our-campUS/our-campUS-BE
Length of output: 1889
🏁 Script executed:
# Get more of the StudentCouncil entity to see the council_type field
cat src/main/java/com/campus/campus/domain/council/domain/entity/StudentCouncil.java | sed -n '50,100p'Repository: our-campUS/our-campUS-BE
Length of output: 1275
@entitygraph를 Native Query와 함께 사용할 수 없습니다
-
@EntityGraph는 Native Query에서 작동하지 않습니다.@EntityGraph는 JPQL/HQL 쿼리에만 적용되며,nativeQuery = true일 때 무시됩니다. 불필요한 annotation이므로 제거하세요. -
attributePaths = {"places"}가 잘못되었습니다.StudentCouncilPost엔티티의 관계 필드명은place(단수)입니다. 다만,@EntityGraph가 native query에서 무시되므로 이 값도 적용되지 않습니다.
권장 수정안
- `@EntityGraph`(attributePaths = {"places"})
- `@Query`(value = """
+ `@Query`(value = """또는 JPQL로 전환하여 @EntityGraph를 활용하세요.
📝 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.
| @EntityGraph(attributePaths = {"places"}) | |
| @Query(value = """ | |
| SELECT scp.* | |
| FROM student_council_post scp | |
| JOIN student_councils sc | |
| ON scp.writer_id = sc.student_council_id | |
| JOIN places p | |
| ON scp.place_id = p.place_id | |
| LEFT JOIN reviews r | |
| ON r.place_id = p.place_id | |
| AND r.created_at >= :from | |
| WHERE scp.start_date_time <= :now | |
| AND scp.end_date_time >= :now | |
| AND ( | |
| (sc.council_type =:majorType AND sc.major_id = :majorId) | |
| OR (sc.council_type =:collegeType AND sc.college_id = :collegeId) | |
| OR (sc.council_type =:schoolType AND sc.school_id = :schoolId) | |
| ) | |
| GROUP BY scp.id | |
| ORDER BY COUNT(r.id) DESC | |
| LIMIT 3; | |
| """, nativeQuery = true) | |
| `@Query`(value = """ | |
| SELECT scp.* | |
| FROM student_council_post scp | |
| JOIN student_councils sc | |
| ON scp.writer_id = sc.student_council_id | |
| JOIN places p | |
| ON scp.place_id = p.place_id | |
| LEFT JOIN reviews r | |
| ON r.place_id = p.place_id | |
| AND r.created_at >= :from | |
| WHERE scp.start_date_time <= :now | |
| AND scp.end_date_time >= :now | |
| AND ( | |
| (sc.council_type =:majorType AND sc.major_id = :majorId) | |
| OR (sc.council_type =:collegeType AND sc.college_id = :collegeId) | |
| OR (sc.council_type =:schoolType AND sc.school_id = :schoolId) | |
| ) | |
| GROUP BY scp.id | |
| ORDER BY COUNT(r.id) DESC | |
| LIMIT 3; | |
| """, nativeQuery = true) |
🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java`
around lines 286 - 308, Remove the `@EntityGraph`(attributePaths = {"places"})
annotation from StudentCouncilPostRepository because `@EntityGraph` is ignored for
native queries (nativeQuery = true) and the attribute name is incorrect for the
entity (the field is place, not places); either delete the annotation or convert
the native query to a JPQL query so `@EntityGraph` on StudentCouncilPostRepository
can be effective (or change attributePaths to "place" if you switch to JPQL).
| double averageStar = averageStarMap.getOrDefault(post.getId(), 0.0); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
버그: averageStarMap 조회 키가 잘못되었습니다.
averageStarMap은 placeId를 키로 사용하지만, 여기서 post.getId()(게시글 ID)로 조회하고 있습니다. 이로 인해 항상 0.0이 반환됩니다.
🐛 수정 제안
- double averageStar = averageStarMap.getOrDefault(post.getId(), 0.0);
+ double averageStar = averageStarMap.getOrDefault(post.getPlace().getPlaceId(), 0.0);📝 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.
| double averageStar = averageStarMap.getOrDefault(post.getId(), 0.0); | |
| double averageStar = averageStarMap.getOrDefault(post.getPlace().getPlaceId(), 0.0); |
🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java`
around lines 133 - 134, The lookup uses the wrong key: averageStarMap stores
averages by placeId but the code does averageStarMap.getOrDefault(post.getId(),
0.0). Replace the lookup key with the place ID for the post (e.g.,
post.getPlace().getId() or post.getPlaceId() depending on the Post API) so
averageStar = averageStarMap.getOrDefault(placeId, 0.0); ensure you reference
the same placeId accessor used elsewhere in PartnershipPlaceService.
| for (StudentCouncil council : councils) { | ||
| CouncilPartnershipSuggestion demand = | ||
| partnershipSuggestionRepository.findByPlaceAndCouncil(place, council) | ||
| .orElseGet(() -> | ||
| partnershipSuggestionRepository.save( | ||
| CouncilPartnershipSuggestion.create(place, council) | ||
| )); | ||
| demand.increase(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
fd -t f "CouncilPartnershipSuggestion.java"Repository: our-campUS/our-campUS-BE
Length of output: 160
🏁 Script executed:
fd -t f "UserPartnershipSuggestion.java"Repository: our-campUS/our-campUS-BE
Length of output: 157
🏁 Script executed:
fd -t f "PlaceService.java" --exec head -200 {}Repository: our-campUS/our-campUS-BE
Length of output: 15076
🏁 Script executed:
cat -n src/main/java/com/campus/campus/domain/place/domain/entity/CouncilPartnershipSuggestion.javaRepository: our-campUS/our-campUS-BE
Length of output: 2041
🏁 Script executed:
cat -n src/main/java/com/campus/campus/domain/place/domain/entity/UserPartnershipSuggestion.javaRepository: our-campUS/our-campUS-BE
Length of output: 1787
🏁 Script executed:
fd -t f "\.sql" | head -20Repository: our-campUS/our-campUS-BE
Length of output: 50
🏁 Script executed:
rg -A 5 "uniqueConstraints\|@Unique" --type javaRepository: our-campUS/our-campUS-BE
Length of output: 50
🏁 Script executed:
fd -t f "\.sql" -o -t f "migration\|schema" | head -30Repository: our-campUS/our-campUS-BE
Length of output: 184
🏁 Script executed:
find . -name "*.sql" -o -name "*migration*" -o -name "*schema*" 2>/dev/null | head -20Repository: our-campUS/our-campUS-BE
Length of output: 50
🏁 Script executed:
rg "council_partnership_suggestion|user_partnership_suggestion" --type sql --type yaml --type xmlRepository: our-campUS/our-campUS-BE
Length of output: 50
동시성 경합(race condition) 문제 해결 필요
findByPlaceAndCouncil().orElseGet(() -> save()) 패턴은 원자적이지 않아 동시 요청 시 중복 CouncilPartnershipSuggestion 생성이 발생할 수 있습니다. 엔티티 정의를 확인한 결과 (place_id, student_council_id) 조합에 대한 DB 유니크 제약 조건이 없으므로, 두 스레드가 모두 find() 체크를 통과하여 중복 저장할 수 있습니다.
UserPartnershipSuggestion은 existsByUserAndPlace() 체크로 보호되어 있지만, CouncilPartnershipSuggestion은 같은 방식을 사용하지 않습니다. 다음 중 하나로 해결하세요:
- 데이터베이스 유니크 제약 조건 추가 후 예외 처리 (검토 의견의 제안 방식)
existsByPlaceAndCouncil()체크 추가 (UserPartnershipSuggestion 패턴과 일관성)
현재 코드에서는 제약 조건이 없으므로 제안된 DataIntegrityViolationException 처리만으로는 충분하지 않습니다. 제약 조건 추가 또는 애플리케이션 레벨의 중복 체크가 필수입니다.
🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java`
around lines 144 - 151, The current findByPlaceAndCouncil().orElseGet(() ->
partnershipSuggestionRepository.save(CouncilPartnershipSuggestion.create(place,
council))) pattern can race and create duplicate CouncilPartnershipSuggestion
rows; either add a DB unique constraint on (place_id, student_council_id) and
catch DataIntegrityViolationException around the save, or mirror the
UserPartnershipSuggestion approach by adding a
partnershipSuggestionRepository.existsByPlaceAndCouncil(place, council)
pre-check and only calling save when !exists (then fetch the entity), updating
the code around findByPlaceAndCouncil, CouncilPartnershipSuggestion.create, and
demand.increase() to use the chosen strategy to prevent duplicates.
| @NotNull | ||
| @DecimalMin(value = "0.0", inclusive = true) | ||
| @DecimalMax(value = "5.0", inclusive = true) | ||
| @Schema(example = "3.5") | ||
| Double star, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
star 필드에 범위 유효성 검사가 누락되었습니다.
AI 요약에 따르면 @DecimalMin과 @DecimalMax 어노테이션이 제거되었습니다. 별점 필드에 범위 검증이 없으면 -100.0이나 999.0 같은 비정상적인 값이 저장될 수 있습니다.
🔧 별점 범위 유효성 검사 추가 제안
`@NotNull`
`@Schema`(example = "3.5")
+ `@DecimalMin`(value = "0.0", message = "별점은 0점 이상이어야 합니다.")
+ `@DecimalMax`(value = "5.0", message = "별점은 5점 이하여야 합니다.")
Double star,📝 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.
| @NotNull | |
| @DecimalMin(value = "0.0", inclusive = true) | |
| @DecimalMax(value = "5.0", inclusive = true) | |
| @Schema(example = "3.5") | |
| Double star, | |
| `@NotNull` | |
| `@Schema`(example = "3.5") | |
| `@DecimalMin`(value = "0.0", message = "별점은 0점 이상이어야 합니다.") | |
| `@DecimalMax`(value = "5.0", message = "별점은 5점 이하여야 합니다.") | |
| Double star, |
🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java`
around lines 19 - 21, The ReviewRequest.star field in class ReviewRequest lacks
range validation; add `@DecimalMin`(value = "0.0", inclusive = true) and
`@DecimalMax`(value = "5.0", inclusive = true) annotations to the Double star
field (keeping the existing `@NotNull` and `@Schema`) and add the corresponding
imports (javax.validation.constraints.DecimalMin and DecimalMax) so out-of-range
values like -100.0 or 999.0 are rejected.
| package com.campus.campus.domain.review.application.exception; | ||
|
|
||
| import com.campus.campus.global.common.exception.ApplicationException; | ||
|
|
||
| public class NotPartnershipReceiptException extends ApplicationException { | ||
| public NotPartnershipReceiptException() { | ||
| super(ErrorCode.NOT_PARTNERSHIP_RECEIPT_ERROR); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ErrorCode import 누락으로 컴파일 오류 발생
ErrorCode.NOT_PARTNERSHIP_RECEIPT_ERROR를 사용하고 있지만 ErrorCode 클래스에 대한 import 문이 없습니다. AI 요약에 따르면 ErrorCode는 com.campus.campus.domain.place.application.exception 패키지에 정의되어 있으므로 import가 필요합니다.
🐛 누락된 import 추가
package com.campus.campus.domain.review.application.exception;
import com.campus.campus.global.common.exception.ApplicationException;
+import com.campus.campus.domain.place.application.exception.ErrorCode;
public class NotPartnershipReceiptException extends ApplicationException {
public NotPartnershipReceiptException() {
super(ErrorCode.NOT_PARTNERSHIP_RECEIPT_ERROR);
}
}📝 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.
| package com.campus.campus.domain.review.application.exception; | |
| import com.campus.campus.global.common.exception.ApplicationException; | |
| public class NotPartnershipReceiptException extends ApplicationException { | |
| public NotPartnershipReceiptException() { | |
| super(ErrorCode.NOT_PARTNERSHIP_RECEIPT_ERROR); | |
| } | |
| } | |
| package com.campus.campus.domain.review.application.exception; | |
| import com.campus.campus.global.common.exception.ApplicationException; | |
| import com.campus.campus.domain.place.application.exception.ErrorCode; | |
| public class NotPartnershipReceiptException extends ApplicationException { | |
| public NotPartnershipReceiptException() { | |
| super(ErrorCode.NOT_PARTNERSHIP_RECEIPT_ERROR); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/review/application/exception/NotPartnershipReceiptException.java`
around lines 1 - 9, The NotPartnershipReceiptException class uses ErrorCode but
lacks its import; add an import for
com.campus.campus.domain.place.application.exception.ErrorCode at the top of the
file so NotPartnershipReceiptException (which extends ApplicationException) can
reference ErrorCode.NOT_PARTNERSHIP_RECEIPT_ERROR without compile errors.
| extractFormat(originalFilename); | ||
|
|
||
| //json body 구성 | ||
| Map<String, Object> image = new HashMap<>(); | ||
| image.put("format", "png"); | ||
| image.put("data", base64Image); | ||
| image.put("name", "receipt_test2"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
심각한 버그: extractFormat() 반환값 미사용
extractFormat()을 호출하지만 반환값을 무시하고, Line 40에서 format을 "png"로 하드코딩하고 있습니다. JPG 이미지를 업로드하면 OCR API에 잘못된 포맷 정보가 전달됩니다.
🐛 수정 제안
public String requestReceiptOcr(byte[] imageBytes, String originalFilename) {
//이미지->Base64
String base64Image = Base64.getEncoder().encodeToString(imageBytes);
- extractFormat(originalFilename);
+ String format = extractFormat(originalFilename);
//json body 구성
Map<String, Object> image = new HashMap<>();
- image.put("format", "png");
+ image.put("format", format);
image.put("data", base64Image);
image.put("name", "receipt_test2");📝 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.
| extractFormat(originalFilename); | |
| //json body 구성 | |
| Map<String, Object> image = new HashMap<>(); | |
| image.put("format", "png"); | |
| image.put("data", base64Image); | |
| image.put("name", "receipt_test2"); | |
| String format = extractFormat(originalFilename); | |
| //json body 구성 | |
| Map<String, Object> image = new HashMap<>(); | |
| image.put("format", format); | |
| image.put("data", base64Image); | |
| image.put("name", "receipt_test2"); |
🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/review/infrastructure/ocr/ClovaOcrClient.java`
around lines 36 - 42, ClovaOcrClient currently calls
extractFormat(originalFilename) but ignores its return and hardcodes
image.put("format","png"); fix by capturing the returned format (e.g., String
format = extractFormat(originalFilename)) and use that value when building the
JSON body (replace the hardcoded "png" in image.put("format", ...)); update any
related variable names and validation if extractFormat can return null/empty to
fall back to a safe default.
| @PostMapping("/receipt-ocr") | ||
| @Operation(summary = "영수증 ocr을 통해 제휴 매장 이용 인증") | ||
| public CommonResponse<ReviewPartnerResponse> upload( | ||
| @RequestPart MultipartFile file, | ||
| @Valid @RequestBody SavedPlaceInfo placeInfo, | ||
| @CurrentUserId Long userId | ||
| ) { | ||
| return CommonResponse.success(ReviewResponseCode.OCR_SUCCESS, | ||
| ocrService.processReceipt(file, userId, placeInfo)); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find `@CurrentUserId` implementation
find . -type f -name "*.java" | xargs grep -l "@CurrentUserId" | head -5Repository: our-campUS/our-campUS-BE
Length of output: 514
🏁 Script executed:
#!/bin/bash
# Check PermitUrlConfig to see if /receipt-ocr is public
fd -t f "PermitUrlConfig" --type fRepository: our-campUS/our-campUS-BE
Length of output: 134
🏁 Script executed:
#!/bin/bash
# Search for other multipart endpoints in the codebase to compare
rg -n "@RequestPart" --type java -B 2 -A 5Repository: our-campUS/our-campUS-BE
Length of output: 1086
🏁 Script executed:
#!/bin/bash
# Check SavedPlaceInfo class to understand its structure
fd -t f "SavedPlaceInfo.java"Repository: our-campUS/our-campUS-BE
Length of output: 157
🏁 Script executed:
#!/bin/bash
# Find `@CurrentUserId` annotation definition
fd -t f "CurrentUserId.java"Repository: our-campUS/our-campUS-BE
Length of output: 136
🏁 Script executed:
#!/bin/bash
# Check PermitUrlConfig content to see if /receipt-ocr is permitted
cat src/main/java/com/campus/campus/global/config/PermitUrlConfig.java | head -100Repository: our-campUS/our-campUS-BE
Length of output: 1144
🏁 Script executed:
#!/bin/bash
# Check SavedPlaceInfo structure
cat src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.javaRepository: our-campUS/our-campUS-BE
Length of output: 1188
🏁 Script executed:
#!/bin/bash
# Check for other `@RequestBody` with multipart in the codebase
rg -n "@RequestBody" --type java -B 3 -A 1 | grep -A 3 -B 3 "@RequestPart"Repository: our-campUS/our-campUS-BE
Length of output: 713
🏁 Script executed:
#!/bin/bash
# Check `@CurrentUserId` annotation implementation
cat src/main/java/com/campus/campus/global/annotation/CurrentUserId.javaRepository: our-campUS/our-campUS-BE
Length of output: 657
🏁 Script executed:
#!/bin/bash
# Check if there's a resolver for `@CurrentUserId`
find . -type f -name "*CurrentUserId*" -o -name "*ArgumentResolver*" | grep -i resolverRepository: our-campUS/our-campUS-BE
Length of output: 334
🏁 Script executed:
#!/bin/bash
# Search for the resolver that handles `@CurrentUserId`
rg -n "CurrentUserId" --type java -A 5 | grep -i "resolver\|argument" | head -20Repository: our-campUS/our-campUS-BE
Length of output: 2766
🏁 Script executed:
#!/bin/bash
# Check if there's a MethodArgumentResolver that processes `@CurrentUserId`
fd -t f "*.java" | xargs grep -l "MethodArgumentResolver" | xargs grep -l "CurrentUserId"Repository: our-campUS/our-campUS-BE
Length of output: 355
🏁 Script executed:
#!/bin/bash
# Check the full CurrentUserIdArgumentResolver implementation
cat src/main/java/com/campus/campus/global/annotation/CurrentUserIdArgumentResolver.javaRepository: our-campUS/our-campUS-BE
Length of output: 2230
🏁 Script executed:
#!/bin/bash
# Check if multipart/form-data with `@RequestBody` is commonly used in Spring
# Let's look for documentation or similar patterns in the code
rg -n "@RequestPart" --type java -A 10 | head -50Repository: our-campUS/our-campUS-BE
Length of output: 1390
🏁 Script executed:
#!/bin/bash
# Check if there are any tests or documentation about multipart with `@RequestBody`
rg -n "multipart" --type java -i -A 3 -B 3 | head -50Repository: our-campUS/our-campUS-BE
Length of output: 4121
🏁 Script executed:
#!/bin/bash
# Verify: does the endpoint actually require authentication?
# Check if there's any security configuration that might override PermitUrlConfig
fd -t f "Security" --type f -name "*Config*" | xargs grep -l "receipt-ocr" 2>/dev/nullRepository: our-campUS/our-campUS-BE
Length of output: 300
🏁 Script executed:
#!/bin/bash
# Check if there are integration tests for the receipt-ocr endpoint
find . -type f -name "*Test*.java" | xargs grep -l "receipt-ocr" 2>/dev/nullRepository: our-campUS/our-campUS-BE
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Let's check the full ReviewController to understand the context better
cat src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java | head -100Repository: our-campUS/our-campUS-BE
Length of output: 4021
🏁 Script executed:
#!/bin/bash
# Check if `@CurrentUserId` is used with required=false anywhere
rg -n "@CurrentUserId" --type java -A 1 | grep -i "required"Repository: our-campUS/our-campUS-BE
Length of output: 50
🏁 Script executed:
#!/bin/bash
# Let's check if there's any security configuration that might affect the public endpoint
find . -type f -name "*Security*" -name "*.java" | head -5Repository: our-campUS/our-campUS-BE
Length of output: 135
🏁 Script executed:
#!/bin/bash
# Check JwtAuthenticationFilter to understand how authentication is set
fd -t f "JwtAuthenticationFilter.java"Repository: our-campUS/our-campUS-BE
Length of output: 144
🏁 Script executed:
#!/bin/bash
# Search for web security configuration
rg -n "SecurityFilterChain\|WebSecurityConfigurerAdapter" --type java -A 5 | head -50Repository: our-campUS/our-campUS-BE
Length of output: 50
🏁 Script executed:
#!/bin/bash
# Check SecurityConfig to understand how public URLs are handled
cat src/main/java/com/campus/campus/global/config/SecurityConfig.javaRepository: our-campUS/our-campUS-BE
Length of output: 2818
🏁 Script executed:
#!/bin/bash
# Check JwtAuthenticationFilter to see how it handles requests
cat src/main/java/com/campus/campus/global/util/jwt/JwtAuthenticationFilter.javaRepository: our-campUS/our-campUS-BE
Length of output: 4573
@RequestPart와 @RequestBody의 혼용으로 인한 요청 처리 오류 발생 가능
@RequestPart MultipartFile file과 @Valid @RequestBody SavedPlaceInfo placeInfo를 함께 사용하고 있습니다. 이 조합은 multipart/form-data 요청에서 일반적으로 작동하지 않습니다. 표준적인 방식은 두 파라미터 모두 @RequestPart를 사용하고, 클라이언트에서 JSON 문자열로 인코딩하여 전송하는 것입니다.
`@PostMapping`("/receipt-ocr")
`@Operation`(summary = "영수증 ocr을 통해 제휴 매장 이용 인증")
public CommonResponse<ReviewPartnerResponse> upload(
`@RequestPart` MultipartFile file,
- `@Valid` `@RequestBody` SavedPlaceInfo placeInfo,
+ `@Valid` `@RequestPart` SavedPlaceInfo placeInfo,
`@CurrentUserId` Long userId
) {중요: @CurrentUserId 필수 요구 vs 공개 엔드포인트의 모순
더 심각한 문제로, 이 엔드포인트가 PermitUrlConfig에서 공개(permitAll)로 설정되어 있지만 @CurrentUserId 어노테이션을 사용합니다. @CurrentUserId는 기본값으로 required=true이므로, 인증 토큰 없이 접근하는 경우 SecurityContextHolder의 인증 정보가 null이 되어 UnAuthorizedException을 발생시킵니다.
공개 엔드포인트로 설정하려면 @CurrentUserId(required=false)로 변경하거나, 인증이 필수라면 PermitUrlConfig에서 해당 경로를 제거해야 합니다.
🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java`
around lines 91 - 100, The upload method mixes `@RequestPart` MultipartFile file
with `@Valid` `@RequestBody` SavedPlaceInfo placeInfo which will fail for
multipart/form-data; change placeInfo to `@RequestPart` and have clients send the
JSON-encoded SavedPlaceInfo as a part (or accept a String part and deserialize
it) so both parts are handled as multipart. Also address the auth mismatch:
either make `@CurrentUserId` optional by using `@CurrentUserId`(required=false) on
the upload method if this endpoint must remain permitAll, or remove this path
from PermitUrlConfig so authentication is required; update the upload method
signature accordingly (method name: upload, parameter: `@CurrentUserId` Long
userId) to match the chosen behavior.
| private KakaoTokenResponse getToken(String authorizationCode) { | ||
| RestClient client = RestClient.create(); | ||
|
|
||
| MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); | ||
| body.add("grant_type", "authorization_code"); | ||
| body.add("client_id", kakaoOauthProperty.getClientId()); | ||
| body.add("redirect_uri", kakaoOauthProperty.getRedirectUri()); | ||
| body.add("code", authorizationCode); | ||
|
|
||
| if (kakaoOauthProperty.getClientSecret() != null && | ||
| !kakaoOauthProperty.getClientSecret().isBlank()) { | ||
| body.add("client_secret", kakaoOauthProperty.getClientSecret()); | ||
| } | ||
|
|
||
| return client.post() | ||
| .uri(KAUTH_BASE_URL + "/oauth/token") | ||
| .contentType(MediaType.APPLICATION_FORM_URLENCODED) | ||
| .body(body) | ||
| .retrieve() | ||
| .body(KakaoTokenResponse.class); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
토큰 교환 실패 시 에러 처리 부재
getToken() 메서드에서 Kakao API 호출 실패 또는 null 응답에 대한 처리가 없습니다. unlink() 메서드(line 84-96)처럼 에러 처리를 추가하거나, 최소한 null 체크를 권장합니다.
🔧 에러 처리 추가 제안
private KakaoTokenResponse getToken(String authorizationCode) {
RestClient client = RestClient.create();
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", kakaoOauthProperty.getClientId());
body.add("redirect_uri", kakaoOauthProperty.getRedirectUri());
body.add("code", authorizationCode);
if (kakaoOauthProperty.getClientSecret() != null &&
!kakaoOauthProperty.getClientSecret().isBlank()) {
body.add("client_secret", kakaoOauthProperty.getClientSecret());
}
- return client.post()
+ KakaoTokenResponse response = client.post()
.uri(KAUTH_BASE_URL + "/oauth/token")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(body)
.retrieve()
.body(KakaoTokenResponse.class);
+
+ if (response == null || response.accessToken() == null) {
+ throw new RuntimeException("카카오 토큰 발급 실패");
+ }
+
+ return response;
}커스텀 예외 클래스를 만들어 사용하면 더 좋습니다.
🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java`
around lines 137 - 157, getToken currently makes the Kakao token request but
does not handle HTTP errors or null responses; update getToken in
KakaoOauthService to validate the response and surface failures by (1) wrapping
the RestClient call in try/catch to catch client/transport errors (e.g.,
RestClientException/RuntimeException), (2) check the returned KakaoTokenResponse
for null or missing expected fields, and (3) throw a clear custom exception
(e.g., KakaoOauthException) with contextual details (include authorizationCode
or response/error body) similar to how unlink() handles errors; ensure to
reference KakaoTokenResponse, kakaoOauthProperty and KAUTH_BASE_URL when
logging/constructing the error so callers can handle failures deterministically.
| @Bean | ||
| public RestTemplate clovaOcrRestTemplate() { | ||
| return new RestTemplate(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
외부 API 호출용 RestTemplate에 타임아웃 설정 필요
Clova OCR은 외부 서비스이므로, 타임아웃 없이 호출하면 서비스가 느리거나 응답하지 않을 때 스레드가 무한정 블로킹될 수 있습니다. 이는 스레드 풀 고갈로 이어져 장애를 유발할 수 있습니다.
🔧 타임아웃 설정 추가 제안
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+
`@Configuration`
public class ClovaOcrConfig {
`@Bean`
public RestTemplate clovaOcrRestTemplate() {
- return new RestTemplate();
+ SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+ factory.setConnectTimeout(5000); // 5초
+ factory.setReadTimeout(30000); // 30초 (OCR 처리 시간 고려)
+ return new RestTemplate(factory);
}
}또는 Spring Boot 3.x를 사용 중이라면 RestTemplateBuilder를 주입받아 사용하는 것이 더 권장됩니다:
`@Bean`
public RestTemplate clovaOcrRestTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(30))
.build();
}🤖 Prompt for AI Agents
In `@src/main/java/com/campus/campus/global/config/ClovaOcrConfig.java` around
lines 10 - 13, The clovaOcrRestTemplate bean currently returns a plain
RestTemplate with no timeouts; update clovaOcrRestTemplate to accept a
RestTemplateBuilder parameter and configure sensible timeouts (e.g.,
setConnectTimeout and setReadTimeout) before calling build() so external Clova
OCR calls won't block threads indefinitely; locate the clovaOcrRestTemplate
method in ClovaOcrConfig and replace the new RestTemplate() return with a
RestTemplate built via
RestTemplateBuilder.setConnectTimeout(...).setReadTimeout(...).build().
| "/api/partnership/map", | ||
| "/reviews/receipt-ocr" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for receipt-ocr endpoint implementation in ReviewController
rg -n -A 15 'receipt-ocr|receiptOcr' --type=javaRepository: our-campUS/our-campUS-BE
Length of output: 2415
🏁 Script executed:
#!/bin/bash
# Find `@CurrentUserId` annotation definition and implementation
rg -n -B 2 -A 5 'CurrentUserId' --type=java | head -50Repository: our-campUS/our-campUS-BE
Length of output: 5936
🏁 Script executed:
#!/bin/bash
# Find the `@CurrentUserId` annotation definition
find . -name "CurrentUserId.java" -o -name "*CurrentUserId*" | grep -v ".class"Repository: our-campUS/our-campUS-BE
Length of output: 225
🏁 Script executed:
#!/bin/bash
# Read the `@CurrentUserId` annotation definition
cat -n ./src/main/java/com/campus/campus/global/annotation/CurrentUserId.javaRepository: our-campUS/our-campUS-BE
Length of output: 804
🏁 Script executed:
#!/bin/bash
# Read the ArgumentResolver implementation
cat -n ./src/main/java/com/campus/campus/global/annotation/CurrentUserIdArgumentResolver.javaRepository: our-campUS/our-campUS-BE
Length of output: 2685
🏁 Script executed:
#!/bin/bash
# Check PermitUrlConfig usage in SecurityConfig
rg -n -B 5 -A 10 'PermitUrlConfig|permitUrl|PUBLIC_URLS' --type=java | head -80Repository: our-campUS/our-campUS-BE
Length of output: 4469
🏁 Script executed:
#!/bin/bash
# Check JwtAuthenticationFilter implementation to see if it processes all requests or only protected ones
rg -n -B 5 -A 20 'class JwtAuthenticationFilter' --type=javaRepository: our-campUS/our-campUS-BE
Length of output: 2939
🏁 Script executed:
#!/bin/bash
# Get the complete doFilterInternal method to understand token handling
sed -n '47,110p' ./src/main/java/com/campus/campus/global/util/jwt/JwtAuthenticationFilter.java | cat -nRepository: our-campUS/our-campUS-BE
Length of output: 2673
🏁 Script executed:
#!/bin/bash
# Check if there are any other endpoints in PermitUrlConfig and see the full list
cat -n ./src/main/java/com/campus/campus/global/config/PermitUrlConfig.javaRepository: our-campUS/our-campUS-BE
Length of output: 1424
설정 불일치: OCR 엔드포인트가 인증을 요구하면 PermitUrlConfig에서 제거 필요
/reviews/receipt-ocr 엔드포인트는 현재 PermitUrlConfig의 공개 URL 목록(line 37)에 포함되어 있지만, 컨트롤러의 @CurrentUserId 파라미터(required=true)가 유효한 인증을 반드시 요구합니다.
이는 설정 불일치를 야기합니다:
- PermitUrlConfig에 등록된 엔드포인트는 Spring Security 레벨에서 인증 없이 접근 가능하지만
- 실제 요청은 컨트롤러에서
UnAuthorizedException으로 실패합니다
권장 조치:
사용자별 영수증 OCR이 인증이 필요한 기능이라면, /reviews/receipt-ocr을 PermitUrlConfig에서 제거하여 Spring Security 레벨에서 인증을 강제해야 합니다. 이렇게 하면 설정과 구현이 일치하게 됩니다.
🤖 Prompt for AI Agents
In `@src/main/java/com/campus/campus/global/config/PermitUrlConfig.java` around
lines 36 - 37, PermitUrlConfig currently lists "/reviews/receipt-ocr" as a
public URL but the controller handling that endpoint requires an authenticated
user via `@CurrentUserId` (required=true) and throws UnAuthorizedException; remove
"/reviews/receipt-ocr" from the PermitUrlConfig public URL array so Spring
Security will require authentication for that endpoint, aligning security
configuration with the controller's authentication requirement.
🔀 변경 내용
✅ 작업 항목
📸 스크린샷 (선택)
📎 참고 이슈
관련 이슈 번호 #37
Summary by CodeRabbit
릴리스 노트
새로운 기능
기능 제거
API 개선
✏️ Tip: You can customize this high-level summary in your review settings.