-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/#86 장소 상세조회, 제휴 신청, 리뷰 OCR 인증 기능 구현 #87
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제휴 제안·검증(영수증 OCR) 및 장소 상세 조회 기능과 관련 DTO/예외/저장소/매퍼/서비스/컨트롤러가 추가·확장되었고, 학생회 관련 저장소에 FK 기반 조회 메서드가 3개 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant PlaceController as PlaceController
participant PlaceService as PlaceService
participant UserPartRepo as UserPartnershipSuggestionRepository
participant CouncilPartRepo as CouncilPartnershipSuggestionRepository
participant StudentCouncilRepo as StudentCouncilRepository
participant DB as Database
Client->>PlaceController: POST /suggest-partnership (placeInfo)
PlaceController->>PlaceService: suggestPartnership(userId, placeInfo)
PlaceService->>PlaceService: createPlace(placeInfo) or find
PlaceService->>DB: save/find Place
PlaceService->>UserPartRepo: existsByUserAndPlace?
alt 이미 제안됨
PlaceService-->>Client: throw AlreadySuggestedPartnershipException
else 새 제안
PlaceService->>UserPartRepo: save(UserPartnershipSuggestion)
PlaceService->>StudentCouncilRepo: findByMajor/College/School
loop 각 학생회
PlaceService->>CouncilPartRepo: findForUpdate(place, council)
alt 존재
PlaceService->>CouncilPartRepo: increase requestCount (update)
else
PlaceService->>CouncilPartRepo: save(create(place, council))
end
end
PlaceService-->>Client: PARTNERSHIP_SUGGEST_SUCCESS
end
sequenceDiagram
participant Client as Client
participant ReviewController as ReviewController
participant OcrService as OcrService
participant Clova as ClovaOcrClient
participant Parser as Parser
participant ReviewService as ReviewService
participant StudentCouncilPostRepo as StudentCouncilPostRepository
participant ReviewMapper as ReviewMapper
Client->>ReviewController: POST /reviews/receipt-ocr (file, placeId)
ReviewController->>OcrService: processReceipt(file, userId, placeId)
OcrService->>OcrService: convert file -> byte[]
OcrService->>Clova: requestReceiptOcr(bytes, filename)
Clova-->>OcrService: OCR JSON
OcrService->>Parser: parse(json)
Parser-->>OcrService: ReceiptOcrResponse
OcrService->>OcrService: extractReceiptResult(...)
OcrService->>ReviewService: findPartnership(placeId, receiptResult, userId)
ReviewService->>StudentCouncilPostRepo: findValidPartnershipForUserScope(...)
alt 파트너십 발견
ReviewService->>ReviewMapper: toReviewPartnerResponse(...)
ReviewService-->>OcrService: ReviewPartnerResponse
OcrService-->>Client: OCR_SUCCESS + ReviewPartnerResponse
else 미발견
ReviewService-->>OcrService: throw NotPartnershipReceiptException
OcrService-->>Client: Error Response
end
sequenceDiagram
participant Client as Client
participant PlaceController as PlaceController
participant PartnershipService as PartnershipPlaceService
participant PlaceRepo as PlaceRepository
participant PostRepo as StudentCouncilPostRepository
participant ReviewService as ReviewService
participant PlaceMapper as PlaceMapper
Client->>PlaceController: GET /detail?placeId=&lat=&lng=
PlaceController->>PartnershipService: getPlaceDetails(userId, placeId, lat, lng)
PartnershipService->>PlaceRepo: findById(placeId)
alt 장소 없음
PartnershipService-->>Client: PlaceInfoNotFoundException
else 장소 존재
PartnershipService->>PostRepo: findActiveByPlaceAndUserScope(...)
alt 활성 파트너십 게시물 존재
PartnershipService->>ReviewService: getReviewSummaryList, getAverageOfStars
PartnershipService->>PlaceMapper: toPartnershipResponse / toPlaceDetailResponse
PartnershipService-->>Client: PlaceDetailView (파트너십 상세)
else 비파트너십
PartnershipService->>ReviewService: getReviewSummaryList, getAverageOfStars
PartnershipService->>PlaceMapper: toPlaceDetailResponse
PartnershipService-->>Client: PlaceDetailView (일반)
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45분 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: 19
🤖 Fix all issues with AI agents
In
`@src/main/java/com/campus/campus/domain/council/domain/repository/StudentCouncilRepository.java`:
- Around line 48-52: The three repository methods findByMajor_MajorId,
findByCollege_CollegeId, and findBySchool_SchoolId can return unapproved or
deleted councils used by PlaceService.resolveCouncils; update these methods to
only return councils with managerApproved = true and deletedAt = null (e.g.,
rename/replace them with
findByMajor_MajorIdAndManagerApprovedTrueAndDeletedAtIsNull,
findByCollege_CollegeIdAndManagerApprovedTrueAndDeletedAtIsNull, and
findBySchool_SchoolIdAndManagerApprovedTrueAndDeletedAtIsNull or add equivalent
`@Query` filters) so resolveCouncils only sees approved, non-deleted
StudentCouncil entities.
In
`@src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java`:
- Around line 415-437: The query in existsActiveByPlaceAndUserScope is missing
the writer soft-delete check; update the JPQL WHERE clause (the query on
StudentCouncilPost p joining p.writer w) to include "w.deletedAt IS NULL"
alongside the existing time/place conditions so only non-deleted writers are
considered; ensure the condition is combined with the other AND predicates and
refers to alias w used in the JOIN.
- Around line 318-343: The query in
StudentCouncilPostRepository.findValidPartnershipForUserScope is missing the
soft-delete check for the writer alias (w); update the JPQL WHERE clause to
include "w.deletedAt IS NULL" alongside the existing conditions (placeId,
paymentDate range, and councilType/id OR clauses) so that posts by deleted
writers are excluded; ensure you add this condition with the other top-level
WHERE predicates in the same query string.
- Around line 439-462: The JPQL query in findActiveByPlaceAndUserScope omits the
soft-delete filter and can return multiple rows despite returning Optional; add
the writer null-check "AND w.deletedAt IS NULL" into the WHERE clause (keeping
aliases p and w) and ensure a single-result semantics by either adding an
explicit single-result constraint (use a native query with "LIMIT 1") or change
the repository signature to return a List/Pageable or use Spring Data's first
result support (e.g., rename to findFirstBy... or use Pageable with size 1) so
the method and query agree.
In
`@src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java`:
- Around line 107-108: averageStarMap is keyed by placeId from
reviewService.getAverageListOfStars(placeIds) but the code looks up
averageStarMap using post.getId(); change the lookup to use the placeId key
(e.g., post.getPlaceId() or the loop variable representing the placeId) wherever
averageStarMap.get(post.getId()) is used (including the occurrences around the
current post handling and the other place at the same region), so replace
averageStarMap.get(post.getId()) with averageStarMap.get(post.getPlaceId()) or
the correct placeId variable and handle a possible null/default value as before.
- Around line 201-230: Add a transactional boundary and guard against a missing
Place: annotate getPartnershipDetail(...) with `@Transactional`(readOnly = true)
to prevent lazy-loading issues, and after retrieving StudentCouncilPost via
studentCouncilPostRepository.findById(...), check post.getPlace() for null and
throw an appropriate exception (e.g., PlaceNotFoundException) if null before
using place; keep the rest of the logic (calculateDistance,
calculateAverageStar, isLiked, getImgUrls, reviewService calls, and
placeMapper.toPartnershipResponse) unchanged.
In
`@src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java`:
- Around line 189-222: The find-create-increase sequence in suggestPartnership
causes a race on partnershipSuggestionRepository.findByPlaceAndCouncil ->
CouncilPartnershipSuggestion.create -> demand.increase(); fix by replacing the
read-then-save with a concurrency-safe upsert: implement a repository-level
upsert/increment operation (e.g. a custom repository method like
incrementCountByPlaceAndCouncil(place, council) that executes a DB-level INSERT
... ON CONFLICT/ON DUPLICATE KEY UPDATE or an UPDATE ... RETURNING then INSERT
fallback) or use a transactional pessimistic lock
(findByPlaceAndCouncilForUpdate) so only one thread creates the
CouncilPartnershipSuggestion and then calls increase(); ensure the new method is
used inside suggestPartnership for each council instead of the current
find-or-save flow.
In
`@src/main/java/com/campus/campus/domain/place/domain/entity/CouncilPartnershipSuggestion.java`:
- Around line 40-44: The increase() method on CouncilPartnershipSuggestion is
not concurrency-safe and can cause lost updates when called concurrently; fix by
either (A) enabling optimistic locking: add a version field (e.g., private
`@Version` Long version) to the CouncilPartnershipSuggestion entity and handle
OptimisticLockingFailure exceptions in the service that calls increase(), or (B)
perform the increment as an atomic DB update from the repository: create a
`@Modifying` repository method such as incrementRequestCountById(Long id) (e.g.,
UPDATE ... SET requestCount = requestCount + 1 WHERE id = :id) and call that
from the service instead of calling entity.increase(), ensuring the update is
executed in a transaction.
- Around line 20-40: Add a DB-level unique constraint on the (place, council)
pair and make requestCount non-null with a default to prevent concurrent
duplicate rows and null counts: update the CouncilPartnershipSuggestion entity
(class CouncilPartnershipSuggestion / `@Table`(name =
"council_partnership_suggestion")) to declare a unique constraint over
(place_id, student_council_id) and mark the requestCount field as non-nullable
(requestCount / `@Column`(nullable = false)) and ensure the schema migration sets
a default value (e.g., 0) for existing rows; also add an appropriate migration
to create the UNIQUE INDEX/constraint and alter the request_count column so the
DB enforces uniqueness and not-null semantics.
In
`@src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java`:
- Around line 29-31: The ReviewRequest DTO exposes the Boolean isVerified field
which lets clients control verification; remove or make this field
write-protected so the server exclusively sets verification based on OCR results
(update ReviewRequest to drop or annotate out isVerified), then ensure the
review creation flow (e.g., ReviewController.createReview /
ReviewService.createReview) accepts the receipt image/receipt data, performs
server-side OCR verification and sets the entity's isVerified flag before
persistence; update any mapping logic that populated isVerified from
ReviewRequest to use the OCR result instead.
In
`@src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptResultDto.java`:
- Around line 6-10: The record ReceiptResultDto has a misnamed component
totalPlace; rename the component to totalPrice in the ReceiptResultDto record
declaration and update all usages (e.g., constructors and deconstruction)
accordingly—specifically adjust the call site in
OcrService.extractReceiptResult() where totalPrice is passed into the
ReceiptResultDto constructor so the parameter names match the new record
component name.
In
`@src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java`:
- Around line 12-25: Conflict: the ErrorCode enum constants NOT_REVIEW_WRITER
and RECEIPT_OCR_FAILED reuse codes 2701/2702 which collide with manager domain
codes; update these two constants in ErrorCode (NOT_REVIEW_WRITER and
RECEIPT_OCR_FAILED) to new unique numeric values that do not overlap with the
manager domain (choose unused 27xx range), and then update all references/usages
of ErrorCode.NOT_REVIEW_WRITER and ErrorCode.RECEIPT_OCR_FAILED throughout the
codebase to use the new numeric values via the enum (no other API changes);
ensure any tests or serialization that depend on the numeric codes are adjusted
accordingly.
In
`@src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java`:
- Line 35: The mapping uses request.isVerified() which is a nullable Boolean and
may NPE when assigned to a primitive boolean in Review; update the ReviewMapper
mapping (the call that sets isVerified, e.g., Review.builder().isVerified(...))
to defensively handle nulls by converting the boxed Boolean to a primitive with
a safe default (for example use Boolean.TRUE.equals(request.isVerified()) or
Optional.ofNullable(request.isVerified()).orElse(false)) so that
Review.isVerified always receives a non-null boolean.
In
`@src/main/java/com/campus/campus/domain/review/application/service/OcrService.java`:
- Around line 55-61: The current code in OcrService logs the raw OCR response
(rawResponse returned by clovaOcrClient.requestReceiptOcr) at INFO level which
may contain sensitive payment or card data; remove that INFO-level log and
either (a) drop logging of rawResponse entirely, or (b) change it to a
DEBUG-level log and redact/mask any sensitive fields before logging (e.g., mask
card numbers, amounts) while leaving parse(rawResponse) and
extractReceiptResult(ReceiptOcrResponse) unchanged; update the log invocation
around rawResponse to use the chosen approach and ensure only non-sensitive or
redacted content is emitted.
- Around line 113-118: The paymentDate extraction in OcrService currently
produces null on parse/missing which leads to NPE later in
ReviewService.findPartnership() when calling paymentDateTime.atStartOfDay();
update OcrService (paymentDate variable creation) to either validate and throw a
clear exception when receipt.paymentInfo() or the parsed date is
missing/invalid, or return an Optional<LocalDate>/use a sentinel so callers
handle absence; ensure callers (notably ReviewService.findPartnership()) no
longer assume non-null—either propagate the exception or add null-safe logic
before calling paymentDateTime.atStartOfDay().
In
`@src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java`:
- Around line 293-306: getAverageOfStars and getReviewCount load all Review
entities into memory (reviewRepository.findALlByPlace_PlaceId) causing
performance/memory issues; add repository-level aggregation queries (e.g.
Optional<Double> findAverageStarByPlaceId(Long placeId) using AVG and long
countByPlaceId(Long placeId) using COUNT) and change getAverageOfStars to call
findAverageStarByPlaceId (use orElse(0.0)) and getReviewCount to call
countByPlaceId (cast/return int if needed), and annotate getAverageOfStars with
`@Transactional`(readOnly = true); replace uses of
reviewRepository.findALlByPlace_PlaceId in these methods with the new repository
methods.
- Around line 272-291: In findPartnership(Long placeId, ReceiptResultDto result,
Long userId) avoid calling user.getMajor(), user.getCollege(), and
user.getSchool() without null checks; check the user's profile first (e.g.,
reuse/isolate the logic from isProfileNotCompleted()) and throw a clear
exception (or return a specific response) when any of major/college/school is
null, or otherwise validate and handle nulls before reading
majorId/collegeId/schoolId so NullPointerException cannot occur.
In
`@src/main/java/com/campus/campus/domain/review/infrastructure/ClovaOcrClient.java`:
- Around line 33-42: In requestReceiptOcr, extractFormat(originalFilename) is
called but its return value is ignored while "png" is hardcoded into the image
map; update requestReceiptOcr to capture the return of extractFormat (e.g.,
String format = extractFormat(originalFilename)), validate or normalize it
(fallback to "png" if null/unsupported), and use that variable when populating
image.put("format", format) so the actual file extension is sent to the OCR API;
keep extractFormat and requestReceiptOcr signatures intact and ensure any
normalization for "jpg"/"jpeg" is handled before setting the map.
In `@src/main/java/com/campus/campus/global/config/ClovaOcrConfig.java`:
- Around line 10-12: The clovaOcrRestTemplate bean currently returns a plain
RestTemplate with no timeouts; update the ClovaOcrConfig.clovaOcrRestTemplate
method to build the RestTemplate via RestTemplateBuilder (inject it or accept as
parameter) and set sensible connect and read timeouts (e.g.,
Duration.ofSeconds(2) for connect and Duration.ofSeconds(10) for read) before
calling build(), so external Clova OCR calls won't block indefinitely.
🧹 Nitpick comments (10)
src/main/java/com/campus/campus/domain/review/domain/entity/Review.java (1)
50-53: Nullability 불일치 — 입력 검증으로 인한 NPE 리스크 완화Line 50-53에서
double원시형으로 변경되었는데,star필드는Double(nullable) 타입으로 정의되어 있습니다. 타입 불일치는 존재하지만,ReviewRequest의star필드에@NotNull+@DecimalMin/@DecimalMax검증이 적용되고 컨트롤러에서@RequestBody@Valid``로 검증되므로 실제 NPE 리스크는 낮습니다. 다만 타입 일관성을 위해 아래와 같이 수정하는 것이 권장됩니다:
Double타입으로 되돌려 필드와 일치시키거나star필드에@Column(nullable = false)추가하여 데이터베이스 제약을 명시하기src/main/resources/application-local.yml (1)
74-77: invoke-url도 환경 변수로 외부화하여 환경별 설정을 일관되게 관리하세요.현재
secret-key는 이미${OCR_KEY}환경 변수를 사용하고 있는 반면,invoke-url은 하드코딩되어 있습니다. NAVER Clova OCR 서비스의 엔드포인트는 환경(dev/prod)이나 리전에 따라 달라질 수 있으므로,invoke-url도 환경 변수로 분리하는 것이 포팅 시 혼선을 방지하고 설정 관리를 일관되게 유지할 수 있습니다.♻️ 수정 제안
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/domain/repository/ReviewRepository.java (1)
68-70:findALlBy...오타/관례 불일치입니다.
Spring Data 네이밍 관례상findAllBy...가 표준입니다. 현재 표기는 가독성 저하 및 파서 동작 혼동 가능성이 있어 표준 표기로 정리하는 것을 권장합니다.🔧 제안 수정안
- List<Review> findALlByPlace_PlaceId(long placeId); + List<Review> findAllByPlace_PlaceId(long placeId);src/main/java/com/campus/campus/domain/review/infrastructure/ClovaOcrClient.java (1)
56-61: 외부 API 호출에 대한 에러 처리 부재
restTemplate.postForEntity호출이 네트워크 오류나 타임아웃 시RestClientException을 던질 수 있습니다. 이를 적절히 처리하여 사용자에게 의미 있는 에러 메시지를 제공하는 것이 좋습니다.♻️ 제안 수정
+import org.springframework.web.client.RestClientException; +import com.campus.campus.domain.review.application.exception.ReceiptOcrFailedException; //호출 -ResponseEntity<String> response = - restTemplate.postForEntity(invokeUrl, request, String.class); -return response.getBody(); +try { + ResponseEntity<String> response = + restTemplate.postForEntity(invokeUrl, request, String.class); + return response.getBody(); +} catch (RestClientException e) { + throw new ReceiptOcrFailedException(); +}src/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PartnershipDetailResponse.java (1)
27-28:reviewSize필드 중복 가능성 검토
reviewSize는reviews.size()로 계산 가능하므로 중복 데이터일 수 있습니다. 다만, 전체 리뷰 수(페이징된 경우 전체 개수)를 나타내는 것이라면 의미가 있습니다. 의도를 명확히 하기 위해 필드명을totalReviewCount로 변경하거나, 단순히 목록 크기라면 제거를 고려해보세요.src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptItemDto.java (1)
3-6: 가격을 문자열로 둘 의도 확인 필요가격을 합산/비교/검증에 사용할 계획이라면 String 대신
BigDecimal(또는 정수 센트 단위)로 두는 편이 안전합니다. 표시용이라면 현재도 문제없습니다.src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java (1)
109-116:@Valid어노테이션 누락
SavedPlaceInfo placeInfo파라미터에@Valid어노테이션이 누락되었습니다.PlaceController의 다른 엔드포인트들에서는SavedPlaceInfo에@Valid를 사용하고 있어 일관성을 위해 추가가 필요합니다.♻️ 제안된 수정
public CommonResponse<ReviewPartnerResponse> upload( `@RequestPart`("file") MultipartFile file, - `@RequestPart`("request") SavedPlaceInfo placeInfo, + `@RequestPart`("request") `@Valid` SavedPlaceInfo placeInfo, `@CurrentUserId` Long userId ) {src/main/java/com/campus/campus/domain/review/application/service/OcrService.java (1)
43-64: 영수증 상호명 미사용 - 검증 로직 추가 권장
extractReceiptResult()에서 영수증의storeName을 필수로 추출하고 있으나,findPartnership()메서드에서 이 값을 사용하거나place.placeName과 비교하지 않습니다. 현재는placeId와paymentDate를 기반으로 제휴 여부를 검증하고 있어 기본 검증은 충분하지만, 추가 방어 계층으로 상호명 일치 검증을 추가하는 것이 좋습니다.// findPartnership 메서드에서 다음과 같이 추가 검증 if (!isStoreNameMatching(result.storeName(), post.getPlace().getPlaceName())) { throw new StoreNameMismatchException(); }src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java (2)
278-284: Optional 처리 개선 가능
if-else패턴 대신Optional의map과orElseGet을 사용하면 더 간결합니다.♻️ 리팩토링 제안
- if (activePost.isPresent()) { - StudentCouncilPost post = activePost.get(); - return getPartnershipDetailInternal(post, user, userLat, userLng); - } else { - return getNormalPlaceDetailInternal(user, place, userLat, userLng); - } + return activePost + .map(post -> (PlaceDetailView) getPartnershipDetailInternal(post, user, userLat, userLng)) + .orElseGet(() -> getNormalPlaceDetailInternal(user, place, userLat, userLng));
286-294: 거리 계산 반올림 방식 일관성
calculateAverageStar에서는BigDecimal을 사용하여 정밀한 반올림을 수행하는 반면,calculateDistance에서는Math.round(distance * 100.0) / 100.0을 사용합니다. 부동소수점 정밀도 문제를 피하려면 일관되게BigDecimal을 사용하는 것이 좋습니다.♻️ 리팩토링 제안
private double calculateDistance(Place place, double lat, double lng) { double distance = GeoUtil.distanceMeter( lat, lng, place.getCoordinate().latitude(), place.getCoordinate().longitude() ); - return Math.round(distance * 100.0) / 100.0; + return BigDecimal.valueOf(distance) + .setScale(2, RoundingMode.HALF_UP) + .doubleValue(); }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (49)
src/main/java/com/campus/campus/domain/council/domain/repository/StudentCouncilRepository.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/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/domain/entity/CouncilPartnershipSuggestion.javasrc/main/java/com/campus/campus/domain/place/domain/entity/UserPartnershipSuggestion.javasrc/main/java/com/campus/campus/domain/place/domain/repository/CouncilPartnershipSuggestionRepository.javasrc/main/java/com/campus/campus/domain/place/domain/repository/PlaceImagesRepository.javasrc/main/java/com/campus/campus/domain/place/domain/repository/UserPartnershipSuggestionRepository.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/ReviewPartnerResponse.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/SimpleReviewResponse.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/repository/ReviewRepository.javasrc/main/java/com/campus/campus/domain/review/infrastructure/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/global/config/ClovaOcrConfig.javasrc/main/resources/application-local.yml
🧰 Additional context used
🧬 Code graph analysis (6)
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (2)
src/main/java/com/campus/campus/domain/place/application/exception/AlreadySuggestedPartnershipException.java (1)
AlreadySuggestedPartnershipException(5-9)src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)
src/main/java/com/campus/campus/domain/place/domain/entity/UserPartnershipSuggestion.java (2)
src/main/java/com/campus/campus/domain/place/domain/entity/CouncilPartnershipSuggestion.java (1)
Entity(20-56)src/main/java/com/campus/campus/domain/review/domain/entity/Review.java (1)
Entity(22-61)
src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (3)
src/main/java/com/campus/campus/domain/councilpost/application/exception/PostImageLimitExceededException.java (1)
PostImageLimitExceededException(5-9)src/main/java/com/campus/campus/domain/review/application/exception/NotPartnershipReceiptException.java (1)
NotPartnershipReceiptException(5-9)src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)
src/main/java/com/campus/campus/domain/review/infrastructure/ClovaOcrClient.java (1)
src/main/java/com/campus/campus/domain/review/application/exception/ReceiptImageFormatException.java (1)
ReceiptImageFormatException(5-9)
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/review/domain/entity/Review.java (1)
Entity(22-61)
src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java (4)
src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)src/main/java/com/campus/campus/domain/councilpost/application/exception/PostNotFoundException.java (1)
PostNotFoundException(5-9)src/main/java/com/campus/campus/domain/councilpost/application/exception/PlaceInfoNotFoundException.java (1)
PlaceInfoNotFoundException(5-9)src/main/java/com/campus/campus/global/util/geocoder/GeoUtil.java (1)
GeoUtil(3-32)
🔇 Additional comments (41)
src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java (1)
19-20: LGTM!새로운
OCR_SUCCESS응답 코드가 기존 enum 패턴과 일관되게 추가되었습니다. OCR 인식 성공에 대한 응답 코드로 적절합니다.src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/PriceInfo.java (1)
3-6: DTO 구조가 명확합니다.
가격/단가를 분리해 전달하는 불변 record 구성은 OCR 응답 모델링에 적절합니다.src/main/java/com/campus/campus/domain/review/application/exception/NotPartnershipReceiptException.java (1)
5-8: 예외 매핑이 일관적입니다.
기본 생성자에서 ErrorCode를 직접 매핑해 표준 예외 패턴을 잘 따르고 있습니다.src/main/java/com/campus/campus/domain/review/application/dto/response/PlaceStarAvgRow.java (1)
3-5: 집계 결과 DTO로 적절합니다.
장소별 평균 별점 반환을 record로 분리한 점이 깔끔합니다.src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewPartnerResponse.java (1)
3-8: 별점 널 가능성만 확인해주세요.
매핑 소스가Double이고 널 가능성이 있다면 primitivedouble언박싱에서 NPE가 날 수 있습니다. 별점이 항상 채워지는지 확인 부탁드립니다.src/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.java (1)
13-24: 에러 코드 확장이 자연스럽습니다.
신규 코드 추가와 메시지 필드 도입이 일관된 에러 응답 구조를 유지합니다.src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/TextField.java (1)
3-10: DTO 구조가 명확하고 사용 목적에 부합합니다.
OCR 응답 구조를 표현하기에 충분히 간결합니다.src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/StoreInfo.java (1)
3-5: 간결한 DTO 정의로 충분합니다.
OCR 응답 계층과 잘 맞습니다.src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/TotalPrice.java (1)
3-5: 구조가 명확하고 문제 없습니다.
가격 정보 DTO로 적절합니다.src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/PaymentInfo.java (1)
3-7: LGTM!OCR 응답에서 결제 정보를 담는 간결한 record DTO입니다. 구조가 명확하고 불변성이 보장됩니다.
src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptOcrItem.java (1)
3-7: LGTM!영수증의 개별 항목(품목명, 가격)을 담는 record DTO로 적절하게 구현되었습니다.
src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/SubResult.java (1)
5-8: LGTM!OCR 결과의 하위 항목 목록을 담는 래퍼 record입니다. ReceiptResult와의 계층 구조가 잘 설계되었습니다.
src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptResult.java (1)
5-11: LGTM!영수증 OCR 결과의 핵심 DTO로, 매장 정보, 결제 정보, 총액, 하위 항목 목록을 포함합니다. OCR API 응답 구조에 맞게 잘 설계되었습니다.
totalPrice가 직접 필드로도,paymentInfo내부에도 존재하는데, 이는 OCR API 응답 형식에 따른 것으로 보입니다.src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptWrapper.java (1)
3-6: LGTM!ReceiptResult를 감싸는 래퍼 record로, OCR API 응답의 JSON 구조에 맞게 적절히 구현되었습니다.
src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ImageResult.java (1)
1-5: DTO 구조가 명확합니다.
OCR 응답 모델과 일관성 있게 잘 정의됐습니다.src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptOcrResponse.java (1)
1-8: 응답 래퍼 정의 OK입니다.
필드 구성도 단순하고 의도에 맞습니다.src/main/java/com/campus/campus/domain/review/application/dto/response/SimpleReviewResponse.java (1)
1-11: 간단 리뷰 응답 DTO 정의 좋습니다.
필드 구성이 명확하고 사용하기 편합니다.src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java (1)
37-48: 이 메서드의 빈 Set 처리는 이미 호출부에서 관리되고 있습니다.
getAverageListOfStars메서드에서placeIds가 null이거나 비어있으면 조회 전에 즉시 빈 맵을 반환하므로,findAverageStarsByPlaceIds에는 항상 유효한 데이터만 전달됩니다. 별도의 처리가 필요하지 않습니다.Likely an incorrect or invalid review comment.
src/main/java/com/campus/campus/domain/place/application/dto/response/PlaceDetailView.java (1)
3-26: 인터페이스 설계가 적절합니다.계약 정의가 깔끔하고 구현체들(PlaceDetailResponse, PartnershipDetailResponse)과 잘 맞습니다.
한 가지 고려 사항:
latitude/longitude는Double(박싱 타입)을 사용하고,star/distance는double(원시 타입)을 사용합니다. 구현체에서 null 값이 반환될 가능성이 있다면 일관성을 위해 타입 선택을 통일하는 것이 좋습니다.src/main/java/com/campus/campus/domain/place/application/dto/response/PlaceDetailResponse.java (1)
7-25:reviewSize와reviews.size()간 데이터 불일치 가능성
reviewSize필드가reviews리스트와 별도로 존재합니다. 만약reviewSize가 전체 리뷰 수(페이징된reviews리스트가 아닌)를 나타내기 위한 것이라면 현재 구조가 적절합니다. 그렇지 않다면reviews.size()에서 파생하는 것을 고려해 보세요.의도를 확인해 주세요:
reviewSize가 전체 리뷰 수를 의미하는지, 아니면 반환된reviews리스트의 크기를 의미하는지 명확히 해주세요.src/main/java/com/campus/campus/domain/review/infrastructure/ClovaOcrClient.java (1)
64-80: LGTM!파일 확장자 검증 로직이 명확하고 지원하지 않는 형식에 대해 적절한 예외를 던집니다.
src/main/java/com/campus/campus/domain/review/application/exception/ReceiptOcrFailedException.java (1)
1-9: 구현이 적절합니다.예외 클래스가 일관된 패턴으로 잘 구현되어 있습니다.
ReceiptImageFormatException과 마찬가지로ErrorCode가 동일 패키지에 있는지 확인이 필요합니다.src/main/java/com/campus/campus/domain/place/application/exception/AlreadySuggestedPartnershipException.java (1)
1-9: LGTM!제휴 중복 신청 방지를 위한 예외 클래스가 적절하게 구현되었습니다.
HttpStatus.CONFLICT(409)와 함께 사용되는 것은 "이미 존재하는 리소스" 시나리오에 적합합니다.src/main/java/com/campus/campus/domain/review/application/exception/ReceiptFileConvertException.java (1)
1-9: LGTM!OCR 처리 중 파일 변환 실패를 나타내는 예외 클래스가 일관된 패턴으로 구현되었습니다.
src/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PartnershipDetailResponse.java (2)
9-29: 전반적인 DTO 구조는 적절합니다.
PlaceDetailView인터페이스 구현과 함께 장소 상세 정보, 제휴 정보, 리뷰 목록을 포함하는 응답 구조가 잘 설계되어 있습니다.
10-10: Java record의 boolean 필드 네이밍 패턴 확인Jackson 기본 설정에서
isPartnership(),isLiked()getter는 JSON 직렬화 시partnership,liked로 변환됩니다. 이는 Jackson의 표준 동작입니다.현재 코드베이스의 다른 response DTO들도 유사한 패턴을 사용하고 있으며, 별도의 Jackson 설정 오버라이드가 없습니다. 의도한 JSON 필드명이
isPartnership,isLiked라면@JsonProperty어노테이션 추가를 고려하세요.src/main/java/com/campus/campus/domain/review/application/exception/ReceiptImageFormatException.java (1)
1-9: ErrorCode import은 필요하지 않습니다. ErrorCode 클래스가 ReceiptImageFormatException과 동일한 패키지(com.campus.campus.domain.review.application.exception)에 있기 때문에 Java의 동일 패키지 접근 규칙에 따라 명시적 import 없이도 참조 가능합니다. 현재 코드는 정상입니다.Likely an incorrect or invalid review comment.
src/main/java/com/campus/campus/domain/place/domain/repository/CouncilPartnershipSuggestionRepository.java (1)
11-13: 구현 깔끔합니다.조회 메서드 시그니처도 명확합니다.
src/main/java/com/campus/campus/domain/place/domain/repository/PlaceImagesRepository.java (1)
19-25: 쿼리 의도 명확하고 깔끔합니다.정렬 기준까지 포함해 응답 안정성이 보장됩니다.
src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java (1)
49-56: LGTM!
toSimpleReviewResponse메서드가 적절하게 구현되었습니다. 빌더 패턴을 사용하여 간결하게 응답 객체를 생성합니다.src/main/java/com/campus/campus/domain/place/domain/repository/UserPartnershipSuggestionRepository.java (1)
1-12: LGTM!리포지토리 인터페이스가 올바르게 정의되었습니다.
existsByUserAndPlace파생 쿼리 메서드는 Spring Data JPA 네이밍 규칙을 따르고 있습니다.src/main/java/com/campus/campus/domain/place/presentation/PlaceResponseCode.java (1)
18-20: LGTM!새로운 응답 코드가 기존 패턴에 맞게 잘 추가되었습니다.
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
339-361: 코드가 올바르게 구현되었습니다.null 체크가 적절하게 수행되고 있으며, 사용자의 학과/단과대/학교에 해당하는 학생회를 조회합니다.
현재 구현에서는 최대 3개의 별도 DB 쿼리가 발생합니다. StudentCouncilRepository에 배치 조회 메서드가 없으므로, 향후 성능 최적화가 필요한 경우 단일 쿼리로 통합하는 방안을 검토할 수 있습니다.
src/main/java/com/campus/campus/domain/place/domain/entity/UserPartnershipSuggestion.java (1)
20-47: LGTM!엔티티 구조가 올바르게 구현되었습니다.
CouncilPartnershipSuggestion과 일관된 패턴을 따르고 있으며, LAZY fetch 전략과 정적 팩토리 메서드가 적절히 사용되었습니다.src/main/java/com/campus/campus/domain/place/presentation/PlaceController.java (2)
191-199: LGTM!새로운 제휴 신청 엔드포인트가 기존 패턴(
/like-place)과 일관되게 구현되었습니다.@Valid어노테이션이 적절히 사용되었습니다.
146-164: 파라미터 순서가 서비스 메서드 시그니처와 정확하게 일치합니다.getPlaceDetails(userId, placeId, double userLat, double userLng)메서드 시그니처와 controller의 호출getPlaceDetails(userId, placeId, lat, lng)가 순서대로 매핑되므로 버그가 없습니다.src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java (2)
176-199: 구현이 올바릅니다.내부 헬퍼 메서드로서 역할이 명확하고, 필요한 데이터를 적절히 조합하여 응답을 구성합니다.
232-251: LGTM!비제휴 장소에 대한 상세 정보를 올바르게 구성합니다.
getPartnershipDetailInternal과 일관된 구조를 유지합니다.src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java (3)
77-99: LGTM!
PartnershipDetailResponse로의 변환 로직이 올바르게 구현되었습니다. 모든 필드가 적절히 매핑됩니다.
101-119: LGTM!
PlaceDetailResponse생성 로직이toPartnershipResponse와 일관된 구조로 잘 구현되었습니다.isPartnership이false로 하드코딩되어 비제휴 장소임을 명확히 표시합니다.
121-129: LGTM!리뷰 파트너 응답 변환이 올바르게 구현되었습니다.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| Optional<StudentCouncil> findByMajor_MajorId(Long majorId); | ||
|
|
||
| Optional<StudentCouncil> findByCollege_CollegeId(Long collegeId); | ||
|
|
||
| Optional<StudentCouncil> findBySchool_SchoolId(Long schoolId); |
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:
# First, find and read the StudentCouncilRepository file
find . -name "StudentCouncilRepository.java" -type fRepository: our-campUS/our-campUS-BE
Length of output: 164
🏁 Script executed:
# Look for the file in the provided path
cat -n src/main/java/com/campus/campus/domain/council/domain/repository/StudentCouncilRepository.javaRepository: our-campUS/our-campUS-BE
Length of output: 2482
🏁 Script executed:
# Search for usages of the three new methods
rg -nP --type=java '(findByMajor_MajorId|findByCollege_CollegeId|findBySchool_SchoolId)' -C3Repository: our-campUS/our-campUS-BE
Length of output: 6498
🏁 Script executed:
# Read PlaceService.java to see the full context of how councils list is used
sed -n '330,380p' src/main/java/com/campus/campus/domain/place/application/service/PlaceService.javaRepository: our-campUS/our-campUS-BE
Length of output: 1387
🏁 Script executed:
# Search for where resolveCouncils is called to see how the returned councils are used
rg -nP --type=java 'resolveCouncils' -C5Repository: our-campUS/our-campUS-BE
Length of output: 2781
미승인/삭제된 학생회가 비즈니스 로직에 포함될 수 있습니다.
세 메서드가 PlaceService.resolveCouncils()에서 호출되어 협력제안 기록을 생성할 때 사용됩니다. 기존 저장소 메서드 대부분이 managerApproved 및 deletedAt 필터를 포함하는데, 이 메서드들은 누락되어 있어 삭제되거나 미승인된 학생회가 협력제안 대상으로 포함될 위험이 있습니다.
✅ 제안 수정안 (필터 포함)
- Optional<StudentCouncil> findByMajor_MajorId(Long majorId);
- Optional<StudentCouncil> findByCollege_CollegeId(Long collegeId);
- Optional<StudentCouncil> findBySchool_SchoolId(Long schoolId);
+ Optional<StudentCouncil> findByMajor_MajorIdAndManagerApprovedIsTrueAndDeletedAtIsNull(Long majorId);
+ Optional<StudentCouncil> findByCollege_CollegeIdAndManagerApprovedIsTrueAndDeletedAtIsNull(Long collegeId);
+ Optional<StudentCouncil> findBySchool_SchoolIdAndManagerApprovedIsTrueAndDeletedAtIsNull(Long schoolId);📝 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.
| Optional<StudentCouncil> findByMajor_MajorId(Long majorId); | |
| Optional<StudentCouncil> findByCollege_CollegeId(Long collegeId); | |
| Optional<StudentCouncil> findBySchool_SchoolId(Long schoolId); | |
| Optional<StudentCouncil> findByMajor_MajorIdAndManagerApprovedIsTrueAndDeletedAtIsNull(Long majorId); | |
| Optional<StudentCouncil> findByCollege_CollegeIdAndManagerApprovedIsTrueAndDeletedAtIsNull(Long collegeId); | |
| Optional<StudentCouncil> findBySchool_SchoolIdAndManagerApprovedIsTrueAndDeletedAtIsNull(Long schoolId); |
🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/council/domain/repository/StudentCouncilRepository.java`
around lines 48 - 52, The three repository methods findByMajor_MajorId,
findByCollege_CollegeId, and findBySchool_SchoolId can return unapproved or
deleted councils used by PlaceService.resolveCouncils; update these methods to
only return councils with managerApproved = true and deletedAt = null (e.g.,
rename/replace them with
findByMajor_MajorIdAndManagerApprovedTrueAndDeletedAtIsNull,
findByCollege_CollegeIdAndManagerApprovedTrueAndDeletedAtIsNull, and
findBySchool_SchoolIdAndManagerApprovedTrueAndDeletedAtIsNull or add equivalent
`@Query` filters) so resolveCouncils only sees approved, non-deleted
StudentCouncil entities.
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.
@1224kang 요거 각각 DeletedAtIsNull 추가해주세요!!
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.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
...ava/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java
Show resolved
Hide resolved
...ava/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java
Outdated
Show resolved
Hide resolved
| @Query(""" | ||
| SELECT p | ||
| FROM StudentCouncilPost p | ||
| JOIN p.writer w | ||
| WHERE p.place = :place | ||
| AND p.startDateTime <= :now | ||
| AND p.endDateTime >= :now | ||
| AND ( | ||
| (w.councilType = :majorType AND w.major.majorId = :majorId) | ||
| OR (w.councilType = :collegeType AND w.college.collegeId = :collegeId) | ||
| OR (w.councilType = :schoolType AND w.school.schoolId = :schoolId) | ||
| ) | ||
| ORDER BY p.endDateTime DESC | ||
| """) | ||
| Optional<StudentCouncilPost> findActiveByPlaceAndUserScope( | ||
| @Param("place") Place place, | ||
| @Param("now") LocalDateTime now, | ||
| @Param("majorType") CouncilType majorType, | ||
| @Param("majorId") Long majorId, | ||
| @Param("collegeType") CouncilType collegeType, | ||
| @Param("collegeId") Long collegeId, | ||
| @Param("schoolType") CouncilType schoolType, | ||
| @Param("schoolId") Long schoolId | ||
| ); |
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.
w.deletedAt IS NULL 조건 누락 및 Optional 반환 주의
- 이 쿼리에도
w.deletedAt IS NULL조건이 누락되어 있습니다. Optional을 반환하지만 쿼리에LIMIT이 없어 여러 행이 반환될 수 있습니다. Spring Data JPA가 첫 번째 결과를 반환하지만, 불필요한 데이터를 조회할 수 있습니다.
🐛 제안 수정
`@Query`("""
SELECT p
FROM StudentCouncilPost p
JOIN p.writer w
WHERE p.place = :place
AND p.startDateTime <= :now
AND p.endDateTime >= :now
+ AND w.deletedAt IS NULL
AND (
(w.councilType = :majorType AND w.major.majorId = :majorId)
OR (w.councilType = :collegeType AND w.college.collegeId = :collegeId)
OR (w.councilType = :schoolType AND w.school.schoolId = :schoolId)
)
ORDER BY p.endDateTime DESC
""")
+Optional<StudentCouncilPost> findActiveByPlaceAndUserScope(
+ ...
+ Pageable pageable // Pageable.ofSize(1) 전달하여 LIMIT 적용
+);또는 메서드가 Pageable 없이 단일 결과를 반환해야 한다면, ORDER BY 후 LIMIT 1을 명시적으로 추가하거나 네이티브 쿼리를 사용하세요.
🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java`
around lines 439 - 462, The JPQL query in findActiveByPlaceAndUserScope omits
the soft-delete filter and can return multiple rows despite returning Optional;
add the writer null-check "AND w.deletedAt IS NULL" into the WHERE clause
(keeping aliases p and w) and ensure a single-result semantics by either adding
an explicit single-result constraint (use a native query with "LIMIT 1") or
change the repository signature to return a List/Pageable or use Spring Data's
first result support (e.g., rename to findFirstBy... or use Pageable with size
1) so the method and query agree.
src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/review/application/service/OcrService.java
Outdated
Show resolved
Hide resolved
| @Transactional(readOnly = true) | ||
| public ReviewPartnerResponse findPartnership(Long placeId, ReceiptResultDto result, Long userId) { | ||
| User user = userRepository.findById(userId) | ||
| .orElseThrow(UserNotFoundException::new); | ||
| Long majorId = user.getMajor().getMajorId(); | ||
| Long collegeId = user.getCollege().getCollegeId(); | ||
| Long schoolId = user.getSchool().getSchoolId(); | ||
|
|
||
| LocalDate paymentDateTime = result.paymentDate(); | ||
| LocalDateTime time = paymentDateTime.atStartOfDay(); //시간은 우선 임의로 | ||
|
|
||
| //제휴기간 내에 결제 했는지 확인 | ||
| StudentCouncilPost post = studentCouncilPostRepository.findValidPartnershipForUserScope( | ||
| placeId, time, majorId, collegeId, schoolId, CouncilType.MAJOR_COUNCIL, | ||
| CouncilType.COLLEGE_COUNCIL, CouncilType.SCHOOL_COUNCIL | ||
| ).orElseThrow(NotPartnershipReceiptException::new); | ||
|
|
||
| double averageStar = getAverageOfStars(placeId); | ||
| return placeMapper.toReviewPartnerResponse(post, post.getPlace(), averageStar); | ||
| } |
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 -e java -type f | grep -i "User.java" | head -20Repository: our-campUS/our-campUS-BE
Length of output: 238
🏁 Script executed:
rg "class User" -t java -A 20 | head -100Repository: our-campUS/our-campUS-BE
Length of output: 10351
🏁 Script executed:
fd -e java User.java -o user/User.java -o domain/user/User.java | xargs -I {} wc -l {}Repository: our-campUS/our-campUS-BE
Length of output: 215
🏁 Script executed:
cat -n src/main/java/com/campus/campus/domain/user/domain/entity/User.javaRepository: our-campUS/our-campUS-BE
Length of output: 3384
null 체크 추가 필요
user.getMajor(), user.getCollege(), user.getSchool()이 null일 수 있습니다. User 엔티티의 이 필드들은 @NotNull 제약이 없으며, isProfileNotCompleted() 메서드(라인 91)에서 school과 major가 null일 수 있음을 명시적으로 확인하고 있습니다. 라인 276-278에서 이들 값을 null 체크 없이 호출하면 NullPointerException이 발생합니다. 적절한 null 체크를 추가하거나, 사용자의 학적정보 입력이 필수 선행조건임을 명시하세요.
🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java`
around lines 272 - 291, In findPartnership(Long placeId, ReceiptResultDto
result, Long userId) avoid calling user.getMajor(), user.getCollege(), and
user.getSchool() without null checks; check the user's profile first (e.g.,
reuse/isolate the logic from isProfileNotCompleted()) and throw a clear
exception (or return a specific response) when any of major/college/school is
null, or otherwise validate and handle nulls before reading
majorId/collegeId/schoolId so NullPointerException cannot occur.
| public double getAverageOfStars(Long placeId) { | ||
| List<Review> reviews = reviewRepository.findALlByPlace_PlaceId(placeId); | ||
| double averageStar = reviews.stream() | ||
| .mapToDouble(Review::getStar) | ||
| .average() | ||
| .orElse(0.0); | ||
| return averageStar; | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public int getReviewCount(Long placeId) { | ||
| List<Review> reviews = reviewRepository.findALlByPlace_PlaceId(placeId); | ||
| return reviews.size(); | ||
| } |
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.
🛠️ Refactor suggestion | 🟠 Major
성능 문제: 전체 리뷰를 메모리에 로드함
getAverageOfStars와 getReviewCount 메서드가 평균과 개수를 계산하기 위해 모든 리뷰를 메모리에 로드합니다. 리뷰 수가 많아지면 성능 저하와 메모리 문제가 발생할 수 있습니다. Repository에서 AVG와 COUNT 쿼리를 사용하는 것이 좋습니다.
또한 getAverageOfStars에 @Transactional(readOnly = true) 어노테이션이 누락되었습니다.
♻️ 제안된 수정
Repository에 다음 메서드들을 추가하세요:
`@Query`("SELECT AVG(r.star) FROM Review r WHERE r.place.placeId = :placeId")
Optional<Double> findAverageStarByPlaceId(`@Param`("placeId") Long placeId);
`@Query`("SELECT COUNT(r) FROM Review r WHERE r.place.placeId = :placeId")
long countByPlaceId(`@Param`("placeId") Long placeId);그리고 서비스 메서드를 다음과 같이 수정하세요:
+@Transactional(readOnly = true)
public double getAverageOfStars(Long placeId) {
- List<Review> reviews = reviewRepository.findALlByPlace_PlaceId(placeId);
- double averageStar = reviews.stream()
- .mapToDouble(Review::getStar)
- .average()
- .orElse(0.0);
- return averageStar;
+ return reviewRepository.findAverageStarByPlaceId(placeId).orElse(0.0);
}
`@Transactional`(readOnly = true)
public int getReviewCount(Long placeId) {
- List<Review> reviews = reviewRepository.findALlByPlace_PlaceId(placeId);
- return reviews.size();
+ return (int) reviewRepository.countByPlaceId(placeId);
}🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java`
around lines 293 - 306, getAverageOfStars and getReviewCount load all Review
entities into memory (reviewRepository.findALlByPlace_PlaceId) causing
performance/memory issues; add repository-level aggregation queries (e.g.
Optional<Double> findAverageStarByPlaceId(Long placeId) using AVG and long
countByPlaceId(Long placeId) using COUNT) and change getAverageOfStars to call
findAverageStarByPlaceId (use orElse(0.0)) and getReviewCount to call
countByPlaceId (cast/return int if needed), and annotate getAverageOfStars with
`@Transactional`(readOnly = true); replace uses of
reviewRepository.findALlByPlace_PlaceId in these methods with the new repository
methods.
src/main/java/com/campus/campus/domain/review/infrastructure/ClovaOcrClient.java
Show resolved
Hide resolved
src/main/java/com/campus/campus/global/config/ClovaOcrConfig.java
Outdated
Show resolved
Hide resolved
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: 5
🤖 Fix all issues with AI agents
In
`@src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java`:
- Around line 256-287: In getPlaceDetails, calling
user.getMajor()/getCollege()/getSchool() can NPE when academic info is null;
before invoking studentCouncilPostRepository.findActiveByPlaceAndUserScope, call
the existing validateAcademicInfo(user) (or inline the same null checks) to
ensure user.getMajor(), user.getCollege(), and user.getSchool() are present and
throw the same validation exception used elsewhere; then proceed to call
studentCouncilPostRepository.findActiveByPlaceAndUserScope and the subsequent
logic.
In
`@src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java`:
- Around line 197-206: The UserPartnershipSuggestion entity lacks a unique
constraint on (user_id, place_id) causing TOCTOU races; add a class-level unique
constraint in UserPartnershipSuggestion (e.g., `@Table`(uniqueConstraints =
`@UniqueConstraint`(columnNames = {"user_id","place_id"}))) to enforce DB-level
uniqueness, and in PlaceService.suggestPartnership() wrap the
userPartnershipSuggestionRepository.save(UserPartnershipSuggestion.create(user,
place)) call to catch DataIntegrityViolationException (or the specific
constraint violation) and translate it to AlreadySuggestedPartnershipException;
keep the pre-check using existsByUserAndPlace for fast-fail but rely on the DB
constraint + exception handler to cover concurrent inserts.
In
`@src/main/java/com/campus/campus/domain/review/application/service/OcrService.java`:
- Around line 68-76: The parse method in OcrService currently logs the full raw
JSON on parse failure which may expose sensitive receipt data; update the catch
block in parse(String json) to avoid emitting the raw payload—instead compute
and log a safe identifier (e.g., a SHA-256 or hex digest of the json) and/or a
small truncated snippet (first 50 chars) or masked content, include the
exception e in the log, and then throw ReceiptOcrFailedException as before;
locate the parse method in OcrService and replace the log.error("[OCR PARSE
FAILED] raw={}", json, e) call with a sanitized log entry that references the
computed digest/snippet and the exception (while keeping objectMapper.readValue
and ReceiptOcrFailedException behavior unchanged).
In
`@src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.java`:
- Around line 15-22: The class ReviewImage is using `@SuperBuilder` despite no
inheritance from BaseEntity; either restore BaseEntity to retain audit fields
(createdAt/updatedAt) by having ReviewImage extend BaseEntity and keep
`@SuperBuilder`, or if audit inheritance was intentionally removed, replace
`@SuperBuilder` with `@Builder` and remove any now-unused imports/constructs;
confirm consistency with other entities like Review and
CouncilPartnershipSuggestion and apply the chosen change to the ReviewImage
class and its constructors accordingly.
In
`@src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java`:
- Line 72: In the ReviewRepository interface rename the mis-typed query method
findALlByPlace_PlaceId to findAllByPlace_PlaceId (preserve the return type
List<Review> and parameter long placeId) so it conforms to Spring Data JPA
naming; update any call sites referencing
ReviewRepository.findALlByPlace_PlaceId to the corrected
ReviewRepository.findAllByPlace_PlaceId.
♻️ Duplicate comments (4)
src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java (1)
14-20: 에러 코드 충돌 문제가 여전히 존재합니다.이전 리뷰에서 지적된 에러 코드 충돌이 해결되지 않았습니다.
NOT_REVIEW_WRITER(2701)와RECEIPT_OCR_FAILED(2702)가 manager 도메인의MANAGER_NOT_FOUND(2701),PASSWORD_NOT_CORRECT(2702)와 충돌합니다.에러 로깅 및 모니터링 시 혼동을 방지하려면 리뷰 도메인 에러 코드를 2800번대 등 사용되지 않는 범위로 변경해야 합니다.
#!/bin/bash # manager 도메인의 에러 코드 확인 rg -n "2701|2702" --type=java -C 2src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (1)
272-291: null 체크 추가 필요
user.getMajor(),user.getCollege(),user.getSchool()이 null일 수 있습니다. User 엔티티의isProfileNotCompleted()메서드에서 이 필드들이 null일 수 있음을 명시하고 있으므로, 라인 276-278에서 null 체크 없이 호출하면NullPointerException이 발생할 수 있습니다.🐛 제안된 수정
public ReviewPartnerResponse findPartnership(Long placeId, ReceiptResultDto result, Long userId) { User user = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); + + if (user.getMajor() == null || user.getCollege() == null || user.getSchool() == null) { + throw new AcademicInfoNotSetException(); + } + Long majorId = user.getMajor().getMajorId(); Long collegeId = user.getCollege().getCollegeId(); Long schoolId = user.getSchool().getSchoolId();src/main/java/com/campus/campus/domain/review/application/service/OcrService.java (1)
57-63: 민감 정보 로깅 주의라인 59에서 원시 OCR 응답을 INFO 레벨로 로깅하고 있습니다. 영수증에는 결제 정보, 카드 번호 일부 등 민감한 정보가 포함될 수 있어 프로덕션 환경에서 보안 및 개인정보 보호 문제가 발생할 수 있습니다.
🔒 제안된 수정
//ocr String rawResponse = clovaOcrClient.requestReceiptOcr(imageBytes, file.getOriginalFilename()); -log.info("[OCR RAW RESPONSE] {}", rawResponse); +log.debug("[OCR RAW RESPONSE] {}", rawResponse);src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
212-219: PESSIMISTIC_WRITE만으로는 “미존재 행” 동시 생성 경쟁을 막지 못합니다.행이 없을 때는 락이 걸리지 않아서 동시에 insert 시도 후 중복/예외가 발생할 수 있습니다. DB upsert(권장) 또는 유니크 제약 +
saveAndFlush+ 재조회/증가 재시도로 보완해 주세요.🔧 수정 제안
for (StudentCouncil council : councils) { - CouncilPartnershipSuggestion demand = - partnershipSuggestionRepository.findForUpdate(place, council) - .orElseGet(() -> - partnershipSuggestionRepository.save( - CouncilPartnershipSuggestion.create(place, council) - )); - demand.increase(); + try { + CouncilPartnershipSuggestion demand = + partnershipSuggestionRepository.findForUpdate(place, council) + .orElseGet(() -> + partnershipSuggestionRepository.saveAndFlush( + CouncilPartnershipSuggestion.create(place, council) + )); + demand.increase(); + } catch (DataIntegrityViolationException e) { + CouncilPartnershipSuggestion demand = + partnershipSuggestionRepository.findForUpdate(place, council) + .orElseThrow(() -> new IllegalStateException("CouncilPartnershipSuggestion 생성 실패")); + demand.increase(); + } }Spring Data JPA PESSIMISTIC_WRITE lock behavior when no row exists
🧹 Nitpick comments (5)
src/main/java/com/campus/campus/domain/review/infrastructure/ClovaOcrClient.java (1)
33-42: 이미지 포맷 추출 문제가 해결되었습니다. 단, 이미지 이름 하드코딩 검토 필요.이전 리뷰에서 지적된
extractFormat반환값 미사용 문제가 수정되었습니다.다만
name필드가 여전히"receipt_test2"로 하드코딩되어 있습니다. 디버깅 및 로그 추적을 위해 고유 식별자 사용을 권장합니다.♻️ 수정 제안
Map<String, Object> image = new HashMap<>(); image.put("format", format); image.put("data", base64Image); - image.put("name", "receipt_test2"); + image.put("name", UUID.randomUUID().toString());src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (1)
293-295:@Transactional(readOnly = true)어노테이션 누락Repository 레벨 집계 쿼리 사용으로 성능 문제는 해결되었지만, 다른 조회 메서드들과의 일관성을 위해
@Transactional(readOnly = true)어노테이션 추가를 권장합니다.♻️ 제안된 수정
+@Transactional(readOnly = true) public double getAverageOfStars(Long placeId) { return reviewRepository.findAverageStarByPlaceId(placeId).orElse(0.0); }src/main/java/com/campus/campus/domain/review/application/service/OcrService.java (1)
153-164: null 반환 대신 예외를 던지는 것이 더 명확합니다.
parseDate가 null/blank 입력에 대해 null을 반환하고 있어, 호출 측에서orElseThrow로 처리됩니다. 그러나 유효하지 않은 형식은ReceiptDateParseException을 던지면서 빈 입력은 null을 반환하는 것이 일관성이 떨어집니다. 빈 입력도 예외를 던지거나, 적어도 이 동작에 대한 주석을 추가하는 것을 고려하세요.♻️ 제안된 수정 (일관된 예외 처리)
private LocalDate parseDate(String text) { - if (text == null || text.isBlank()) - return null; + if (text == null || text.isBlank()) { + throw new ReceiptDateParseException(); + } // 숫자가 아닌 문자 제거 String normalized = text.replaceAll("[^0-9]", ""); + if (normalized.isEmpty()) { + throw new ReceiptDateParseException(); + } try { return LocalDate.parse(normalized, DateTimeFormatter.BASIC_ISO_DATE); } catch (DateTimeParseException e) { log.warn("[OCR DATE PARSE FAILED] text={}", text, e); throw new ReceiptDateParseException(); } }src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java (1)
176-199: private 메서드 내 방어적 null 체크 고려
getPartnershipDetailInternal에서post.getPlace()가 null일 경우를 방어적으로 처리하면 더 안전합니다. 현재 호출자들이 null 체크를 수행하지만, 향후 유지보수 시 실수를 방지할 수 있습니다.♻️ 제안된 수정
private PartnershipDetailResponse getPartnershipDetailInternal( StudentCouncilPost post, User user, double userLat, double userLng ) { Place place = post.getPlace(); + if (place == null) { + throw new PlaceInfoNotFoundException(); + } double distance = calculateDistance(place, userLat, userLng);src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
339-361: 중복 council 방지로 카운트 중복 증가를 예방할 수 있습니다.동일 council이 여러 경로(major/college/school)로 반환될 가능성이 있다면
Set으로 중복을 제거해 안전하게 처리하는 편이 좋습니다(도메인 상 중복이 불가능하다면 생략 가능).♻️ 선택적 개선 예시
- private List<StudentCouncil> resolveCouncils(User user) { - List<StudentCouncil> councils = new ArrayList<>(); + private List<StudentCouncil> resolveCouncils(User user) { + Set<StudentCouncil> councils = new LinkedHashSet<>(); @@ - return councils; + return new ArrayList<>(councils); }(추가 import 필요:
java.util.LinkedHashSet,java.util.Set)
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.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/domain/entity/CouncilPartnershipSuggestion.javasrc/main/java/com/campus/campus/domain/place/domain/repository/CouncilPartnershipSuggestionRepository.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptResultDto.javasrc/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.javasrc/main/java/com/campus/campus/domain/review/application/exception/ReceiptDateParseException.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/ReviewImage.javasrc/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.javasrc/main/java/com/campus/campus/domain/review/infrastructure/ClovaOcrClient.javasrc/main/java/com/campus/campus/global/config/ClovaOcrConfig.javasrc/main/java/com/campus/campus/global/config/PermitUrlConfig.java
💤 Files with no reviewable changes (1)
- src/main/java/com/campus/campus/global/config/PermitUrlConfig.java
🚧 Files skipped from review as they are similar to previous changes (3)
- src/main/java/com/campus/campus/domain/place/domain/entity/CouncilPartnershipSuggestion.java
- src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptResultDto.java
- src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java
🧰 Additional context used
🧬 Code graph analysis (5)
src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.java (2)
src/main/java/com/campus/campus/domain/place/domain/entity/CouncilPartnershipSuggestion.java (1)
Entity(22-60)src/main/java/com/campus/campus/domain/review/domain/entity/Review.java (1)
Entity(22-61)
src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java (4)
src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)src/main/java/com/campus/campus/domain/councilpost/application/exception/PostNotFoundException.java (1)
PostNotFoundException(5-9)src/main/java/com/campus/campus/domain/councilpost/application/exception/PlaceInfoNotFoundException.java (1)
PlaceInfoNotFoundException(5-9)src/main/java/com/campus/campus/global/util/geocoder/GeoUtil.java (1)
GeoUtil(3-32)
src/main/java/com/campus/campus/domain/review/infrastructure/ClovaOcrClient.java (1)
src/main/java/com/campus/campus/domain/review/application/exception/ReceiptImageFormatException.java (1)
ReceiptImageFormatException(5-9)
src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (2)
src/main/java/com/campus/campus/domain/review/application/exception/NotPartnershipReceiptException.java (1)
NotPartnershipReceiptException(5-9)src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (2)
src/main/java/com/campus/campus/domain/place/application/exception/AlreadySuggestedPartnershipException.java (1)
AlreadySuggestedPartnershipException(5-9)src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)
🔇 Additional comments (14)
src/main/java/com/campus/campus/global/config/ClovaOcrConfig.java (1)
11-19: 타임아웃 설정이 적절하게 추가되었습니다.이전 리뷰에서 지적된 타임아웃 미설정 문제가
SimpleClientHttpRequestFactory를 통해 해결되었습니다. 연결 타임아웃 3초, 읽기 타임아웃 15초는 OCR API 특성을 고려할 때 합리적인 값입니다.src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java (1)
38-53: 평점 집계 쿼리가 적절하게 구현되었습니다.JPQL 생성자 표현식을 사용한
PlaceStarAvgRow매핑과 단건 평균 조회 쿼리가 올바르게 작성되었습니다.src/main/java/com/campus/campus/domain/review/application/exception/ReceiptDateParseException.java (1)
5-9: LGTM!기존 예외 패턴을 따르며,
RECEIPT_DATE_PARSE_ERROR코드와 적절하게 연결되어 있습니다.src/main/java/com/campus/campus/domain/review/infrastructure/ClovaOcrClient.java (1)
64-80: 포맷 검증 및 정규화 로직이 적절합니다.null 체크, 지원 포맷 검증, jpeg→jpg 정규화가 올바르게 구현되었습니다.
src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (3)
188-219: LGTM! 리뷰 요약 목록 조회 로직이 잘 구현되었습니다.N+1 쿼리 문제를 방지하기 위해 리뷰 ID 목록을 먼저 수집하고 이미지를 일괄 조회하는 방식이 적절합니다.
Collectors.toMap에서 merge function(existing, ignored) -> existing을 사용하여 첫 번째 이미지만 취하는 로직도 명확합니다.
302-325: LGTM! 벌크 평균 별점 조회가 잘 구현되었습니다.null/empty 입력 처리, Repository 레벨 집계 쿼리 사용, 리뷰가 없는 장소에 대한 0.0 기본값 설정이 적절합니다.
93-96: LGTM! 검증 로직이 개선되었습니다.
request.isVerified()조건에 따라 리뷰 검증 및 스탬프 부여가 적절하게 처리됩니다.src/main/java/com/campus/campus/domain/review/application/service/OcrService.java (2)
45-66:@Transactional어노테이션 고려
processReceipt메서드는placeService.findOrCreatePlace를 호출하는데, 이 메서드가 Place를 생성할 수 있다면 트랜잭션 경계 설정이 필요할 수 있습니다. 현재 구조에서 트랜잭션 관리가 의도대로 동작하는지 확인하세요.
78-147: LGTM! 영수증 데이터 추출 로직이 견고하게 구현되었습니다.필수 필드(storeName, totalPrice, paymentDate)에 대한 null 체크와 예외 처리가 적절합니다. 결제일 누락 시 명확한 예외를 던지도록 수정되어 이전 리뷰의 NPE 우려가 해소되었습니다.
src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java (4)
59-148: LGTM! 제휴 장소 목록 조회 로직이 개선되었습니다.이전 리뷰에서 지적된
averageStarMap조회 키 문제가 수정되었습니다 (라인 133:post.getPlace().getPlaceId()사용). 거리 계산, 평균 별점 조회, 좋아요 상태 확인이 효율적으로 처리됩니다.
201-233: LGTM! 이전 리뷰 피드백이 반영되었습니다.
@Transactional(readOnly = true)어노테이션과placenull 체크가 추가되어 이전 리뷰에서 지적된 문제들이 해결되었습니다.
289-317: LGTM! 헬퍼 메서드들이 잘 구현되었습니다.
calculateDistance와calculateAverageStar의 반올림 로직이 명확하고,getPlaceImgUrls가 기존getImgUrls와 일관된 패턴을 따릅니다.BigDecimal을 사용한 정밀한 반올림 처리가 적절합니다.
235-254: LGTM! 비제휴 장소 상세 조회 로직이 잘 구현되었습니다.
getPartnershipDetailInternal과 일관된 패턴으로 구현되어 있으며, 거리, 평균 별점, 이미지, 리뷰 정보를 적절하게 조합합니다.src/main/java/com/campus/campus/domain/place/domain/repository/CouncilPartnershipSuggestionRepository.java (1)
16-28: 비관적 락 조회 정의가 명확합니다.쿼리와 락 선언이 간단하고 목적에 잘 맞습니다.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| @Transactional(readOnly = true) | ||
| public PlaceDetailView getPlaceDetails( | ||
| Long userId, | ||
| Long placeId, | ||
| double userLat, | ||
| double userLng | ||
| ) { | ||
| User user = userRepository.findById(userId) | ||
| .orElseThrow(UserNotFoundException::new); | ||
|
|
||
| double distanceMeter = GeoUtil.distanceMeter( | ||
| userLat, userLng, place.getCoordinate().latitude(), place.getCoordinate().longitude() | ||
| Place place = placeRepository.findById(placeId) | ||
| .orElseThrow(PlaceInfoNotFoundException::new); | ||
|
|
||
| Optional<StudentCouncilPost> activePost = | ||
| studentCouncilPostRepository.findActiveByPlaceAndUserScope( | ||
| place, | ||
| LocalDateTime.now(), | ||
| CouncilType.MAJOR_COUNCIL, | ||
| user.getMajor().getMajorId(), | ||
| CouncilType.COLLEGE_COUNCIL, | ||
| user.getCollege().getCollegeId(), | ||
| CouncilType.SCHOOL_COUNCIL, | ||
| user.getSchool().getSchoolId() | ||
| ); | ||
|
|
||
| if (activePost.isPresent()) { | ||
| StudentCouncilPost post = activePost.get(); | ||
| return getPartnershipDetailInternal(post, user, userLat, userLng); | ||
| } else { | ||
| return getNormalPlaceDetailInternal(user, place, userLat, userLng); | ||
| } | ||
| } |
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.
user 학적 정보 null 체크 누락
라인 274-278에서 user.getMajor(), user.getCollege(), user.getSchool()에 접근하는데, 이 필드들이 null일 경우 NullPointerException이 발생합니다. 다른 메서드들(validateAcademicInfo)과 동일한 검증 로직을 적용해야 합니다.
🐛 제안된 수정
`@Transactional`(readOnly = true)
public PlaceDetailView getPlaceDetails(
Long userId,
Long placeId,
double userLat,
double userLng
) {
User user = userRepository.findById(userId)
.orElseThrow(UserNotFoundException::new);
+ validateAcademicInfo(user);
+
Place place = placeRepository.findById(placeId)
.orElseThrow(PlaceInfoNotFoundException::new);
Optional<StudentCouncilPost> activePost =
studentCouncilPostRepository.findActiveByPlaceAndUserScope(🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java`
around lines 256 - 287, In getPlaceDetails, calling
user.getMajor()/getCollege()/getSchool() can NPE when academic info is null;
before invoking studentCouncilPostRepository.findActiveByPlaceAndUserScope, call
the existing validateAcademicInfo(user) (or inline the same null checks) to
ensure user.getMajor(), user.getCollege(), and user.getSchool() are present and
throw the same validation exception used elsewhere; then proceed to call
studentCouncilPostRepository.findActiveByPlaceAndUserScope and the subsequent
logic.
| //이미 신청했는지 체크 | ||
| if (userPartnershipSuggestionRepository | ||
| .existsByUserAndPlace(user, place)) { | ||
| throw new AlreadySuggestedPartnershipException(); | ||
| } | ||
|
|
||
| //중복 방지 저장 | ||
| userPartnershipSuggestionRepository.save( | ||
| UserPartnershipSuggestion.create(user, place) | ||
| ); |
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 UserPartnershipSuggestion entity definition
fd -a "UserPartnershipSuggestion.java" src/main/javaRepository: our-campUS/our-campUS-BE
Length of output: 176
🏁 Script executed:
#!/bin/bash
# Check for unique constraints and table annotations
for f in $(fd -a "UserPartnershipSuggestion.java" src/main/java); do
cat -n "$f"
doneRepository: our-campUS/our-campUS-BE
Length of output: 1741
🏁 Script executed:
#!/bin/bash
# Check PlaceService.java for context around lines 197-206
fd -a "PlaceService.java" src/main/javaRepository: our-campUS/our-campUS-BE
Length of output: 282
🏁 Script executed:
#!/bin/bash
# Look for schema/migration files with constraints
find . -type f \( -name "*.sql" -o -name "*.xml" -o -name "*migration*" \) | head -20Repository: our-campUS/our-campUS-BE
Length of output: 50
🏁 Script executed:
#!/bin/bash
# Read PlaceService.java around lines 197-206
wc -l /home/jailuser/git/src/main/java/com/campus/campus/domain/place/application/service/PlaceService.javaRepository: our-campUS/our-campUS-BE
Length of output: 173
🏁 Script executed:
#!/bin/bash
# Read the suggestPartnership method context
sed -n '180,220p' /home/jailuser/git/src/main/java/com/campus/campus/domain/place/application/service/PlaceService.javaRepository: our-campUS/our-campUS-BE
Length of output: 1240
🏁 Script executed:
#!/bin/bash
# Search for database migration/schema files more thoroughly
find . -type f \( -name "*.sql" -o -name "*.yml" -o -name "*.yaml" \) 2>/dev/null | grep -i -E "migration|schema|flyway|liquibase" | head -20Repository: our-campUS/our-campUS-BE
Length of output: 50
🏁 Script executed:
#!/bin/bash
# Search for unique constraint definitions anywhere in the codebase
rg -i "unique.*constraint|uniqueconstraint" --type sql --type java --max-count 20Repository: our-campUS/our-campUS-BE
Length of output: 1454
🏁 Script executed:
#!/bin/bash
# Check if there's Hibernate DDL auto configuration
rg "hibernate.ddl-auto|spring.jpa.hibernate.ddl-auto" --type properties --type yaml --max-count 10Repository: our-campUS/our-campUS-BE
Length of output: 102
🏁 Script executed:
#!/bin/bash
# Look for Flyway/Liquibase migration files
find . -type d \( -name "db" -o -name "migration" -o -name "flyway" -o -name "liquibase" \) 2>/dev/nullRepository: our-campUS/our-campUS-BE
Length of output: 50
🏁 Script executed:
#!/bin/bash
# Find all SQL files in the repository
find . -name "*.sql" -type f 2>/dev/null | head -30Repository: our-campUS/our-campUS-BE
Length of output: 50
🏁 Script executed:
#!/bin/bash
# Check application configuration files
find . -name "application*.yml" -o -name "application*.yaml" -o -name "application*.properties" 2>/dev/nullRepository: our-campUS/our-campUS-BE
Length of output: 235
🏁 Script executed:
#!/bin/bash
# Check if CouncilPartnershipSuggestion entity shows the pattern for unique constraint
cat /home/jailuser/git/src/main/java/com/campus/campus/domain/place/domain/entity/CouncilPartnershipSuggestion.java | head -50Repository: our-campUS/our-campUS-BE
Length of output: 1592
유저 제휴 신청의 유니크 제약이 누락되어 있어 동시성 문제가 발생할 수 있습니다.
현재 코드의 existsByUserAndPlace → save 패턴은 TOCTOU 경쟁 조건에 노출되어 있습니다. 그러나 제안된 DataIntegrityViolationException 처리 방식이 동작하려면 먼저 UserPartnershipSuggestion 엔티티에 유니크 제약이 정의되어 있어야 합니다.
현재 엔티티 정의에는 @UniqueConstraint(columnNames = {"user_id", "place_id"})가 없습니다. 유사한 LikedPlace와 CouncilPartnershipSuggestion 엔티티와 달리 이 제약이 누락되어 있습니다.
두 가지 수정이 필요합니다:
UserPartnershipSuggestion.java에 유니크 제약 추가PlaceService.suggestPartnership()메서드에서 예외 처리 구현
🔧 수정 제안
UserPartnershipSuggestion.java:
`@Entity`
-@Table(name = "user_partnership_suggestion")
+@Table(name = "user_partnership_suggestion",
+ uniqueConstraints = `@UniqueConstraint`(columnNames = {"user_id", "place_id"}))
`@Getter`PlaceService.java:
- //중복 방지 저장
- userPartnershipSuggestionRepository.save(
- UserPartnershipSuggestion.create(user, place)
- );
+ //중복 방지 저장 (동시성 대비)
+ try {
+ userPartnershipSuggestionRepository.saveAndFlush(
+ UserPartnershipSuggestion.create(user, place)
+ );
+ } catch (DataIntegrityViolationException e) {
+ throw new AlreadySuggestedPartnershipException();
+ }🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java`
around lines 197 - 206, The UserPartnershipSuggestion entity lacks a unique
constraint on (user_id, place_id) causing TOCTOU races; add a class-level unique
constraint in UserPartnershipSuggestion (e.g., `@Table`(uniqueConstraints =
`@UniqueConstraint`(columnNames = {"user_id","place_id"}))) to enforce DB-level
uniqueness, and in PlaceService.suggestPartnership() wrap the
userPartnershipSuggestionRepository.save(UserPartnershipSuggestion.create(user,
place)) call to catch DataIntegrityViolationException (or the specific
constraint violation) and translate it to AlreadySuggestedPartnershipException;
keep the pre-check using existsByUserAndPlace for fast-fail but rely on the DB
constraint + exception handler to cover concurrent inserts.
| private ReceiptOcrResponse parse(String json) { | ||
| try { | ||
| log.debug("[OCR PARSE INPUT] {}", json); | ||
| return objectMapper.readValue(json, ReceiptOcrResponse.class); | ||
| } catch (Exception e) { | ||
| log.error("[OCR PARSE FAILED] raw={}", json, e); | ||
| throw new ReceiptOcrFailedException(); | ||
| } | ||
| } |
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.
에러 로깅에서 민감 정보 노출 가능
라인 73에서 파싱 실패 시 원시 JSON을 로깅합니다. 이 JSON에는 영수증의 민감한 정보가 포함될 수 있으므로, ERROR 레벨에서도 전체 내용을 로깅하는 것은 주의가 필요합니다.
🔒 제안된 수정
} catch (Exception e) {
- log.error("[OCR PARSE FAILED] raw={}", json, e);
+ log.error("[OCR PARSE FAILED] JSON parsing error occurred", e);
throw new ReceiptOcrFailedException();
}🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/review/application/service/OcrService.java`
around lines 68 - 76, The parse method in OcrService currently logs the full raw
JSON on parse failure which may expose sensitive receipt data; update the catch
block in parse(String json) to avoid emitting the raw payload—instead compute
and log a safe identifier (e.g., a SHA-256 or hex digest of the json) and/or a
small truncated snippet (first 50 chars) or masked content, include the
exception e in the log, and then throw ReceiptOcrFailedException as before;
locate the parse method in OcrService and replace the log.error("[OCR PARSE
FAILED] raw={}", json, e) call with a sanitized log entry that references the
computed digest/snippet and the exception (while keeping objectMapper.readValue
and ReceiptOcrFailedException behavior unchanged).
src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.java
Outdated
Show resolved
Hide resolved
|
|
||
| long countByPlace_PlaceIdAndUser_School_SchoolId(Long placeId, long schoolId); | ||
|
|
||
| List<Review> findALlByPlace_PlaceId(long placeId); |
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.
메서드명 오타: findALlByPlace_PlaceId → findAllByPlace_PlaceId
ALl이 아닌 All로 수정해야 합니다. Spring Data JPA 쿼리 메서드 명명 규칙과 일관성을 위해 수정이 필요합니다.
🔧 수정 제안
- List<Review> findALlByPlace_PlaceId(long placeId);
+ List<Review> findAllByPlace_PlaceId(long placeId);📝 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.
| List<Review> findALlByPlace_PlaceId(long placeId); | |
| List<Review> findAllByPlace_PlaceId(long placeId); |
🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java`
at line 72, In the ReviewRepository interface rename the mis-typed query method
findALlByPlace_PlaceId to findAllByPlace_PlaceId (preserve the return type
List<Review> and parameter long placeId) so it conforms to Spring Data JPA
naming; update any call sites referencing
ReviewRepository.findALlByPlace_PlaceId to the corrected
ReviewRepository.findAllByPlace_PlaceId.
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.
@1224kang 수정 부탁드려요!!
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.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
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: 1
🤖 Fix all issues with AI agents
In `@src/main/resources/application-dev.yml`:
- Around line 74-77: The clova.ocr.invoke-url property contains a sensitive
identifier in plain text and should be encrypted like clova.ocr.secret-key
(which uses ENC(...)); replace the plain invoke-url with an encrypted value
(e.g., ENC(...)) and update the configuration loading/decryption flow to decrypt
clova.ocr.invoke-url at runtime the same way clova.ocr.secret-key is handled
(ensure the config decrypter recognizes the property and that deployment/secret
management stores the encrypted URL), then verify code paths that read
clova.ocr.invoke-url continue to work with the decrypted string.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/main/resources/application-dev.ymlsrc/main/resources/application-prod.ymlsrc/test/resources/application-test.yml
🧰 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/test/resources/application-test.ymlsrc/main/resources/application-dev.yml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
src/test/resources/application-test.yml (1)
64-68: LGTM!Clova OCR 테스트 설정이 올바르게 추가되었습니다.
dummy값 사용은 이 파일의 다른 외부 서비스 설정(map.naver, map.google 등)과 일관성 있게 적용되어 있습니다. 기존 학습 내용에 따르면, 이 프로젝트의 테스트 설정 파일에서는 하드코딩된 더미 값을 사용하는 것이 허용된 패턴입니다.src/main/resources/application-prod.yml (2)
68-73: 변경은 리포맷/공백 수준으로 보여 추가 코멘트 없음.
74-77: Clova OCR 엔드포인트 형식 검증 완료 - 최신 문서 기준 정상입니다.최신 Naver Cloud 문서에 따르면 receipt OCR invoke-url 형식은
https://{apigw-host}/custom/v1/{DomainId}/{InvokeKey}/document/receipt이며, 코드의 엔드포인트가 이 형식과 정확히 일치합니다. 현재 설정값은 올바릅니다.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| clova: | ||
| ocr: | ||
| invoke-url: https://oprno6zgu9.apigw.ntruss.com/custom/v1/49413/b8f236bbccbb9b0008d8d0485d9028faba366671ac72ccab91d2df2bb9c96001/document/receipt | ||
| secret-key: ENC(27rOnbpzREczXXFbh6OSHXiqb6BQLhTNZ4VFKfv63lgV9O3ABBrNM8GHOxj3w+G4tp+lHGhazf0=) |
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.
invoke-url에 민감한 식별자가 포함되어 있어 암호화가 필요합니다.
URL 경로에 포함된 긴 16진수 문자열(b8f236bb...)은 Clova OCR의 API 키 또는 템플릿 식별자로 보입니다. secret-key는 ENC()로 암호화되어 있지만, invoke-url은 평문으로 노출되어 있어 보안 정책이 일관되지 않습니다.
이 파일의 다른 민감한 값들(datasource URL, OAuth 키 등)과 동일하게 invoke-url도 암호화하는 것을 권장합니다.
🔒 제안된 수정
clova:
ocr:
- invoke-url: https://oprno6zgu9.apigw.ntruss.com/custom/v1/49413/b8f236bbccbb9b0008d8d0485d9028faba366671ac72ccab91d2df2bb9c96001/document/receipt
+ invoke-url: ENC(<jasypt로 암호화된 값>)
secret-key: ENC(27rOnbpzREczXXFbh6OSHXiqb6BQLhTNZ4VFKfv63lgV9O3ABBrNM8GHOxj3w+G4tp+lHGhazf0=)🤖 Prompt for AI Agents
In `@src/main/resources/application-dev.yml` around lines 74 - 77, The
clova.ocr.invoke-url property contains a sensitive identifier in plain text and
should be encrypted like clova.ocr.secret-key (which uses ENC(...)); replace the
plain invoke-url with an encrypted value (e.g., ENC(...)) and update the
configuration loading/decryption flow to decrypt clova.ocr.invoke-url at runtime
the same way clova.ocr.secret-key is handled (ensure the config decrypter
recognizes the property and that deployment/secret management stores the
encrypted URL), then verify code paths that read clova.ocr.invoke-url continue
to work with the decrypted string.
1winhyun
left a 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.
수고하셨습니다!! 우선 1차로 리뷰 남겼습니다.
확인 후 수정 부탁드려요!!
| Optional<StudentCouncil> findByMajor_MajorId(Long majorId); | ||
|
|
||
| Optional<StudentCouncil> findByCollege_CollegeId(Long collegeId); | ||
|
|
||
| Optional<StudentCouncil> findBySchool_SchoolId(Long schoolId); |
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.
@1224kang 요거 각각 DeletedAtIsNull 추가해주세요!!
...in/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptOcrResponse.java
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/review/application/service/OcrService.java
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/review/application/service/OcrService.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/review/application/service/OcrService.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.java
Outdated
Show resolved
Hide resolved
|
|
||
| long countByPlace_PlaceIdAndUser_School_SchoolId(Long placeId, long schoolId); | ||
|
|
||
| List<Review> findALlByPlace_PlaceId(long placeId); |
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.
@1224kang 수정 부탁드려요!!
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: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.java (1)
17-24:ReviewImage에서@SuperBuilder사용은 올바릅니다.
BaseEntity를 상속하면서 빌더 패턴을 사용할 때@SuperBuilder는 필수입니다. 확인 결과:
BaseEntity에@SuperBuilder가 적용되어 있음 ✓ReviewImage의@SuperBuilder사용이 올바름 ✓다만, 코드베이스 전체에서 심각한 일관성 문제가 있습니다.
BaseEntity를 상속하는 15개 엔티티 중 9개가@Builder를 사용하고 있습니다:
@SuperBuilder사용 (올바름): User, UserPartnershipSuggestion, EmailVerification, StudentCouncil@Builder사용 (잘못됨): Review, Stamp, Reward, Notification, LikedPlace, StudentCouncilNotice, NoticeImage, StudentCouncilPost, LikePost
@SuperBuilder부모 클래스를 상속할 때@Builder를 사용하면 빌더 상속이 제대로 작동하지 않을 수 있습니다.BaseEntity를 상속하는 모든 엔티티를@SuperBuilder로 통일하기를 권장합니다.
🤖 Fix all issues with AI agents
In
`@src/main/java/com/campus/campus/domain/review/application/service/OcrService.java`:
- Around line 75-83: The code calls response.images().stream() which can NPE if
images() is null; update the OcrService logic to defensively handle a null
images list by replacing response.images() with an empty list before streaming
(e.g., use
Optional.ofNullable(response.images()).orElse(Collections.emptyList()) or
similar) so the log size check and the subsequent stream().findFirst() operate
safely, and keep the existing behavior of logging a warning and throwing
ReceiptOcrFailedException when no image is present.
- Around line 57-60: In OcrService (methods using parse and
extractReceiptResult), stop logging full OCR results at INFO level; change the
log call that logs the ReceiptOcrResponse/ReceiptResultDto (currently using
log.info("영수증 ocr 인식 결과:{}", result)) to either log.debug or log.info with a
redacted/summary string only (e.g., non-sensitive metadata like receiptId,
storeName truncated, totalAmount), and ensure any sensitive fields in
ReceiptOcrResponse and ReceiptResultDto are masked or omitted before logging.
♻️ Duplicate comments (5)
src/main/java/com/campus/campus/domain/council/domain/repository/StudentCouncilRepository.java (1)
48-52:managerApproved필터가 누락되어 있습니다.이전 리뷰에서 지적된 대로, 이 세 메서드에
managerApproved필터가 여전히 누락되어 있습니다. 현재DeletedAtIsNull만 추가되었지만, 미승인된 학생회가 비즈니스 로직에 포함될 수 있습니다.🔧 수정 제안
- Optional<StudentCouncil> findByMajor_MajorIdAndDeletedAtIsNull(Long majorId); + Optional<StudentCouncil> findByMajor_MajorIdAndManagerApprovedIsTrueAndDeletedAtIsNull(Long majorId); - Optional<StudentCouncil> findByCollege_CollegeIdAndDeletedAtIsNull(Long collegeId); + Optional<StudentCouncil> findByCollege_CollegeIdAndManagerApprovedIsTrueAndDeletedAtIsNull(Long collegeId); - Optional<StudentCouncil> findBySchool_SchoolIdAndDeletedAtIsNull(Long schoolId); + Optional<StudentCouncil> findBySchool_SchoolIdAndManagerApprovedIsTrueAndDeletedAtIsNull(Long schoolId);src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java (1)
39-39:isVerified가 null일 때 NPE 발생 가능이전 리뷰에서 지적된 대로,
request.isVerified()가Boolean타입이고 null일 수 있습니다.Review엔티티의isVerified필드가 primitiveboolean인 경우, 언박싱 과정에서 NPE가 발생합니다.🔧 수정 제안
- .isVerified(request.isVerified()) + .isVerified(Boolean.TRUE.equals(request.isVerified()))src/main/java/com/campus/campus/domain/review/application/service/OcrService.java (1)
64-70: 파싱 실패 시 raw JSON 전체 로그는 제거/축약 필요
실패 케이스에서도 원문 전체 로깅은 민감 정보 노출 위험이 큽니다. 요약 로그로 교체하세요.🔒 수정 제안
- log.error("[OCR PARSE FAILED] raw={}", json, e); + log.error("[OCR PARSE FAILED] JSON 파싱 실패", e);src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
197-206: 중복 신청 레이스 컨디션은 여전히 남아 있습니다.
existsByUserAndPlace → save는 동시 요청 시 TOCTOU 문제가 발생합니다. DB 유니크 제약과 예외 처리(또는 upsert)로 보강하세요.🔒 수정 제안
- //이미 신청했는지 체크 - if (userPartnershipSuggestionRepository - .existsByUserAndPlace(user, place)) { - throw new AlreadySuggestedPartnershipException(); - } - - //중복 방지 저장 - userPartnershipSuggestionRepository.save( - UserPartnershipSuggestion.create(user, place) - ); + //중복 방지 저장 (동시성 대비) + try { + if (userPartnershipSuggestionRepository.existsByUserAndPlace(user, place)) { + throw new AlreadySuggestedPartnershipException(); + } + userPartnershipSuggestionRepository.saveAndFlush( + UserPartnershipSuggestion.create(user, place) + ); + } catch (DataIntegrityViolationException e) { + throw new AlreadySuggestedPartnershipException(); + }src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (1)
284-289: 학적정보 null 가능성으로 NPE 위험
user.getMajor()/getCollege()/getSchool()은 null일 수 있습니다. 프로필 미완료 시 명확한 예외 처리가 필요합니다.🐛 수정 제안
+ if (user.isProfileNotCompleted()) { + throw new AcademicInfoNotSetException(); + } Long majorId = user.getMajor().getMajorId(); Long collegeId = user.getCollege().getCollegeId(); Long schoolId = user.getSchool().getSchoolId();
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
src/main/java/com/campus/campus/domain/council/domain/repository/StudentCouncilRepository.javasrc/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.javasrc/main/java/com/campus/campus/domain/place/application/service/PlaceService.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ReviewPartnerResponse.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptOcrResponse.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/ReviewImage.javasrc/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (4)
src/main/java/com/campus/campus/domain/councilpost/application/exception/PlaceInfoNotFoundException.java (1)
PlaceInfoNotFoundException(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/review/application/exception/NotPartnershipReceiptException.java (1)
NotPartnershipReceiptException(5-9)src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)
src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.java (2)
src/main/java/com/campus/campus/domain/place/domain/entity/CouncilPartnershipSuggestion.java (1)
Entity(22-60)src/main/java/com/campus/campus/domain/review/domain/entity/Review.java (1)
Entity(22-61)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (13)
src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewPartnerResponse.java (1)
3-12: LGTM!응답 DTO로서 적절한 구조입니다. Java record를 활용하여 불변 객체로 설계되었습니다.
src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java (3)
38-49: LGTM!배치 조회를 위한 JPQL 쿼리가 적절하게 구현되었습니다. DTO projection을 사용하여 불필요한 데이터 전송을 방지합니다.
51-52: LGTM!단일 장소에 대한 평균 별점 조회 메서드입니다.
Optional<Double>반환 타입으로 리뷰가 없는 경우를 적절히 처리합니다.
72-72: LGTM!Spring Data JPA의 명명 규칙을 준수하여 최신 3개 리뷰를 조회하는 메서드입니다.
src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptOcrResponse.java (1)
5-71: LGTM!이전 리뷰 피드백을 반영하여 Inner Record 구조로 잘 정리되었습니다. OCR API 응답 형식에 맞게 계층적 구조가 적절히 구성되어 있습니다.
src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java (2)
44-68: LGTM!OCR 응답 매핑 메서드들이 적절히 구현되었습니다.
safeText와extractPriceText헬퍼 메서드에서 null 처리가 잘 되어 있습니다.
79-86: LGTM!
SimpleReviewResponse매핑 메서드가 적절히 구현되었습니다.src/main/java/com/campus/campus/domain/review/application/service/OcrService.java (1)
133-143: 날짜 정규화 후 파싱 처리 적절합니다.
OCR 결과의 다양한 날짜 포맷을 흡수하는 방식으로 충분히 견고합니다.src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java (1)
77-131: 상세/리뷰 응답 매핑 확장이 깔끔합니다.
새 DTO들로의 매핑 필드가 일관되고 응답 확장 요구사항에 맞습니다.src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
339-361: 학생회 범위 해석 로직 분리 좋습니다.
가독성과 재사용성이 높아졌습니다.src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (3)
98-101: 검증된 리뷰에만 스탬프 부여 처리 적절합니다.
실제 인증 흐름과 일관됩니다.
193-224: 리뷰 요약 조회 구현이 깔끔합니다.
상위 3개 + 대표 이미지 매핑 흐름이 명확합니다.
304-336: 평균/카운트 집계 API 추가 방향 좋습니다.
집계 쿼리 기반으로 확장 가능한 형태입니다.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| ReceiptOcrResponse ocrResponse = parse(rawResponse); | ||
| ReceiptResultDto result = extractReceiptResult(ocrResponse); | ||
| log.info("영수증 ocr 인식 결과:{}", result); | ||
|
|
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.
민감 정보가 포함된 OCR 결과를 INFO로 로깅하지 마세요.
영수증 상세가 포함될 수 있어 개인정보/결제정보 노출 위험이 있습니다. 최소 DEBUG로 낮추거나 요약 정보만 남겨주세요.
🔒 수정 제안
- log.info("영수증 ocr 인식 결과:{}", result);
+ log.debug("영수증 ocr 인식 결과:{}", result);🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/review/application/service/OcrService.java`
around lines 57 - 60, In OcrService (methods using parse and
extractReceiptResult), stop logging full OCR results at INFO level; change the
log call that logs the ReceiptOcrResponse/ReceiptResultDto (currently using
log.info("영수증 ocr 인식 결과:{}", result)) to either log.debug or log.info with a
redacted/summary string only (e.g., non-sensitive metadata like receiptId,
storeName truncated, totalAmount), and ensure any sensitive fields in
ReceiptOcrResponse and ReceiptResultDto are masked or omitted before logging.
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.
@1224kang 이 부분도 debug로 수정해주세요.
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.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
src/main/java/com/campus/campus/domain/review/application/service/OcrService.java
Show resolved
Hide resolved
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: 1
🤖 Fix all issues with AI agents
In
`@src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java`:
- Around line 122-133: The tag parameter is being passed as
post.getWriter().getCouncilName(), causing duplicate values with the council
field in toReviewPartnerResponse; update the call site in ReviewService to pass
the resolved tag (use resolveTag(...) that returns school/college/department)
instead of post.getWriter().getCouncilName(), or alternatively change the
toReviewPartnerResponse signature to accept a StudentCouncilPost and call
resolveTag inside that method; locate the call that constructs the
ReviewPartnerResponse in ReviewService and replace the tag argument with
resolveTag(...) (or move resolveTag logic into toReviewPartnerResponse) so
council stays councilName and tag contains the resolved
school/college/department value.
♻️ Duplicate comments (4)
src/main/resources/application-dev.yml (1)
78-81:invoke-url의 민감한 식별자 암호화 필요 (기존 리뷰 중복)이전 리뷰에서 이미 지적된 바와 같이,
invoke-url에 포함된 API 게이트웨이 URL과 템플릿 식별자(b8f236bb...)가 평문으로 노출되어 있습니다.secret-key는ENC()로 암호화되어 있으므로 일관성을 위해invoke-url도 동일하게 암호화해야 합니다.src/main/java/com/campus/campus/domain/review/application/service/OcrService.java (2)
58-60: OCR 결과 전체를 INFO로 로깅하지 마세요.
영수증 데이터가 포함될 수 있어 운영 로그 노출 위험이 큽니다. DEBUG로 낮추거나 요약/마스킹만 남겨주세요.🔒 수정 제안
- log.info("영수증 ocr 인식 결과:{}", result); + log.debug("영수증 ocr 인식 결과:{}", result);
65-71: 파싱 실패 시 원문 JSON 로그를 제거/마스킹하세요.
ERROR 로그에 원문을 남기면 개인정보 노출 위험이 있습니다.🔒 수정 제안
- } catch (Exception e) { - log.error("[OCR PARSE FAILED] raw={}", json, e); + } catch (Exception e) { + log.error("[OCR PARSE FAILED] JSON parsing error occurred", e); throw new ReceiptOcrFailedException(); }src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (1)
284-287: 사용자 학적 정보 null 체크가 필요합니다.
user.getMajor()/getCollege()/getSchool()가 null이면 NPE가 발생합니다. 프로필 미완성 시 명확한 예외 처리 또는 검증을 추가하세요.
🧹 Nitpick comments (2)
src/main/resources/application-prod.yml (1)
78-81: invoke-url 암호화 재검토 권장.
invoke-url은 모든 환경 설정 파일(application-dev.yml, application-local.yml, application-prod.yml)에서 일관되게 평문으로 유지되고 있습니다. 이는 의도된 설계일 가능성이 있으나, 같은 파일 내에서secret-key는ENC()로 암호화되어 있어 보안 일관성이 떨어집니다.파일의 다른 민감한 값들(datasource url, password, admin-key, private-key, api-key, server-uri 등)이 대부분 암호화되어 있는 패턴을 고려할 때, API 엔드포인트 URL도 암호화하는 것이 권장됩니다. 특히
secret-key와 함께 암호화되어야 API 자격증명의 일관된 보안 수준을 유지할 수 있습니다.
invoke-url을 평문으로 유지하는 것이 의도된 설계라면, 그 이유를 문서화하는 것을 고려해주시기 바랍니다.src/main/java/com/campus/campus/global/config/ClovaOcrConfig.java (1)
11-21: 이전 리뷰에서 지적된 타임아웃 설정이 잘 적용되었습니다.연결 타임아웃 3초, 읽기 타임아웃 15초로 적절하게 설정되어 외부 OCR API 호출 시 무한 대기 위험이 해소되었습니다.
다만, Spring 6.1+ 에서
setConnectTimeout(int)와setReadTimeout(int)메서드가 deprecated 되었습니다.Duration기반 메서드 사용을 권장합니다.♻️ Duration 기반 메서드로 변경 제안
+import java.time.Duration; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.web.client.RestClient; `@Configuration` public class ClovaOcrConfig { `@Bean` public RestClient clovaOcrRestClient() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); - factory.setConnectTimeout(3_000); // 연결 타임아웃 3초 - factory.setReadTimeout(15_000); // 응답 대기 타임아웃 15초 + factory.setConnectTimeout(Duration.ofSeconds(3)); + factory.setReadTimeout(Duration.ofSeconds(15)); return RestClient.builder() .requestFactory(factory) .build(); } }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (20)
src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/PlaceStarAvgRow.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ReviewPartnerResponse.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/SimpleReviewResponse.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/ReceiptOcrResponse.javasrc/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptResultDto.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/service/OcrService.javasrc/main/java/com/campus/campus/domain/review/application/service/ReviewService.javasrc/main/java/com/campus/campus/domain/review/infrastructure/ClovaOcrClient.javasrc/main/java/com/campus/campus/domain/review/presentation/ReviewController.javasrc/main/java/com/campus/campus/global/config/ClovaOcrConfig.javasrc/main/resources/application-dev.ymlsrc/main/resources/application-local.ymlsrc/main/resources/application-prod.yml
🚧 Files skipped from review as they are similar to previous changes (8)
- src/main/resources/application-local.yml
- src/main/java/com/campus/campus/domain/review/infrastructure/ClovaOcrClient.java
- src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java
- src/main/java/com/campus/campus/domain/review/application/dto/response/SimpleReviewResponse.java
- src/main/java/com/campus/campus/domain/review/application/exception/ReceiptImageFormatException.java
- src/main/java/com/campus/campus/domain/review/application/exception/NotPartnershipReceiptException.java
- src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptItemDto.java
- src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.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-prod.yml
🧬 Code graph analysis (2)
src/main/java/com/campus/campus/domain/review/application/service/OcrService.java (1)
src/main/java/com/campus/campus/domain/councilpost/application/exception/PlaceInfoNotFoundException.java (1)
PlaceInfoNotFoundException(5-9)
src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java (1)
src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java (1)
Component(30-174)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (7)
src/main/resources/application-dev.yml (2)
33-36: LGTM!
spring.servlet.multipart설정이 올바르게 추가되었습니다. OCR 이미지 업로드를 위한 10MB 제한은 적절합니다.
74-76: LGTM!Firebase 자격 증명 경로가
ENC()로 적절히 암호화되어 있습니다.src/main/resources/application-prod.yml (1)
33-36: LGTM!
servlet.multipart설정이spring네임스페이스 아래에 올바르게 배치되어 있습니다. 10MB 파일 크기 제한은 OCR/영수증 이미지 업로드에 적합한 설정입니다.src/main/java/com/campus/campus/domain/review/application/dto/response/PlaceStarAvgRow.java (1)
3-7: LGTM!Repository 쿼리 결과를 담기 위한 간결한 record DTO입니다. 설계가 적절합니다.
src/main/java/com/campus/campus/domain/review/application/exception/ReceiptOcrFailedException.java (1)
5-9: LGTM!프로젝트의 예외 처리 패턴을 일관되게 따르고 있습니다.
src/main/java/com/campus/campus/domain/review/application/dto/response/ocr/ReceiptResultDto.java (1)
6-12: LGTM!이전 리뷰에서 지적된
totalPlace→totalPrice필드명 수정이 반영되었습니다. OCR 결과를 담기 위한 적절한 record 구조입니다.src/main/java/com/campus/campus/domain/review/application/exception/ReceiptFileConvertException.java (1)
5-9: LGTM!파일 변환 실패에 대한 명확한 예외 타입입니다. 프로젝트의 예외 처리 패턴을 따르고 있습니다.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| public ReviewPartnerResponse toReviewPartnerResponse(StudentCouncilPost post, Place place, double averageStar, | ||
| String tag, boolean isLiked, LocalDate paymentDate) { | ||
| return new ReviewPartnerResponse( | ||
| place.getPlaceName(), | ||
| place.getPlaceCategory(), | ||
| post.getWriter().getCouncilName(), | ||
| averageStar, //리뷰 별점 | ||
| post.getTitle(), | ||
| tag, | ||
| isLiked, | ||
| paymentDate | ||
| ); |
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 PlaceMapper file and understand the context
find . -type f -name "PlaceMapper.java" | head -20Repository: our-campUS/our-campUS-BE
Length of output: 150
🏁 Script executed:
# Search for calls to toReviewPartnerResponse method
rg "toReviewPartnerResponse" -A 5 -B 2Repository: our-campUS/our-campUS-BE
Length of output: 2017
🏁 Script executed:
# Find ReviewService file to check how tag parameter is passed
find . -type f -name "ReviewService.java" | head -20Repository: our-campUS/our-campUS-BE
Length of output: 154
🏁 Script executed:
# Read ReviewService.java to understand the full context of the call
cat -n ./src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java | head -100Repository: our-campUS/our-campUS-BE
Length of output: 5811
🏁 Script executed:
# Search for resolveTag method
rg "resolveTag" -B 5 -A 10Repository: our-campUS/our-campUS-BE
Length of output: 3468
🏁 Script executed:
# Get more context around the toReviewPartnerResponse call
rg "toReviewPartnerResponse" -B 10 -A 5Repository: our-campUS/our-campUS-BE
Length of output: 3867
tag 파라미터가 councilName으로 전달되어 council 필드와 중복 발생
ReviewService에서 tag에 post.getWriter().getCouncilName()을 넘기고 있어, 현재 toReviewPartnerResponse 메서드 내에서 council 필드도 동일하게 post.getWriter().getCouncilName()을 사용하므로 두 필드가 동일한 값을 갖습니다. resolveTag 메서드가 학교/단과대/학과명을 올바르게 반환하도록 설계되었으므로, ReviewService의 호출부에서 tag 파라미터로 resolveTag 로직을 적용하거나 메서드 시그니처를 수정하여 의도에 맞는 태그 값을 전달하세요.
🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java`
around lines 122 - 133, The tag parameter is being passed as
post.getWriter().getCouncilName(), causing duplicate values with the council
field in toReviewPartnerResponse; update the call site in ReviewService to pass
the resolved tag (use resolveTag(...) that returns school/college/department)
instead of post.getWriter().getCouncilName(), or alternatively change the
toReviewPartnerResponse signature to accept a StudentCouncilPost and call
resolveTag inside that method; locate the call that constructs the
ReviewPartnerResponse in ReviewService and replace the tag argument with
resolveTag(...) (or move resolveTag logic into toReviewPartnerResponse) so
council stays councilName and tag contains the resolved
school/college/department value.
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: 1
🤖 Fix all issues with AI agents
In
`@src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java`:
- Around line 48-85: The example JSON in ReviewController.java embeds a value
that matches an API/key pattern at place.placeKey (inside the requestBody
ExampleObject for ReviewRequest); replace that string with a clearly fake
placeholder (e.g. "example-place-key-0000" or "PLACE_KEY_EXAMPLE") in the
ExampleObject so no real keys appear in the swagger example, and if that value
was an actual secret rotate it immediately; update the ExampleObject in the
requestBody schema for ReviewRequest to use the dummy value.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java
🧰 Additional context used
🪛 Gitleaks (8.30.0)
src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java
[high] 69-69: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (2)
src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java (2)
42-44: OcrService 주입 추가 좋습니다.
리뷰 컨트롤러에서 OCR 로직을 분리한 의존성 주입 방향이 적절합니다.
97-118: 엔드포인트 설계는 깔끔합니다.
다만 파일 업로드이므로max-size, MIME 타입/확장자, 빈 파일 검증이 전역 설정 또는 서비스 레벨에서 보장되는지 확인 부탁드립니다.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| description = "제휴 가게여서 영수증 인증을 마쳤다면 isVerified=true 값으로 넘겨주세요.", | ||
| requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( | ||
| required = true, | ||
| content = @io.swagger.v3.oas.annotations.media.Content( | ||
| mediaType = "application/json", | ||
| examples = @io.swagger.v3.oas.annotations.media.ExampleObject( | ||
| name = "리뷰 작성 요청 예시", | ||
| summary = "리뷰 작성 Request Body", | ||
| value = """ | ||
| { | ||
| "content": "아주 정말 맛있습니다. 저의 완전 짱 또간집. 꼭꼮꼬꼬꼭 가세요.", | ||
| "star": 3.5, | ||
| "imageUrls": [ | ||
| "https://image1.jpg", | ||
| "https://image2.jpg" | ||
| ], | ||
| "place": { | ||
| "placeName": "숙명여자대학교", | ||
| "placeKey": "string", | ||
| "address": "서울특별시 용산구 청파로47길 99", | ||
| "category": "교육,학문>대학교", | ||
| "link": "https://map.naver.com/v5/search/%EC%88%99%EB%AA%85%EC%97%AC%EC%9E%90%EB%8C%80%ED%95%99%EA%B5%90", | ||
| "telephone": "010-1234-1234", | ||
| "coordinate": { | ||
| "latitude": 37.545947, | ||
| "longitude": 126.964578 | ||
| }, | ||
| "imgUrls": [ | ||
| "https://place-image1.jpg" | ||
| ] | ||
| } | ||
| } | ||
| """ | ||
| ) | ||
| schema = @Schema(implementation = ReviewRequest.class), | ||
| examples = { | ||
| @io.swagger.v3.oas.annotations.media.ExampleObject( | ||
| name = "리뷰 작성 요청 예시", | ||
| summary = "영수증 인증 리뷰 작성", | ||
| value = """ | ||
| { | ||
| "content": "아주 정말 맛있습니다. 저의 완전 짱 또간집. 꼭꼭꼭꼭꼭꼭꼭꼭꼮 가세요.", | ||
| "star": 4.5, | ||
| "imageUrls": [ | ||
| "https://image.campus.com/review/1.jpg", | ||
| "https://image.campus.com/review/2.jpg" | ||
| ], | ||
| "isVerified": true, | ||
| "place": { | ||
| "placeName": "숙명여자대학교", | ||
| "placeKey": "a9f3c0d3b1f74c8a9c2a1d9a7b3e1234", | ||
| "address": "서울특별시 용산구 청파로47길 99", | ||
| "category": "교육,학문>대학교", | ||
| "link": "https://map.naver.com/v5/search/%EC%88%99%EB%AA%85%EC%97%AC%EC%9E%90%EB%8C%80%ED%95%99%EA%B5%90", | ||
| "telephone": "02-710-9114", | ||
| "coordinate": { | ||
| "latitude": 37.545947, | ||
| "longitude": 126.964578 | ||
| }, | ||
| "imgUrls": [ | ||
| "https://image.campus.com/place/1.jpg" | ||
| ] | ||
| } | ||
| } | ||
| """ | ||
| ) | ||
| } |
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.
Swagger 예시의 placeKey가 실키일 가능성 점검 필요 (Line 69).
정적 분석에서 API 키 패턴으로 감지되었습니다. 실제 키라면 즉시 회전하고, 문서 예시는 명확한 더미 값으로 교체해주세요.
🛡️ 예시 값 교체안
- "placeKey": "a9f3c0d3b1f74c8a9c2a1d9a7b3e1234",
+ "placeKey": "EXAMPLE_PLACE_KEY_DO_NOT_USE",🧰 Tools
🪛 Gitleaks (8.30.0)
[high] 69-69: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java`
around lines 48 - 85, The example JSON in ReviewController.java embeds a value
that matches an API/key pattern at place.placeKey (inside the requestBody
ExampleObject for ReviewRequest); replace that string with a clearly fake
placeholder (e.g. "example-place-key-0000" or "PLACE_KEY_EXAMPLE") in the
ExampleObject so no real keys appear in the swagger example, and if that value
was an actual secret rotate it immediately; update the ExampleObject in the
requestBody schema for ReviewRequest to use the dummy value.
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: 2
🤖 Fix all issues with AI agents
In
`@src/main/java/com/campus/campus/domain/review/application/dto/request/PartnershipReviewRequest.java`:
- Around line 11-26: The isVerified field in PartnershipReviewRequest is
declared as a nullable Boolean which can cause NPEs during unboxing; update the
DTO by either changing the field type to the primitive boolean (isVerified) or
annotate the existing Boolean isVerified with `@NotNull` so nulls are rejected.
Modify the record declaration for PartnershipReviewRequest (the isVerified
component) and adjust any callers/tests expecting null accordingly to ensure the
service/mapper won't encounter unboxing NPEs.
In
`@src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java`:
- Around line 48-68: The OpenAPI docs in ReviewController (the requestBody
example/description) include isVerified but the DTO PlaceReviewRequest does not;
make them consistent by either removing isVerified from the description/example
and any mention in the summary/description text, or add an isVerified Boolean
field to PlaceReviewRequest (with its getter/setter/validation) and update
controller/service handling accordingly; locate the example/schema in
ReviewController and the PlaceReviewRequest class to apply the appropriate
change so the API docs and DTO match.
♻️ Duplicate comments (6)
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (2)
211-220: TOCTOU 경쟁 조건:existsByUserAndPlace체크와save사이에 레이스 컨디션 존재
existsByUserAndPlace체크 후save호출 사이에 다른 요청이 끼어들어 중복 저장이 발생할 수 있습니다.UserPartnershipSuggestion엔티티에 unique 제약 조건을 추가하고,DataIntegrityViolationException을 처리해야 합니다.🔧 권장 수정안
//중복 방지 저장 - userPartnershipSuggestionRepository.save( - UserPartnershipSuggestion.create(user, place) - ); + try { + userPartnershipSuggestionRepository.saveAndFlush( + UserPartnershipSuggestion.create(user, place) + ); + } catch (DataIntegrityViolationException e) { + throw new AlreadySuggestedPartnershipException(); + }
226-234: 동시성 문제:CouncilPartnershipSuggestion생성 시 레이스 컨디션 가능
findForUpdate가 빈 결과를 반환하는 경우, 두 요청이 동시에save를 시도하여 unique 제약 위반이 발생할 수 있습니다.findForUpdate는 기존 행에 대한 잠금만 제공하며, 새 행 삽입 시에는 보호되지 않습니다.🔧 권장 수정안
for (StudentCouncil council : councils) { - CouncilPartnershipSuggestion demand = - partnershipSuggestionRepository.findForUpdate(place, council) - .orElseGet(() -> - partnershipSuggestionRepository.save( - CouncilPartnershipSuggestion.create(place, council) - )); - demand.increase(); + try { + CouncilPartnershipSuggestion demand = + partnershipSuggestionRepository.findForUpdate(place, council) + .orElseGet(() -> + partnershipSuggestionRepository.save( + CouncilPartnershipSuggestion.create(place, council) + )); + demand.increase(); + } catch (DataIntegrityViolationException e) { + // 동시 생성 시 재조회 후 증가 + CouncilPartnershipSuggestion demand = + partnershipSuggestionRepository.findForUpdate(place, council) + .orElseThrow(() -> new IllegalStateException("CouncilPartnershipSuggestion 생성 실패")); + demand.increase(); + } }src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (2)
131-134:request.isVerified()null 시 NPE.
Boolean언박싱이므로 null이면 예외가 발생합니다. 안전 비교로 방어해 주세요.✅ 제안 수정
- if (request.isVerified()) { + if (Boolean.TRUE.equals(request.isVerified())) { review.verify(); stampService.grantStampForReview(user, review); }
317-319: 사용자 학적정보 null 체크 필요.
user.getMajor()/getCollege()/getSchool()가 null일 수 있어 NPE 위험이 있습니다. 프로필 미완성 처리 로직 재사용 또는 명확한 예외 처리가 필요합니다.src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java (1)
44-50:isVerified언박싱 NPE 위험.
Boolean이 null일 수 있으므로 안전 변환을 넣어 주세요.✅ 제안 수정
- .isVerified(request.isVerified()) + .isVerified(Boolean.TRUE.equals(request.isVerified()))src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java (1)
69-71: Swagger 예시의 placeKey 값 교체 필요.키 패턴으로 탐지되어 실제 키로 오해될 수 있습니다. 명확한 더미 값으로 교체하고, 실제 키였다면 즉시 회전하세요.
🛡️ 예시 값 교체
- "placeKey": "a9f3c0d3b1f74c8a9c2a1d9a7b3e1234", + "placeKey": "EXAMPLE_PLACE_KEY_DO_NOT_USE",
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.javasrc/main/java/com/campus/campus/domain/review/application/dto/request/PartnershipReviewRequest.javasrc/main/java/com/campus/campus/domain/review/application/dto/request/PlaceReviewRequest.javasrc/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.javasrc/main/java/com/campus/campus/domain/review/application/service/ReviewService.javasrc/main/java/com/campus/campus/domain/review/presentation/ReviewController.java
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (4)
src/main/java/com/campus/campus/domain/place/application/exception/AlreadySuggestedPartnershipException.java (1)
AlreadySuggestedPartnershipException(5-9)src/main/java/com/campus/campus/domain/place/application/exception/NaverMapAPIException.java (1)
NaverMapAPIException(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/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)
src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (4)
src/main/java/com/campus/campus/domain/councilpost/application/exception/PlaceInfoNotFoundException.java (1)
PlaceInfoNotFoundException(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/review/application/exception/NotPartnershipReceiptException.java (1)
NotPartnershipReceiptException(5-9)src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)
🪛 Gitleaks (8.30.0)
src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java
[high] 70-70: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (4)
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (2)
189-201: LGTM!
createPlace메서드에서DataIntegrityViolationException을 적절히 처리하여 동시 생성 시나리오를 안전하게 처리하고 있습니다.
353-375: LGTM!
resolveCouncils메서드가 사용자의 학과, 단과대, 학교에 따라 관련 학생회를 올바르게 조회합니다. 빈 리스트 반환도suggestPartnership에서 적절히 처리됩니다.src/main/java/com/campus/campus/domain/review/application/dto/request/PlaceReviewRequest.java (1)
14-49: PlaceReviewRequest 레코드 구성 OK.필드 구성과 검증 어노테이션이 명확하고 DTO 명칭도 일관되어 보입니다.
src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (1)
99-102: 프로젝트가 Java 21을 대상으로 명시적으로 설정되어 있으므로,List#getFirst()는 완전히 호환되며 컴파일 오류가 발생하지 않습니다. 변경이 불필요합니다.Likely an incorrect or invalid review comment.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| public record PartnershipReviewRequest( | ||
| @NotNull | ||
| @Size(min = 10, message = "리뷰 내용은 최소 10자 이상이어야 합니다.") | ||
| @Schema(example = "아주 정말 맛있습니다. 저의 완전 짱 또간집. 꼭꼮꼬꼬꼭 가세요.") | ||
| String content, | ||
|
|
||
| @NotNull | ||
| @DecimalMin(value = "0.0", inclusive = true) | ||
| @DecimalMax(value = "5.0", inclusive = true) | ||
| @Schema(example = "3.5") | ||
| Double star, | ||
|
|
||
| @Schema(description = "영수증 리뷰를 하고 오면 isVerified=True로 주세요.") | ||
| Boolean isVerified, | ||
|
|
||
| List<String> imageUrls |
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.
isVerified nullable → 언박싱 NPE 위험.
Boolean에 @NotNull이 없어 null이 들어오면 서비스/매퍼에서 boolean 언박싱 시 NPE가 납니다. primitive boolean으로 변경(또는 @NotNull 강제)해 주세요.
✅ 제안 수정
- `@Schema`(description = "영수증 리뷰를 하고 오면 isVerified=True로 주세요.")
- Boolean isVerified,
+ `@Schema`(description = "영수증 리뷰를 하고 오면 isVerified=true로 주세요.")
+ boolean isVerified,🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/review/application/dto/request/PartnershipReviewRequest.java`
around lines 11 - 26, The isVerified field in PartnershipReviewRequest is
declared as a nullable Boolean which can cause NPEs during unboxing; update the
DTO by either changing the field type to the primitive boolean (isVerified) or
annotate the existing Boolean isVerified with `@NotNull` so nulls are rejected.
Modify the record declaration for PartnershipReviewRequest (the isVerified
component) and adjust any callers/tests expecting null accordingly to ensure the
service/mapper won't encounter unboxing NPEs.
| summary = "리뷰 작성(제휴 없음)", | ||
| description = "제휴 가게여서 영수증 인증을 마쳤다면 isVerified=true 값으로 넘겨주세요.", | ||
| requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( | ||
| required = true, | ||
| content = @io.swagger.v3.oas.annotations.media.Content( | ||
| mediaType = "application/json", | ||
| examples = @io.swagger.v3.oas.annotations.media.ExampleObject( | ||
| name = "리뷰 작성 요청 예시", | ||
| summary = "리뷰 작성 Request Body", | ||
| value = """ | ||
| { | ||
| "content": "아주 정말 맛있습니다. 저의 완전 짱 또간집. 꼭꼮꼬꼬꼭 가세요.", | ||
| "star": 3.5, | ||
| "imageUrls": [ | ||
| "https://image1.jpg", | ||
| "https://image2.jpg" | ||
| ], | ||
| "place": { | ||
| "placeName": "숙명여자대학교", | ||
| "placeKey": "string", | ||
| "address": "서울특별시 용산구 청파로47길 99", | ||
| "category": "교육,학문>대학교", | ||
| "link": "https://map.naver.com/v5/search/%EC%88%99%EB%AA%85%EC%97%AC%EC%9E%90%EB%8C%80%ED%95%99%EA%B5%90", | ||
| "telephone": "010-1234-1234", | ||
| "coordinate": { | ||
| "latitude": 37.545947, | ||
| "longitude": 126.964578 | ||
| }, | ||
| "imgUrls": [ | ||
| "https://place-image1.jpg" | ||
| ] | ||
| } | ||
| } | ||
| """ | ||
| ) | ||
| schema = @Schema(implementation = PlaceReviewRequest.class), | ||
| examples = { | ||
| @io.swagger.v3.oas.annotations.media.ExampleObject( | ||
| name = "리뷰 작성 요청 예시", | ||
| summary = "영수증 인증 리뷰 작성", | ||
| value = """ | ||
| { | ||
| "content": "아주 정말 맛있습니다. 저의 완전 짱 또간집. 꼭꼭꼭꼭꼭꼭꼭꼭꼮 가세요.", | ||
| "star": 4.5, | ||
| "imageUrls": [ | ||
| "https://image.campus.com/review/1.jpg", | ||
| "https://image.campus.com/review/2.jpg" | ||
| ], | ||
| "isVerified": true, | ||
| "place": { |
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.
요청 DTO와 문서 예시/설명 불일치.
요약은 “제휴 없음”인데 설명/예시에 isVerified가 포함되어 있고, PlaceReviewRequest에는 해당 필드가 없습니다. 문서 또는 DTO를 정합성 있게 맞춰 주세요.
🤖 Prompt for AI Agents
In
`@src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java`
around lines 48 - 68, The OpenAPI docs in ReviewController (the requestBody
example/description) include isVerified but the DTO PlaceReviewRequest does not;
make them consistent by either removing isVerified from the description/example
and any mention in the summary/description text, or add an isVerified Boolean
field to PlaceReviewRequest (with its getter/setter/validation) and update
controller/service handling accordingly; locate the example/schema in
ReviewController and the PlaceReviewRequest class to apply the appropriate
change so the API docs and DTO match.
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: 1
🤖 Fix all issues with AI agents
In
`@src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java`:
- Around line 289-297: The calculateDistance(Place place, double lat, double
lng) method currently dereferences place.getCoordinate() without a null check
which can cause NullPointerException; add a defensive null check for
place.getCoordinate() at the start of calculateDistance and if null either throw
a clear exception (e.g., IllegalArgumentException with a descriptive message) or
return a sensible default per your domain rules, then proceed to call
GeoUtil.distanceMeter(...) using place.getCoordinate().latitude() and
.longitude() and round the result as before.
♻️ Duplicate comments (1)
src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java (1)
263-279: 버그: user 학적 정보 null 체크 누락라인 274-278에서
user.getMajor(),user.getCollege(),user.getSchool()에 접근하는데, 이 필드들이 null일 경우NullPointerException이 발생합니다. 다른 메서드들(getPartnershipPlaces,findPartnerInBounds)처럼validateAcademicInfo(user)를 호출해야 합니다.🐛 수정 제안
`@Transactional`(readOnly = true) public PlaceDetailView getPlaceDetails( Long userId, Long placeId, double userLat, double userLng ) { User user = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); + validateAcademicInfo(user); + Place place = placeRepository.findById(placeId) .orElseThrow(PlaceInfoNotFoundException::new);
🧹 Nitpick comments (1)
src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java (1)
176-199:getPartnershipDetail과getPartnershipDetailInternal간의 코드 중복두 메서드가 거의 동일한 로직을 수행합니다.
getPartnershipDetail에서 유효성 검사 후getPartnershipDetailInternal을 호출하도록 리팩터링하면 중복을 제거할 수 있습니다.♻️ 리팩터링 제안
`@Transactional`(readOnly = true) 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(); if (place == null) { throw new PlaceInfoNotFoundException(); } - 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); }Also applies to: 201-233
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java (4)
src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)src/main/java/com/campus/campus/domain/councilpost/application/exception/PostNotFoundException.java (1)
PostNotFoundException(5-9)src/main/java/com/campus/campus/domain/councilpost/application/exception/PlaceInfoNotFoundException.java (1)
PlaceInfoNotFoundException(5-9)src/main/java/com/campus/campus/global/util/geocoder/GeoUtil.java (1)
GeoUtil(3-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (5)
src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java (5)
3-4: LGTM!새로운 기능 구현에 필요한 import들이 적절하게 추가되었습니다.
Also applies to: 10-10, 29-30, 34-36
55-57: LGTM!새로운 기능에 필요한 의존성들이 적절하게 주입되었습니다.
141-145: 목록 조회에서 reviewSummary를 null로, reviewCount를 size로 전달목록 조회 시
reviewSummary에null을,reviewCount에size를 전달하는 것이 의도된 동작인지 확인이 필요합니다.reviewCount파라미터에 페이지 크기(size)를 전달하는 것은 의미적으로 혼란을 줄 수 있습니다.
299-305: LGTM!
BigDecimal과RoundingMode.HALF_UP을 사용한 별점 반올림 로직이 적절합니다.
307-323: LGTM!헬퍼 메서드들이 명확하고 단일 책임 원칙을 잘 따르고 있습니다.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
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: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (1)
432-460: 학적 정보 null 체크 필요
getRankingResult메서드에서user.getMajor(),user.getCollege(),user.getSchool()에 직접 접근하고 있습니다. 사용자의 학적 정보가 설정되지 않은 경우NullPointerException이 발생할 수 있습니다.writePlaceReview와writePartnershipReview에서 호출되므로 리뷰 작성 시 오류가 발생할 수 있습니다.🐛 제안된 수정
메서드 시작 부분에 검증 로직을 추가하거나,
writePlaceReview와writePartnershipReview에서 리뷰 생성 전에 학적 정보를 검증하세요:private ReviewRankingResponse getRankingResult(Place place, User user) { + if (user.getMajor() == null || user.getCollege() == null || user.getSchool() == null) { + return reviewMapper.toReviewRankingResponse(null, 0, null, 0, null, 0); + } + Long placeId = place.getPlaceId(); Major major = user.getMajor();또는 호출하는 메서드에서 사전 검증을 수행하세요.
♻️ Duplicate comments (3)
src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (2)
321-323:@Transactional(readOnly = true)어노테이션 누락
getReviewCount메서드와 일관성을 위해@Transactional(readOnly = true)어노테이션을 추가해야 합니다.♻️ 제안된 수정
+@Transactional(readOnly = true) public double getAverageOfStars(Long placeId) { return reviewRepository.findAverageStarByPlaceId(placeId).orElse(0.0); }
294-319: null 체크 추가 필요
user.getMajor(),user.getCollege(),user.getSchool()이 null일 수 있습니다.User엔티티의isProfileNotCompleted()메서드에서 이 필드들이 null일 수 있음을 확인하고 있습니다. null 체크 없이 접근하면NullPointerException이 발생합니다.🐛 제안된 수정
`@Transactional`(readOnly = true) public ReviewPartnerResponse findPartnership(Long placeId, ReceiptResultDto result, Long userId) { User user = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); + + if (user.getMajor() == null || user.getCollege() == null || user.getSchool() == null) { + throw new AcademicInfoNotSetException(); + } + Place place = placeRepository.findById(placeId) .orElseThrow(PlaceInfoNotFoundException::new);src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java (1)
256-287: user 학적 정보 null 체크 누락라인 274-278에서
user.getMajor(),user.getCollege(),user.getSchool()에 접근하는데, 이 필드들이 null일 경우NullPointerException이 발생합니다. 다른 메서드들(getPartnershipPlaces,findPartnerInBounds)과 동일하게validateAcademicInfo(user)검증 로직을 적용해야 합니다.🐛 제안된 수정
`@Transactional`(readOnly = true) public PlaceDetailView getPlaceDetails( Long userId, Long placeId, double userLat, double userLng ) { User user = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); + validateAcademicInfo(user); + Place place = placeRepository.findById(placeId) .orElseThrow(PlaceInfoNotFoundException::new);
🧹 Nitpick comments (2)
src/main/java/com/campus/campus/domain/place/domain/repository/CouncilPartnershipSuggestionRepository.java (1)
16-29: 잘 구현되었습니다.
PESSIMISTIC_WRITE잠금을 사용하여 제휴 제안 요청 횟수 증가 시 동시성 문제를 방지하는 것은 적절한 접근입니다. 메서드명findForUpdate도 잠금 의도를 명확히 전달합니다.다만, 서비스 레이어에서 이 레포지토리를 사용할 때 다른 엔티티에도 잠금을 걸 경우, 잠금 획득 순서를 일관되게 유지하여 데드락을 방지해야 합니다.
추가로, 잠금 없이 조회만 필요한 경우를 위해
findByPlaceAndCouncil메서드를 별도로 추가하는 것도 고려해 볼 수 있습니다.♻️ 읽기 전용 조회 메서드 추가 제안
public interface CouncilPartnershipSuggestionRepository extends JpaRepository<CouncilPartnershipSuggestion, Long> { + Optional<CouncilPartnershipSuggestion> findByPlaceAndCouncil(Place place, StudentCouncil council); + `@Lock`(LockModeType.PESSIMISTIC_WRITE) `@Query`(""" SELECT cps FROM CouncilPartnershipSuggestion cps WHERE cps.place = :place AND cps.council = :council """) Optional<CouncilPartnershipSuggestion> findForUpdate( `@Param`("place") Place place, `@Param`("council") StudentCouncil council ); }src/main/java/com/campus/campus/domain/place/domain/entity/UserPartnershipSuggestion.java (1)
20-26: 동시성 문제: (user_id, place_id) 유니크 제약조건 누락
CouncilPartnershipSuggestion엔티티는@UniqueConstraint(columnNames = {"place_id", "student_council_id"})를 사용하고 있습니다.UserPartnershipSuggestion도 동일한 사용자가 동일한 장소에 대해 중복으로 제휴 제안을 할 수 없도록 유니크 제약조건을 추가해야 합니다. 서비스 레벨의existsByUserAndPlace체크만으로는 동시 요청 시 중복 데이터가 생성될 수 있습니다.♻️ 제안된 수정
`@Entity` -@Table(name = "user_partnership_suggestion") +@Table(name = "user_partnership_suggestion", + uniqueConstraints = `@UniqueConstraint`(columnNames = {"user_id", "place_id"})) `@Getter` `@SuperBuilder` `@NoArgsConstructor`(access = AccessLevel.PROTECTED) `@AllArgsConstructor`(access = AccessLevel.PRIVATE) public class UserPartnershipSuggestion extends BaseEntity {
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
src/main/java/com/campus/campus/domain/notification/application/dto/CursorResponse.javasrc/main/java/com/campus/campus/domain/notification/application/dto/NextCursor.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/PlaceDetailResponse.javasrc/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.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/CouncilPartnershipSuggestionRepository.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/review/application/service/ReviewService.java
💤 Files with no reviewable changes (1)
- src/main/java/com/campus/campus/domain/place/domain/repository/PlaceRepository.java
✅ Files skipped from review due to trivial changes (1)
- src/main/java/com/campus/campus/domain/notification/application/dto/CursorResponse.java
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/com/campus/campus/domain/place/domain/entity/CouncilPartnershipSuggestion.java
🧰 Additional context used
🧬 Code graph analysis (3)
src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java (4)
src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)src/main/java/com/campus/campus/domain/councilpost/application/exception/PostNotFoundException.java (1)
PostNotFoundException(5-9)src/main/java/com/campus/campus/domain/councilpost/application/exception/PlaceInfoNotFoundException.java (1)
PlaceInfoNotFoundException(5-9)src/main/java/com/campus/campus/global/util/geocoder/GeoUtil.java (1)
GeoUtil(3-32)
src/main/java/com/campus/campus/domain/place/domain/entity/UserPartnershipSuggestion.java (2)
src/main/java/com/campus/campus/domain/place/domain/entity/CouncilPartnershipSuggestion.java (1)
Entity(22-60)src/main/java/com/campus/campus/domain/review/domain/entity/Review.java (1)
Entity(22-61)
src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (4)
src/main/java/com/campus/campus/domain/councilpost/application/exception/PlaceInfoNotFoundException.java (1)
PlaceInfoNotFoundException(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/review/application/exception/NotPartnershipReceiptException.java (1)
NotPartnershipReceiptException(5-9)src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (10)
src/main/java/com/campus/campus/domain/notification/application/dto/NextCursor.java (1)
8-9: LGTM!포맷팅만 변경된 리팩터링입니다. 기능적 변경 사항 없이 코드 스타일 일관성을 개선하였습니다.
src/main/java/com/campus/campus/domain/place/domain/repository/CouncilPartnershipSuggestionRepository.java (1)
1-15: LGTM!패키지 선언과 import가 적절하게 구성되어 있습니다.
src/main/java/com/campus/campus/domain/place/application/dto/response/PlaceDetailResponse.java (1)
7-25: LGTM!장소 상세 정보를 담는 DTO 레코드 구조가 적절합니다.
PlaceDetailView인터페이스 구현을 통해 다형성을 지원하는 설계가 좋습니다.src/main/java/com/campus/campus/domain/place/domain/repository/UserPartnershipSuggestionRepository.java (1)
9-12: LGTM!Spring Data JPA 리포지토리 인터페이스가 적절하게 정의되었습니다.
existsByUserAndPlace파생 쿼리 메서드가 중복 확인에 적합합니다.src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java (3)
79-100: LGTM!
writePlaceReview메서드의 구조와 흐름이 적절합니다. 이미지 개수 제한 검증, 장소 생성, 리뷰 저장, 이미지 저장이 순차적으로 잘 처리되고 있습니다.
102-129: LGTM!
writePartnershipReview메서드가 제휴 리뷰 작성 로직을 잘 구현하고 있습니다.isVerified플래그에 따른 인증 및 스탬프 부여 로직이 적절합니다.
210-241: LGTM!
getReviewSummaryList메서드가 효율적으로 구현되었습니다. 리뷰 ID 목록을 먼저 추출한 후 이미지를 일괄 조회하여 N+1 문제를 방지하고 있습니다.src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java (3)
201-233: LGTM!
getPartnershipDetail메서드에@Transactional(readOnly = true)어노테이션과placenull 체크가 추가되어 이전 리뷰 피드백이 잘 반영되었습니다.
107-108: LGTM!
averageStarMap조회 시post.getPlace().getPlaceId()를 사용하여 이전 리뷰에서 지적된 잘못된 키 사용 문제가 수정되었습니다.Also applies to: 133-134
289-305: LGTM!
calculateDistance와calculateAverageStar헬퍼 메서드가 잘 분리되었습니다.BigDecimal을 사용한 평균 별점 반올림 처리가 적절합니다.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
🔀 변경 내용
📸 스크린샷 (선택)
📎 참고 이슈
관련 이슈 번호 #86
Summary by CodeRabbit
새로운 기능
개선사항
✏️ Tip: You can customize this high-level summary in your review settings.