-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/#31 : 제휴게시글 관련 기능 구현(재구현) #49
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
WalkthroughPostRequest.place를 문자열에서 SavedPlaceInfo 객체로 바꾸고 StudentCouncilPost.place를 Place 엔티티 연관으로 전환합니다. 제휴(partnership) 기능(리스트, 지도, 상세)을 신규 추가하고 지오코더 통합(GeoCoderClient) 및 거리 계산 유틸(GeoUtil)을 도입합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Controller as PartnershipController
participant Service as PartnershipService
participant PostRepo as StudentCouncilPostRepository
participant UserRepo as UserRepository
participant LikedRepo as LikedPlacesRepository
participant ImgRepo as PostImageRepository
participant Mapper as PlaceMapper
Client->>Controller: GET /api/partnership/list (userId, lat, lng, cursor, size)
Controller->>Service: getPartnershipPlaces(userId, cursor, size, lat, lng)
Service->>UserRepo: load user
Service->>PostRepo: findByUserScopeWithCursor(..., cursor, now, pageable)
PostRepo-->>Service: List<StudentCouncilPost>
loop for each post
Service->>Service: compute distance (GeoUtil)
Service->>LikedRepo: existsByUserAndPlace(user, place)
LikedRepo-->>Service: isLiked
Service->>ImgRepo: findImageUrlsByPost(post)
ImgRepo-->>Service: imgUrls
Service->>Mapper: toPartnershipResponse(user, post, place, isLiked, imgUrls, distance)
Mapper-->>Service: PartnershipResponse
end
Service-->>Controller: List<PartnershipResponse>
Controller-->>Client: CommonResponse(List)
sequenceDiagram
participant Client
participant PlaceController
participant PlaceService
participant GeoCoder as GeoCoderClient
participant Naver as NaverPlaceClient
participant Mapper as PlaceMapper
Client->>PlaceController: GET /places/search?lat=&lng=&keyword=
PlaceController->>PlaceService: search(lat, lng, keyword)
PlaceService->>GeoCoder: getAddress(lat, lng)
GeoCoder-->>PlaceService: AddressResponse
PlaceService->>PlaceService: build searchWord = keyword + " " + address
PlaceService->>Naver: search(searchWord)
Naver-->>PlaceService: List<PlaceInfo>
loop each place
PlaceService->>Mapper: toSavedPlaceInfo(placeInfo)
Mapper-->>PlaceService: SavedPlaceInfo
end
PlaceService-->>PlaceController: List<SavedPlaceInfo>
PlaceController-->>Client: CommonResponse(List)
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
📜 Recent review detailsConfiguration used: Organization UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ 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)
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: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java (2)
30-43: NullPointerException 위험성을 방어해야 합니다.
post.getPlace().getPlaceName()호출이 Place가 null인 경우 NPE를 발생시킬 수 있습니다. StudentCouncilPost의 place 필드는@ManyToOne으로 nullable이며, null 체크 없이 체이닝 호출을 하고 있습니다.이는 Line 50, 62, 72, 87, 112, 140에서도 동일하게 반복됩니다.
🛡️ Null-safe 처리 제안
방법 1: Optional 사용
public PostListItemResponse toPostListItemResponse(StudentCouncilPost post, boolean isLiked) { + String placeName = Optional.ofNullable(post.getPlace()) + .map(Place::getPlaceName) + .orElse(""); + return new PostListItemResponse( post.getId(), post.getCategory(), post.getTitle(), - post.getPlace().getPlaceName(), + placeName, post.isEvent() ? post.getStartDateTime() : post.getEndDateTime(), post.getThumbnailImageUrl(), post.getThumbnailIcon(), isLiked ); }방법 2: StudentCouncilPost에 null-safe 헬퍼 메서드 추가
// StudentCouncilPost.java에 추가 public String getPlaceNameSafe() { return place != null ? place.getPlaceName() : ""; }그 후 매퍼에서:
- post.getPlace().getPlaceName(), + post.getPlaceNameSafe(),동일한 패턴을 모든 매퍼 메서드에 적용하세요.
146-159: Place 파라미터의 null 검증이 필요합니다.
createStudentCouncilPost메서드가 Place 파라미터를 받지만 null 검증이 없습니다. 이후 매퍼에서getPlace().getPlaceName()을 호출할 때 NPE가 발생할 수 있습니다.🔍 검증 로직 제안
public StudentCouncilPost createStudentCouncilPost(StudentCouncil writer, Place place, PostRequest dto, LocalDateTime startDateTime, LocalDateTime endDateTime) { + Objects.requireNonNull(place, "Place must not be null"); + return StudentCouncilPost.builder() .writer(writer) .category(dto.category()) .title(dto.title()) .content(dto.content()) .place(place) .startDateTime(startDateTime) .endDateTime(endDateTime) .thumbnailImageUrl(dto.thumbnailImageUrl()) .thumbnailIcon(dto.thumbnailIcon()) .build(); }src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java (1)
46-56: 중복된 Repository 주입 제거 필요
postRepository(Line 46)와studentCouncilPostRepository(Line 55)가 동일한StudentCouncilPostRepository타입으로 중복 주입되어 있습니다. 하나만 사용해야 합니다.또한
partnershipService(Line 56)가 주입되었지만 이 파일에서 사용되지 않습니다.🔧 수정 제안
private final StudentCouncilPostRepository postRepository; private final StudentCouncilRepository studentCouncilRepository; private final PostImageRepository postImageRepository; private final PresignedUrlService presignedUrlService; private final StudentCouncilPostMapper studentCouncilPostMapper; private static final int MAX_IMAGE_COUNT = 10; private static final long UPCOMING_EVENT_WINDOW_HOURS = 72L; private final PlaceService placeService; - private final StudentCouncilPostRepository studentCouncilPostRepository; - private final PartnershipService partnershipService;
🤖 Fix all issues with AI agents
In
@src/main/java/com/campus/campus/domain/councilpost/application/dto/request/PostRequest.java:
- Around line 26-43: The PostRequest DTO's place field (SavedPlaceInfo place)
lacks validation annotations, allowing null values and skipping nested
validation; add @NotNull and @Valid to the place field in the PostRequest class
so the field is non-null and SavedPlaceInfo's constraints are validated, and
ensure imports for javax/ jakarta validation annotations are included.
- Line 43: Add a javax.validation @NotNull annotation to the place field in the
PostRequest DTO (the SavedPlaceInfo place declaration) so validation fails
early; import javax.validation.constraints.NotNull (or jakarta equivalent) and
ensure request validation is triggered for controllers using PostRequest so
callers like PlaceService.findOrCreatePlace(),
StudentCouncilPostService.update(), StudentCouncilPostMapper.* (where
post.getPlace().getPlaceName() is used) and PostCategory.validateAndNormalize()
no longer see nulls at runtime.
In
@src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostResponse.java:
- Line 25: Many DTOs still expose a field named place while GetPostResponse and
PostListItemResponse use placeName; update the five DTO classes
GetPostListForCouncilResponse, GetUpcomingEventListForCouncilResponse,
GetPostForUserResponse, GetLikedPostResponse, and
GetActivePartnershipListForUserResponse to rename the field from place to
placeName and adjust all related symbols—constructors, builders/static factory
methods, getters/setters, equals/hashCode/toString, and any JSON/serialization
annotations (e.g., @JsonProperty) and mapper code that populate or read that
field—to use placeName so the API contract is consistent across all response
DTOs.
In
@src/main/java/com/campus/campus/domain/councilpost/application/dto/response/PostListItemResponse.java:
- Line 12: PostListItemResponse currently defines the field placeName which is
inconsistent with other response DTOs that use place; change the DTO to use the
same field name "place" (update the field name in PostListItemResponse, its
constructor/builder/getter/setter, any Jackson/serialization annotations, and
all usages/mappers that populate PostListItemResponse) and likewise update
GetPostResponse if it uses placeName—ensure all mapping code (e.g., methods that
convert entities to PostListItemResponse or GetPostResponse) assigns to the
unified "place" property so all response DTOs share the same field name.
In
@src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java:
- Around line 215-218: In StudentCouncilPostService, safeguard against
NullPointerException when comparing and potentially replacing the post's place:
null-check post.getPlace() and dto.place() before calling place.getPlaceName()
or dto.place().placeName(); if dto.place() is null, skip place replacement or
handle according to business rules, and if post.getPlace() is null treat it as
different (or create new) so call placeService.findOrCreatePlace(dto) only when
dto.place() is non-null; update the local variable place with the result of
findOrCreatePlace(dto) accordingly and avoid calling place.getPlaceName() when
place is null.
In
@src/main/java/com/campus/campus/domain/councilpost/domain/entity/StudentCouncilPost.java:
- Around line 49-51: Add a non-null DB constraint to the place relation and make
runtime checks explicit: update the StudentCouncilPost entity's place mapping to
annotate the join column as non-nullable (e.g., @JoinColumn(name = "place_id",
nullable = false)) so the DB and JPA model reflect that Place is mandatory
(StudentCouncilPost.place). Then change callers: in StudentCouncilPostMapper's
methods that call post.getPlace().getPlaceName() add a null-check or use
Objects.requireNonNull(post.getPlace(), "place") before accessing
getPlaceName(), and in the update method that assigns Place validate the
incoming Place parameter (throw IllegalArgumentException or similar) instead of
assigning null silently; also ensure StudentCouncilPostService.create still uses
PlaceService.findOrCreatePlace(dto) as the source of truth.
In
@src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java:
- Line 209: The @EntityGraph on StudentCouncilPostRepository uses attributePaths
= {"writer", "writer.school", "writer.college", "writer.major", "placeName"} but
"placeName" is not a mapped field; replace it with the actual association name
(likely "place") used in the StudentCouncilPost entity so the EntityGraph
references a real relationship (update the attributePaths to include "place"
instead of "placeName" in the @EntityGraph on StudentCouncilPostRepository).
In
@src/main/java/com/campus/campus/domain/partnership/presentation/PartnershipResponseCode.java:
- Line 14: Remove the unused duplicate enum constant PLACE_SAVE_SUCCESS from
PartnershipResponseCode: delete the PLACE_SAVE_SUCCESS entry in the
PartnershipResponseCode enum so only PlaceResponseCode defines it
(PlaceController already uses PlaceResponseCode.PLACE_SAVE_SUCCESS at line 78);
ensure no references remain to PartnershipResponseCode.PLACE_SAVE_SUCCESS, run a
build/compile to confirm removal and update any imports if necessary.
In
@src/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.java:
- Around line 18-19: The ErrorCode enum contains duplicate numeric codes:
PLACE_CREATION_ERROR and GEOCODER_ERROR both use 2605; update one of them (e.g.,
change GEOCODER_ERROR to use a unique code such as 2606 or another unused value)
so each enum constant in ErrorCode has a distinct integer code while leaving the
HttpStatus and message unchanged.
In
@src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java:
- Around line 108-121: The method findOrCreatePlace contains a redundant
presence check: it first returns when existing.isPresent(), then uses
existing.orElseGet(...) which is unnecessary because the earlier return prevents
reaching that code; simplify by replacing the two-step logic with a single
expression orElseGet on placeRepository.findByPlaceKey(placeKey) or by using
ifPresent/else to save—i.e., use
placeRepository.findByPlaceKey(placeKey).orElseGet(() ->
placeRepository.save(placeMapper.createPlace(place))) or rewrite the control
flow in findOrCreatePlace to only call
placeMapper.createPlace/placeRepository.save when the Optional from
placeRepository.findByPlaceKey is empty.
- Around line 63-74: The search method can concatenate a null nowAddress (from
toStringAddress) with keyword producing "nullkeyword"; modify search to
null-safe the address: call toStringAddress(geocoderRes) into nowAddress, if
nowAddress is null or blank set nowAddress = "" (or a sensible default) and then
build searchWord by joining address and keyword with a separator (e.g., a space)
and trimming the result; update references to nowAddress and searchWord so
downstream calls (naverMapClient.searchPlaces) never receive a "null..." string.
- Line 65: PlaceService.search() currently calls GeoCoderClient.getAddress(lat,
lng) (line with AddressResponse geocoderRes) without handling
network/timeout/JSON/runtime errors; wrap that call in a try-catch that catches
WebClientRequestException, java.util.concurrent.TimeoutException (or Reactor
TimeoutCancellationException),
com.fasterxml.jackson.core.JsonProcessingException and generic RuntimeException,
log the error via the service logger, and either return a safe fallback
AddressResponse or rethrow a clear domain exception (e.g., GeocodingException)
so the controller can translate it; additionally, update
GeoCoderClient.getAddress()'s WebClient configuration to set connect/read/write
timeouts and add a Reactor retry strategy or circuit-breaker and ensure it maps
WebClient errors into a consistent exception type used by PlaceService.
In
@src/main/java/com/campus/campus/domain/place/infrastructure/geocoder/GeoCoderClient.java:
- Around line 31-49: The getAddress method uses webClient and calls .block()
without any timeout, risking indefinite hangs; add a timeout to the reactive
pipeline (e.g., use .timeout(Duration.ofSeconds(...)) before .block() or call
.block(Duration.ofSeconds(...))) or configure the underlying
WebClient/HttpClient with connect/read timeouts, and ensure timeouts are mapped
to GeoCoderException (or a specific timeout exception) so callers of getAddress
receive a deterministic error; update getAddress, the reactive chain on
webClient, and GeoCoderException handling accordingly.
In
@src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java:
- Around line 50-52: The login method in KakaoOauthService calls getToken(...)
and then KakaoTokenResponse.accessToken() without null checks which can cause an
NPE; update login to validate the getToken(...) return (and that
KakaoTokenResponse.accessToken() is non-null/non-empty) before calling
getUserInfo(...), and if the token or accessToken is missing throw a clear
domain exception (or IllegalStateException) with a descriptive message so
callers of OauthLoginResponse know token exchange failed; reference the login
method, getToken, KakaoTokenResponse.accessToken(), and getUserInfo for locating
the change.
In @src/main/java/com/campus/campus/global/config/PermitUrlConfig.java:
- Line 33: 현재 PermitUrlConfig에 있는 "/storage/presigned" 항목을 공개 permitAll 목록에서
제거하거나 보호 로직을 추가해야 합니다: 찾아 수정할 대상은 PermitUrlConfig 클래스에 정의된 permit URL 목록(현재에
"/storage/presigned" 문자열)이며, 해결 방안은 두 가지 중 하나로 구현합니다 — 1) 해당 엔드포인트가 인증 필요하다면
목록에서 "/storage/presigned"를 제거하고 HttpSecurity (또는 configure 메서드)에서 인증 필수로 변경하여 인증
필터(예: JWT/Session)로 보호하거나, 2) 진짜로 public이어야 한다면 permitAll을 유지하되 요청당 생성 제한을 위해
rate-limiting 필터(예: Bucket4j 또는 Spring RateLimiter) 또는 IP 화이트리스트/토큰 검증 필터를 해당
엔드포인트 처리 흐름에 추가하여 무제한 presigned URL 생성을 방지하십시오.
In @src/main/java/com/campus/campus/global/util/jwt/GeoUtil.java:
- Line 1: GeoUtil is misplaced in the jwt package; move the GeoUtil class to a
more appropriate package (e.g., package global.util or global.util.geo) by
updating the package declaration in the GeoUtil source to the chosen package and
relocating the file to the matching directory; then update all
references/imports of GeoUtil across the codebase to the new package name
(search for usages of GeoUtil) so compilation and imports remain correct.
In @src/main/resources/application-local.yml:
- Around line 65-66: application-dev.yml and application-prod.yml are missing
the required map.geocoder.api-key setting used by the GeoCoderClient component;
add the map.geocoder.api-key: ${GEOCODER_API_KEY} (under map.geocoder) to both
application-dev.yml and application-prod.yml so the GeoCoderClient can receive
its required property, and update deployment/setup docs to ensure the
GEOCODER_API_KEY environment variable (VWorld API key) is provisioned in dev and
prod.
🧹 Nitpick comments (18)
src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java (2)
100-120: HTTP 에러 처리 및 RestClient 재사용 개선을 권장합니다.
에러 처리 부재: Kakao API 호출 실패 시 (4xx/5xx 응답) Spring의 기본 예외가 발생하며, 클라이언트에게 의미 있는 에러 메시지를 전달하기 어렵습니다.
RestClient 인스턴스 재사용: 매 요청마다
RestClient.create()를 호출하는 대신, Bean으로 주입받거나 필드로 재사용하면 효율적입니다.♻️ 개선 제안
에러 핸들링 추가:
+import org.springframework.web.client.RestClientException; private KakaoTokenResponse getToken(String authorizationCode) { RestClient client = RestClient.create(); // ... body 설정 ... + try { return client.post() .uri(KAUTH_BASE_URL + "/oauth/token") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .body(body) .retrieve() .body(KakaoTokenResponse.class); + } catch (RestClientException e) { + throw new RuntimeException("카카오 토큰 발급 중 오류가 발생했습니다.", e); + } }RestClient 재사용 (선택사항):
private final RestClient restClient = RestClient.create();
94-97:System.err.println대신 로깅 프레임워크 사용을 권장합니다.프로덕션 환경에서
System.err.println은 로그 레벨 제어, 로그 수집, 포맷팅 등이 불가능합니다. 이미 Lombok을 사용하고 있으므로@Slf4j어노테이션을 추가하여 로거를 사용하세요.♻️ 로깅 개선 제안
+import lombok.extern.slf4j.Slf4j; +@Slf4j @Service @RequiredArgsConstructor public class KakaoOauthService {} catch (Exception e) { - System.err.println("카카오 연결 끊기 실패: " + e.getMessage()); + log.error("카카오 연결 끊기 실패: {}", e.getMessage(), e); return false; }src/main/java/com/campus/campus/domain/place/domain/repository/PlaceRepository.java (1)
3-3: 사용되지 않는 import를 제거하세요.현재
PlaceRepository인터페이스에는List를 사용하는 메서드가 없습니다. 불필요한 import는 코드 정리 차원에서 제거하는 것이 좋습니다.♻️ 제안하는 수정
-import java.util.List; import java.util.Optional;src/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PartnershipPlaceSummary.java (1)
16-16: 주석 처리된 코드를 제거하세요.PR의 목표가 "불필요한 메서드 삭제 및 주석 삭제"인 만큼, 주석 처리된
councilId필드는 완전히 제거하는 것이 좋습니다. 필요시 Git 이력을 통해 복원할 수 있습니다.♻️ 제안하는 수정
Double latitude, Double longitude, - // Long councilId, CouncilType councilType ) {src/main/java/com/campus/campus/global/util/jwt/GeoUtil.java (1)
10-24: 입력값 검증을 추가하는 것을 고려하세요.현재 위도/경도 파라미터에 대한 검증이 없어 잘못된 값이 입력될 경우 부정확한 결과가 반환될 수 있습니다. 위도는 -90 ~ 90, 경도는 -180 ~ 180 범위로 제한하는 것을 권장합니다.
♻️ 제안하는 수정
public static double distanceKm( double lat1, double lon1, double lat2, double lon2 ) { + if (lat1 < -90 || lat1 > 90 || lat2 < -90 || lat2 > 90) { + throw new IllegalArgumentException("위도는 -90에서 90 사이여야 합니다."); + } + if (lon1 < -180 || lon1 > 180 || lon2 < -180 || lon2 > 180) { + throw new IllegalArgumentException("경도는 -180에서 180 사이여야 합니다."); + } + double dLat = Math.toRadians(lat2 - lat1); double dLon = Math.toRadians(lon2 - lon1);src/main/java/com/campus/campus/domain/councilpost/domain/repository/PostImageRepository.java (2)
21-33: 아키텍처 계층 분리를 개선해주세요.이 쿼리 메서드에 몇 가지 설계 관련 우려사항이 있습니다:
계층 의존성 위반:
councilpost.domain.repository가place.application.dto.response.partnership.PostImageSummary를 직접 참조하고 있습니다. 도메인 계층이 애플리케이션 계층의 DTO에 의존하는 것은 일반적인 레이어드 아키텍처 원칙에 위배됩니다.카테고리 비교: Line 27에서
category='PARTNERSHIP'문자열 리터럴을 사용하고 있습니다.PostCategory가 enum이므로 타입 안전성을 위해post.category = com.campus.campus.domain.councilpost.domain.entity.PostCategory.PARTNERSHIP를 사용하는 것이 좋습니다.♻️ 개선 제안
옵션 1 (권장): 간단한 프로젝션 인터페이스를 도메인 계층에 정의하고, 서비스 계층에서 DTO로 매핑
도메인 계층에 프로젝션 인터페이스 추가:
// In councilpost.domain package public interface PartnershipImageProjection { Long getPlaceId(); String getImageUrl(); }Repository 메서드 수정:
- @Query(""" - SELECT NEW com.campus.campus.domain.place.application.dto.response.partnership.PostImageSummary - (post.place.placeId, - img.imageUrl) - FROM PostImage img - JOIN img.post post - WHERE post.category='PARTNERSHIP' - AND post.place.placeId in :placeIds - ORDER BY img.id asc - """) - List<PostImageSummary> findPartnershipImagesByPlaceIds( - @Param("placeIds") List<Long> placeIds - ); + @Query(""" + SELECT post.place.placeId as placeId, img.imageUrl as imageUrl + FROM PostImage img + JOIN img.post post + WHERE post.category = com.campus.campus.domain.councilpost.domain.entity.PostCategory.PARTNERSHIP + AND post.place.placeId in :placeIds + ORDER BY img.id asc + """) + List<PartnershipImageProjection> findPartnershipImagesByPlaceIds( + @Param("placeIds") List<Long> placeIds + );옵션 2: 최소한 enum 비교로 수정
- WHERE post.category='PARTNERSHIP' + WHERE post.category = com.campus.campus.domain.councilpost.domain.entity.PostCategory.PARTNERSHIP
35-41: 기존 메서드와의 중복 가능성을 확인해주세요.Line 19의
findAllByPostOrderByIdAsc메서드가 이미 존재하며,List<PostImage>를 반환합니다. 새로운findImageUrlsByPost메서드는 URL만 추출하지만, 서비스 계층에서 기존 메서드 결과를 매핑하여 동일한 결과를 얻을 수 있습니다.두 메서드 모두 유지하는 것이 성능상 이점이 있는지(프로젝션으로 데이터 전송량 감소) 확인하거나, 일관성을 위해 기존 메서드를 재사용하는 것을 고려해보세요.
src/main/java/com/campus/campus/domain/councilpost/domain/entity/StudentCouncilPost.java (1)
61-83: update 메서드의 Place 파라미터 검증을 고려하세요.
update메서드가 Place를 받지만 null 검증이 없습니다. Place가 필수 필드라면 검증을 추가하는 것이 좋습니다.♻️ 검증 추가 제안
public void update( String title, String content, Place place, LocalDateTime startDateTime, LocalDateTime endDateTime, String thumbnailImageUrl, ThumbnailIcon thumbnailIcon, PostCategory category) { + + Objects.requireNonNull(place, "Place must not be null"); this.title = title; this.content = content; this.place = place;src/main/java/com/campus/campus/domain/place/presentation/PlaceController.java (1)
57-71: 응답 형식의 일관성을 고려하세요.새로운
getAddress엔드포인트가ResponseEntity<AddressResponse>를 직접 반환하는 반면, 다른 엔드포인트들은CommonResponse를 사용합니다. API 응답 형식의 일관성을 유지하는 것이 좋습니다.또한 Swagger 문서화를 위한
@Operation어노테이션이 누락되었습니다.♻️ 일관성 개선 제안
+@Operation(summary = "좌표 기반 주소 조회", description = "위도/경도를 받아 해당 위치의 주소를 반환합니다") @GetMapping -public ResponseEntity<AddressResponse> getAddress( +public CommonResponse<AddressResponse> getAddress( @Parameter( description = "현재 위치의 위도", example = "37.50415" ) @RequestParam double lat, @Parameter( description = "현재 위치의 경도", example = "126.9570" ) @RequestParam double lng ) { - return ResponseEntity.ok(geoCoderClient.getAddress(lat, lng)); + AddressResponse response = geoCoderClient.getAddress(lat, lng); + return CommonResponse.success(PlaceResponseCode.ADDRESS_LOOKUP_SUCCESS, response); }단,
PlaceResponseCode에ADDRESS_LOOKUP_SUCCESS를 추가해야 합니다.src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java (2)
12-12: 사용되지 않는 import 제거 필요
AddressResponse가 import되어 있으나 이 파일에서 사용되지 않습니다.♻️ 수정 제안
-import com.campus.campus.domain.place.application.dto.response.geocoder.AddressResponse;
65-65: 하드코딩된 리뷰 점수 추적 필요리뷰 점수가
5.0으로 하드코딩되어 있습니다. 주석대로 리뷰 기능 구현 시 수정이 필요합니다.이 TODO를 추적하기 위한 이슈를 생성해 드릴까요?
src/main/java/com/campus/campus/domain/place/application/dto/response/geocoder/AddressResponse.java (1)
8-35: 다른 DTO와의 일관성을 위해 record 사용 권장다른 geocoder/search 관련 DTO들(
GoogleTextSearchResponse,NaverSearchResponse,GooglePlaceDetailResponse)은 record를 사용하고 있습니다. 일관성을 위해 record로 변환하는 것을 고려해 주세요.또한 Jackson 역직렬화를 위해
@NoArgsConstructor가 필요할 수 있습니다.♻️ record로 변환 예시
public record AddressResponse(Response response) { public record Response(String status, List<Result> result) {} public record Result(String type, String text, Structure structure) {} public record Structure(String level1, String level2, String level3, String level4L, String level4A) {} }src/main/java/com/campus/campus/domain/partnership/application/service/PartnershipService.java (2)
43-43: 사용되지 않는 Repository 제거
placeRepository가 주입되었지만 사용되지 않습니다.♻️ 수정 제안
- private final PlaceRepository placeRepository;
45-46: 읽기 전용 트랜잭션 명시 필요
getPartnershipPlaces,findPartnerInBounds,getPartnershipDetail메서드들은 읽기 전용 작업을 수행합니다.@Transactional(readOnly = true)로 변경하면 성능 최적화에 도움이 됩니다.♻️ 수정 제안
- @Transactional + @Transactional(readOnly = true) public List<PartnershipResponse> getPartnershipPlaces(...) {- @Transactional + @Transactional(readOnly = true) public List<PartnershipPinResponse> findPartnerInBounds(...) {- @Transactional + @Transactional(readOnly = true) public PartnershipResponse getPartnershipDetail(...) {Also applies to: 103-104, 133-134
src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java (1)
242-276: N+1 쿼리 문제 가능성
findPinsInBounds메서드에@EntityGraph가 없습니다. 결과를 순회하면서writer,writer.school등에 접근할 때 N+1 쿼리 문제가 발생할 수 있습니다. 다른 메서드들처럼@EntityGraph를 추가하는 것을 권장합니다.♻️ 수정 제안
+ @EntityGraph(attributePaths = {"writer", "writer.school", "writer.college", "writer.major", "place"}) @Query(""" SELECT p FROM StudentCouncilPost psrc/main/java/com/campus/campus/domain/partnership/presentation/PartnershipController.java (1)
21-24: Swagger @tag 어노테이션 누락다른 컨트롤러들은
@Tag어노테이션을 사용하여 API 그룹을 지정하고 있습니다. 일관성을 위해 추가해 주세요.♻️ 수정 제안
+import io.swagger.v3.oas.annotations.tags.Tag; + @RestController @RequestMapping("/api/partnership") @RequiredArgsConstructor +@Tag(name = "Partnership", description = "제휴 장소 조회 API") public class PartnershipController {src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (2)
147-148: 사용되지 않는 변수 제거 필요Line 147에서
placeName변수를 선언하지만, line 148에서placeMapper.createPlace(placeInfo)를 호출할 때 사용하지 않습니다. 메서드 시그니처 변경으로 인해 불필요해진 변수입니다.♻️ 미사용 변수 제거
place = placeRepository.findByPlaceKey(placeKey) .orElseGet(() -> { //없으면 생성 - String placeName = stripHtml(placeInfo.placeName()); Place newPlace = placeRepository.save(placeMapper.createPlace(placeInfo)); //신규 생성된 경우에만 이미지 저장 migrateImagesToOci(newPlace.getPlaceKey(), placeInfo.imgUrls());
171-173: 구현되지 않은 기능에 대한 주석 처리좋아요 확인과 제휴 장소 확인 로직에 대한 주석만 있고 구현이 없습니다. 향후 구현 예정이라면
TODO키워드를 추가하여 명확히 표시하거나, 불필요한 주석이라면 제거하는 것이 좋습니다.💡 TODO 마커 추가 제안
- //좋아요 있는지 확인 - - //제휴 장소인지 확인 - + // TODO: 좋아요 여부 확인 로직 구현 + // TODO: 제휴 장소 여부 확인 로직 구현 + return placeMapper.toSavedPlaceInfo(response.item(), response.name(), response.placeKey(),
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (32)
src/main/java/com/campus/campus/domain/councilpost/application/dto/request/PostRequest.javasrc/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostResponse.javasrc/main/java/com/campus/campus/domain/councilpost/application/dto/response/PostListItemResponse.javasrc/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.javasrc/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.javasrc/main/java/com/campus/campus/domain/councilpost/domain/entity/StudentCouncilPost.javasrc/main/java/com/campus/campus/domain/councilpost/domain/repository/PostImageRepository.javasrc/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.javasrc/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.javasrc/main/java/com/campus/campus/domain/partnership/application/dto/response/PartnershipPinResponse.javasrc/main/java/com/campus/campus/domain/partnership/application/service/PartnershipService.javasrc/main/java/com/campus/campus/domain/partnership/presentation/PartnershipController.javasrc/main/java/com/campus/campus/domain/partnership/presentation/PartnershipResponseCode.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/geocoder/AddressResponse.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PartnershipPlaceSummary.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PartnershipResponse.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PostImageSummary.javasrc/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.javasrc/main/java/com/campus/campus/domain/place/application/exception/GeoCoderException.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/place/domain/repository/LikedPlacesRepository.javasrc/main/java/com/campus/campus/domain/place/domain/repository/PlaceRepository.javasrc/main/java/com/campus/campus/domain/place/infrastructure/geocoder/GeoCoderClient.javasrc/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.javasrc/main/java/com/campus/campus/domain/place/presentation/PlaceController.javasrc/main/java/com/campus/campus/domain/place/presentation/PlaceResponseCode.javasrc/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.javasrc/main/java/com/campus/campus/domain/user/presentation/AuthController.javasrc/main/java/com/campus/campus/global/config/PermitUrlConfig.javasrc/main/java/com/campus/campus/global/util/jwt/GeoUtil.javasrc/main/resources/application-local.yml
🧰 Additional context used
🧬 Code graph analysis (18)
src/main/java/com/campus/campus/domain/partnership/presentation/PartnershipResponseCode.java (8)
src/main/java/com/campus/campus/domain/place/application/dto/response/geocoder/AddressResponse.java (4)
Getter(8-35)Getter(14-18)Getter(20-25)Getter(27-34)src/main/java/com/campus/campus/domain/council/presentation/StudentCouncilResponseCode.java (1)
Getter(10-26)src/main/java/com/campus/campus/domain/councilnotice/presentation/NoticeResponseCode.java (1)
Getter(10-23)src/main/java/com/campus/campus/domain/manager/presentation/ManagerResponseCode.java (1)
Getter(10-21)src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostResponseCode.java (1)
Getter(10-27)src/main/java/com/campus/campus/global/common/response/ResponseCodeInterface.java (1)
ResponseCodeInterface(5-11)src/main/java/com/campus/campus/domain/mail/presentation/EmailVerificationResponseCode.java (1)
Getter(10-19)src/main/java/com/campus/campus/global/oci/presentation/PresignedUrlResponseCode.java (1)
Getter(10-19)
src/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PartnershipPlaceSummary.java (2)
src/main/java/com/campus/campus/domain/place/domain/entity/Place.java (1)
Entity(17-57)src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java (1)
SavedPlaceInfo(9-36)
src/main/java/com/campus/campus/domain/councilpost/domain/entity/StudentCouncilPost.java (1)
src/main/java/com/campus/campus/domain/place/domain/entity/Place.java (1)
Entity(17-57)
src/main/java/com/campus/campus/domain/councilpost/application/dto/response/PostListItemResponse.java (6)
src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java (1)
SavedPlaceInfo(9-36)src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostListForCouncilResponse.java (1)
GetPostListForCouncilResponse(10-32)src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetLikedPostResponse.java (1)
GetLikedPostResponse(7-23)src/main/java/com/campus/campus/domain/place/domain/entity/Place.java (1)
Entity(17-57)src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetUpcomingEventListForCouncilResponse.java (1)
GetUpcomingEventListForCouncilResponse(10-29)src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetActivePartnershipListForUserResponse.java (1)
GetActivePartnershipListForUserResponse(5-18)
src/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.java (2)
src/main/java/com/campus/campus/domain/place/domain/entity/LikedPlace.java (1)
Entity(21-45)src/main/java/com/campus/campus/domain/councilpost/domain/repository/LikePostRepository.java (2)
Query(38-43)LikePostRepository(17-44)
src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostResponse.java (5)
src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostListForCouncilResponse.java (1)
GetPostListForCouncilResponse(10-32)src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetLikedPostResponse.java (1)
GetLikedPostResponse(7-23)src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetUpcomingEventListForCouncilResponse.java (1)
GetUpcomingEventListForCouncilResponse(10-29)src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetActivePartnershipListForUserResponse.java (1)
GetActivePartnershipListForUserResponse(5-18)src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java (1)
SavedPlaceInfo(9-36)
src/main/java/com/campus/campus/domain/place/domain/repository/PlaceRepository.java (1)
src/main/java/com/campus/campus/domain/place/domain/repository/PlaceImagesRepository.java (1)
PlaceImagesRepository(10-15)
src/main/java/com/campus/campus/domain/place/application/exception/GeoCoderException.java (5)
src/main/java/com/campus/campus/domain/place/application/exception/PlaceCreationException.java (2)
PlaceCreationException(5-9)PlaceCreationException(6-8)src/main/java/com/campus/campus/domain/place/application/exception/NaverMapAPIException.java (2)
NaverMapAPIException(5-9)NaverMapAPIException(6-8)src/main/java/com/campus/campus/global/common/exception/ApplicationException.java (2)
Getter(5-18)ApplicationException(9-12)src/main/java/com/campus/campus/domain/school/application/exception/CollegeNotFoundException.java (1)
CollegeNotFoundException(5-9)src/main/java/com/campus/campus/domain/user/application/exception/NicknameAlreadyExistsException.java (1)
NicknameAlreadyExistsException(5-9)
src/main/java/com/campus/campus/global/config/PermitUrlConfig.java (1)
src/main/java/com/campus/campus/global/oci/presentation/PresignedUrlController.java (1)
RestController(18-61)
src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java (1)
src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.java (1)
Service(43-277)
src/main/java/com/campus/campus/domain/partnership/application/service/PartnershipService.java (4)
src/main/java/com/campus/campus/domain/councilpost/application/exception/PostNotFoundException.java (1)
PostNotFoundException(5-9)src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)src/main/java/com/campus/campus/global/util/jwt/GeoUtil.java (1)
GeoUtil(3-32)src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetActivePartnershipListForUserResponse.java (1)
GetActivePartnershipListForUserResponse(5-18)
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (3)
src/main/java/com/campus/campus/domain/place/domain/entity/Place.java (1)
Entity(17-57)src/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapClient.java (2)
searchPlaces(32-54)Component(12-56)src/main/java/com/campus/campus/domain/place/application/dto/response/SearchCandidateResponse.java (1)
SearchCandidateResponse(5-12)
src/main/java/com/campus/campus/domain/place/presentation/PlaceController.java (3)
src/main/java/com/campus/campus/domain/place/domain/entity/Place.java (1)
Entity(17-57)src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java (1)
SavedPlaceInfo(9-36)src/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapClient.java (1)
searchPlaces(32-54)
src/main/java/com/campus/campus/domain/place/presentation/PlaceResponseCode.java (2)
src/main/java/com/campus/campus/global/oci/presentation/PresignedUrlResponseCode.java (1)
Getter(10-19)src/main/java/com/campus/campus/domain/councilnotice/presentation/NoticeResponseCode.java (1)
Getter(10-23)
src/main/java/com/campus/campus/domain/place/infrastructure/geocoder/GeoCoderClient.java (1)
src/main/java/com/campus/campus/domain/place/application/exception/GeoCoderException.java (1)
GeoCoderException(5-9)
src/main/java/com/campus/campus/domain/councilpost/application/dto/request/PostRequest.java (1)
src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java (1)
SavedPlaceInfo(9-36)
src/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PartnershipResponse.java (1)
src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java (1)
SavedPlaceInfo(9-36)
src/main/java/com/campus/campus/domain/partnership/presentation/PartnershipController.java (3)
src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java (1)
RestController(32-187)src/main/java/com/campus/campus/domain/place/presentation/PlaceController.java (1)
RestController(26-83)src/main/java/com/campus/campus/domain/user/presentation/AuthController.java (1)
RestController(20-42)
🔇 Additional comments (16)
src/main/java/com/campus/campus/domain/place/presentation/PlaceResponseCode.java (1)
14-15: LGTM! 제휴 장소 조회 응답 코드 추가가 올바릅니다.새로운 enum 상수
CHECK_PARTNERSHIP_PLACE_SUCCESS가 적절하게 추가되었습니다. HTTP 200 OK 상태 코드는 조회 작업에 적합하며, 메시지도 명확합니다. 문법과 코드 컨벤션도 올바르게 적용되었습니다.src/main/java/com/campus/campus/domain/user/presentation/AuthController.java (1)
26-32: LGTM! Authorization Code Flow로의 전환이 적절합니다.Native App 방식(Access Token 직접 전달)에서 Authorization Code Flow로 변경한 것은 보안 측면에서 더 나은 접근입니다. 서비스 레이어의 변경사항과 일관성 있게 구현되었습니다.
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java (1)
54-54: LGTM!주석이
placeName으로 업데이트되어 코드베이스의 네이밍 일관성이 개선되었습니다.src/main/java/com/campus/campus/domain/place/application/exception/GeoCoderException.java (1)
1-9: 구현이 올바릅니다.기존 예외 클래스들(
NaverMapAPIException,PlaceCreationException등)과 동일한 패턴을 따르고 있어 일관성이 유지됩니다.ErrorCode.GEOCODER_ERROR의 중복 코드 이슈가 해결되면 정상적으로 동작할 것입니다.src/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.java (2)
30-30: 메서드 추가가 적절합니다.
existsByUserAndPlace는 Spring Data JPA의 표준 네이밍 규칙을 따르며, 엔티티 기반 존재 여부 확인이 필요한 경우에 유용합니다. 기존findByUserIdAndPlace_PlaceKey메서드를 보완하는 역할을 합니다.
19-28: 반환 타입 변경 제안은 재검토가 필요합니다.검증 결과,
findLikedPlaceIds가Set<Long>을 반환하는 것은 적절한 설계입니다.LikePostRepository.findLikedPostIds는List<Long>을 반환하지만, 실제 사용 코드에서Set<Long> likedPostIds = new HashSet<>(likePostRepository.findLikedPostIds(userId, postIds));로 즉시 Set으로 변환됩니다. 따라서findLikedPlaceIds의 Set 반환 타입이 오히려 변환 오버헤드를 없애고 실제 사용 패턴에 더 부합합니다. 두 엔티티 모두 동일한 unique constraint 구조를 가지고 있으므로, 일관성을 위해서라면findLikedPostIds를 List에서 Set으로 변경하는 것이 더 적절합니다.Likely an incorrect or invalid review comment.
src/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PostImageSummary.java (1)
3-7: LGTM!간단한 projection DTO로 구조가 적절합니다. JPQL constructor expression으로 채워지므로 별도의 validation이 필요하지 않습니다.
src/main/java/com/campus/campus/global/config/PermitUrlConfig.java (1)
34-36: 새로 추가된 partnership 및 places 엔드포인트의 public 접근이 적절합니다."/places", "/api/partnership/list", "/api/partnership/map" 엔드포인트는 일반 사용자가 제휴 정보를 조회할 수 있도록 public으로 공개하는 것이 합리적입니다.
src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java (1)
91-117: LGTM!PARTNERSHIP 예시가 새로운
SavedPlaceInfo구조에 맞게 잘 업데이트되었습니다. 실제 사용 시나리오를 반영한 구체적인 예시로 API 문서의 가독성이 향상되었습니다.src/main/java/com/campus/campus/domain/partnership/application/dto/response/PartnershipPinResponse.java (1)
5-22: LGTM!지도 핀 표시를 위한 적절한 DTO 구조입니다. 위도/경도에 primitive
double을 사용하여 null 불가능하도록 한 것이 좋습니다. @Schema 어노테이션도 명확하게 작성되어 API 문서화에 도움이 됩니다.src/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PartnershipResponse.java (1)
6-23: LGTM!새로운 PartnershipResponse DTO의 구조가 명확하고 잘 정의되어 있습니다. 필드 구성이 적절하며 타입도 올바르게 선택되었습니다.
src/main/java/com/campus/campus/domain/place/presentation/PlaceController.java (1)
81-81: 코드 내 주석은 한국어로 작성해도 무방합니다.프로젝트 내부 커뮤니케이션을 위한 주석이므로 문제없습니다.
src/main/java/com/campus/campus/domain/partnership/presentation/PartnershipResponseCode.java (1)
10-21: ResponseCode 패턴이 다른 도메인과 일관성 있게 구현되었습니다.전체적인 구조는 다른 ResponseCode enum들(StudentCouncilResponseCode, NoticeResponseCode 등)과 일관된 패턴을 따르고 있어 좋습니다.
src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java (1)
73-80: resolveTag 로직 검증 필요
councilType은post.getWriter()에서 가져오지만, 반환되는 이름은user파라미터의 school/college/major 이름입니다. 이 조합이 의도된 것인지 확인이 필요합니다.만약 태그가 글을 작성한 학생회를 나타내야 한다면,
user대신post.getWriter()의 정보를 사용해야 할 수 있습니다.src/main/java/com/campus/campus/domain/partnership/presentation/PartnershipController.java (1)
21-27: 보안 설정이 올바르게 구성되어 있습니다
/api/partnership/list와/api/partnership/map엔드포인트는PermitUrlConfig에 명시적으로 등록되어 있어 공개 접근이 가능하며,/api/partnership/detail엔드포인트는 공개 URL 목록에 없으므로 기본적으로 인증이 필요합니다.SecurityConfig의anyRequest().authenticated()규칙이 이를 자동으로 보호합니다.@CurrentUserId는 단순히 인증된 사용자의 ID를 추출하는 어노테이션일 뿐 인증을 강제하지 않으므로, 현재 설정은 의도된 접근 수준(일부는 공개, 일부는 인증 필수)을 올바르게 구현하고 있습니다.src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
17-17: 새로운 의존성 추가가 적절합니다.지오코딩 기능을 위한
GeoCoderClient,AddressResponse, 그리고 장소 생성을 위한PostRequest임포트가 올바르게 추가되었습니다.Also applies to: 21-21, 34-34, 61-61
src/main/java/com/campus/campus/domain/councilpost/application/dto/request/PostRequest.java
Show resolved
Hide resolved
| String title, | ||
| String content, | ||
| String place, | ||
| String placeName, |
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:
# Search for response DTOs with 'place' field to check current state
rg -n --type=java -B2 -A2 'String place[,;)]' src/main/java/com/campus/campus/domain/councilpost/application/dto/response/Repository: our-campUS/our-campUS-BE
Length of output: 3621
🏁 Script executed:
# Also check for placeName to see which DTOs have been updated
rg -n --type=java -B2 -A2 'String placeName[,;)]' src/main/java/com/campus/campus/domain/councilpost/application/dto/response/Repository: our-campUS/our-campUS-BE
Length of output: 1301
🏁 Script executed:
# List all response DTOs in councilpost domain to ensure comprehensive check
fd -e java "Response\.java" src/main/java/com/campus/campus/domain/councilpost/application/dto/response/Repository: our-campUS/our-campUS-BE
Length of output: 922
응답 DTO들의 필드명 place를 placeName으로 통일하세요.
GetPostResponse와 PostListItemResponse에서만 placeName으로 변경되었고, 다음 5개 DTO에서는 여전히 place를 사용하고 있어 일관성 문제가 발생합니다:
- GetPostListForCouncilResponse
- GetUpcomingEventListForCouncilResponse
- GetPostForUserResponse
- GetLikedPostResponse
- GetActivePartnershipListForUserResponse
모든 응답 DTO의 필드명을 placeName으로 통일하여 API 계약 일관성을 확보하세요.
🤖 Prompt for AI Agents
In
@src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostResponse.java
at line 25, Many DTOs still expose a field named place while GetPostResponse and
PostListItemResponse use placeName; update the five DTO classes
GetPostListForCouncilResponse, GetUpcomingEventListForCouncilResponse,
GetPostForUserResponse, GetLikedPostResponse, and
GetActivePartnershipListForUserResponse to rename the field from place to
placeName and adjust all related symbols—constructors, builders/static factory
methods, getters/setters, equals/hashCode/toString, and any JSON/serialization
annotations (e.g., @JsonProperty) and mapper code that populate or read that
field—to use placeName so the API contract is consistent across all response
DTOs.
| PostCategory category, | ||
| String title, | ||
| String place, | ||
| String placeName, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for place field usage in response DTOs
echo "=== Searching for 'place' field ==="
rg -n --type=java 'String place[,;)]' src/main/java/com/campus/campus/domain/councilpost/application/dto/response/
echo -e "\n=== Searching for 'placeName' field ==="
rg -n --type=java 'String placeName[,;)]' src/main/java/com/campus/campus/domain/councilpost/application/dto/response/
echo -e "\n=== List all response DTO files ==="
fd --type f --extension java 'Response\.java$' src/main/java/com/campus/campus/domain/councilpost/application/dto/response/Repository: our-campUS/our-campUS-BE
Length of output: 1927
응답 DTO의 장소명 필드 일관성 문제를 해결해주세요.
응답 DTO들에서 동일한 개념의 필드가 서로 다른 이름으로 정의되어 있습니다:
place사용: GetPostListForCouncilResponse, GetUpcomingEventListForCouncilResponse, GetPostForUserResponse, GetLikedPostResponse, GetActivePartnershipListForUserResponse (5개)placeName사용: PostListItemResponse, GetPostResponse (2개)
API 일관성을 위해 모든 응답 DTO가 동일한 필드명을 사용하도록 통일해야 합니다.
🤖 Prompt for AI Agents
In
@src/main/java/com/campus/campus/domain/councilpost/application/dto/response/PostListItemResponse.java
at line 12, PostListItemResponse currently defines the field placeName which is
inconsistent with other response DTOs that use place; change the DTO to use the
same field name "place" (update the field name in PostListItemResponse, its
constructor/builder/getter/setter, any Jackson/serialization annotations, and
all usages/mappers that populate PostListItemResponse) and likewise update
GetPostResponse if it uses placeName—ensure all mapping code (e.g., methods that
convert entities to PostListItemResponse or GetPostResponse) assigns to the
unified "place" property so all response DTOs share the same field name.
...java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java
Show resolved
Hide resolved
...ava/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java
Outdated
Show resolved
Hide resolved
| public AddressResponse getAddress(double lat, double lng) { | ||
| return webClient.get() | ||
| .uri(uriBuilder -> uriBuilder | ||
| .path("/req/address") | ||
| .queryParam("service", "address") | ||
| .queryParam("request", "getAddress") | ||
| .queryParam("key", apiKey) | ||
| .queryParam("point", lng + "," + lat) | ||
| .queryParam("crs", "epsg:4326") | ||
| .queryParam("type", "both") | ||
| .queryParam("format", "json") | ||
| .build()) | ||
| .retrieve() | ||
| .onStatus( | ||
| HttpStatusCode::isError, | ||
| res -> Mono.error(new GeoCoderException()) | ||
| ) | ||
| .bodyToMono(AddressResponse.class).block(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
외부 API 호출에 타임아웃 설정 필요
외부 API 호출 시 타임아웃이 설정되어 있지 않아 무한 대기 상태가 발생할 수 있습니다. .block()을 사용할 때 타임아웃을 지정하거나 WebClient에 타임아웃을 설정해야 합니다.
🔧 타임아웃 추가 예시
public AddressResponse getAddress(double lat, double lng) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/req/address")
.queryParam("service", "address")
.queryParam("request", "getAddress")
.queryParam("key", apiKey)
.queryParam("point", lng + "," + lat)
.queryParam("crs", "epsg:4326")
.queryParam("type", "both")
.queryParam("format", "json")
.build())
.retrieve()
.onStatus(
HttpStatusCode::isError,
res -> Mono.error(new GeoCoderException())
)
- .bodyToMono(AddressResponse.class).block();
+ .bodyToMono(AddressResponse.class)
+ .timeout(java.time.Duration.ofSeconds(5))
+ .block();
}📝 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.
| public AddressResponse getAddress(double lat, double lng) { | |
| return webClient.get() | |
| .uri(uriBuilder -> uriBuilder | |
| .path("/req/address") | |
| .queryParam("service", "address") | |
| .queryParam("request", "getAddress") | |
| .queryParam("key", apiKey) | |
| .queryParam("point", lng + "," + lat) | |
| .queryParam("crs", "epsg:4326") | |
| .queryParam("type", "both") | |
| .queryParam("format", "json") | |
| .build()) | |
| .retrieve() | |
| .onStatus( | |
| HttpStatusCode::isError, | |
| res -> Mono.error(new GeoCoderException()) | |
| ) | |
| .bodyToMono(AddressResponse.class).block(); | |
| } | |
| public AddressResponse getAddress(double lat, double lng) { | |
| return webClient.get() | |
| .uri(uriBuilder -> uriBuilder | |
| .path("/req/address") | |
| .queryParam("service", "address") | |
| .queryParam("request", "getAddress") | |
| .queryParam("key", apiKey) | |
| .queryParam("point", lng + "," + lat) | |
| .queryParam("crs", "epsg:4326") | |
| .queryParam("type", "both") | |
| .queryParam("format", "json") | |
| .build()) | |
| .retrieve() | |
| .onStatus( | |
| HttpStatusCode::isError, | |
| res -> Mono.error(new GeoCoderException()) | |
| ) | |
| .bodyToMono(AddressResponse.class) | |
| .timeout(java.time.Duration.ofSeconds(5)) | |
| .block(); | |
| } |
🤖 Prompt for AI Agents
In
@src/main/java/com/campus/campus/domain/place/infrastructure/geocoder/GeoCoderClient.java
around lines 31 - 49, The getAddress method uses webClient and calls .block()
without any timeout, risking indefinite hangs; add a timeout to the reactive
pipeline (e.g., use .timeout(Duration.ofSeconds(...)) before .block() or call
.block(Duration.ofSeconds(...))) or configure the underlying
WebClient/HttpClient with connect/read timeouts, and ensure timeouts are mapped
to GeoCoderException (or a specific timeout exception) so callers of getAddress
receive a deterministic error; update getAddress, the reactive chain on
webClient, and GeoCoderException handling accordingly.
| "/managers/login", | ||
| "/places/search", | ||
| "/storage/presigned" | ||
| "/storage/presigned", |
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 and examine the PermitUrlConfig.java file
find . -name "PermitUrlConfig.java" -type fRepository: our-campUS/our-campUS-BE
Length of output: 136
🏁 Script executed:
# Search for presigned endpoints and related security configurations
rg -nP --type=java -C5 '@PostMapping.*presigned'Repository: our-campUS/our-campUS-BE
Length of output: 4093
🏁 Script executed:
# Check for rate limiting implementations
rg -nP --type=java -C3 'RateLimiter|@RateLimit|Bucket4j'Repository: our-campUS/our-campUS-BE
Length of output: 50
🏁 Script executed:
# Check authentication and security filter configurations
rg -nP --type=java -C3 'SecurityFilterChain|permitAll|authenticated'Repository: our-campUS/our-campUS-BE
Length of output: 9439
🏁 Script executed:
cat -n src/main/java/com/campus/campus/global/config/PermitUrlConfig.javaRepository: our-campUS/our-campUS-BE
Length of output: 1390
🏁 Script executed:
# Check if there are multiple presigned endpoints being marked as public
rg -n "presigned" src/main/java/com/campus/campus/global/config/PermitUrlConfig.java -C3Repository: our-campUS/our-campUS-BE
Length of output: 249
"/storage/presigned" 엔드포인트의 인증 없는 공개 접근으로 인한 보안 위험을 완화하세요.
확인 결과:
- "/storage/presigned"은 실제로 public으로 공개되어 있으며, 인증 없이 누구나 "uploads" 경로에 대한 업로드 URL을 생성할 수 있습니다.
- 현재 이 엔드포인트에는 rate limiting이나 다른 보호 조치가 없습니다.
- 다른 presigned 엔드포인트(/storage/posts/images/presigned, /storage/notices/images/presigned)는 인증이 필요하므로 올바르게 보호되어 있습니다.
"/storage/presigned"의 무제한 공개 접근은 다음 위험을 초래합니다:
- 무제한 업로드 URL 생성으로 인한 스토리지 남용
- 악의적인 파일 업로드
- 예상치 못한 스토리지 비용 증가
다음을 검토하세요:
- 이 엔드포인트가 정말 public이어야 하는지, 아니면 인증 필수로 변경해야 하는지
- public이 필요하다면 rate limiting, IP 화이트리스트 또는 토큰 기반 제한 추가
🤖 Prompt for AI Agents
In @src/main/java/com/campus/campus/global/config/PermitUrlConfig.java at line
33, 현재 PermitUrlConfig에 있는 "/storage/presigned" 항목을 공개 permitAll 목록에서 제거하거나 보호
로직을 추가해야 합니다: 찾아 수정할 대상은 PermitUrlConfig 클래스에 정의된 permit URL 목록(현재에
"/storage/presigned" 문자열)이며, 해결 방안은 두 가지 중 하나로 구현합니다 — 1) 해당 엔드포인트가 인증 필요하다면
목록에서 "/storage/presigned"를 제거하고 HttpSecurity (또는 configure 메서드)에서 인증 필수로 변경하여 인증
필터(예: JWT/Session)로 보호하거나, 2) 진짜로 public이어야 한다면 permitAll을 유지하되 요청당 생성 제한을 위해
rate-limiting 필터(예: Bucket4j 또는 Spring RateLimiter) 또는 IP 화이트리스트/토큰 검증 필터를 해당
엔드포인트 처리 흐름에 추가하여 무제한 presigned URL 생성을 방지하십시오.
| geocoder: | ||
| api-key: ${GEOCODER_API_KEY} |
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
# Verify geocoding service provider and GEOCODER_API_KEY usage
echo "=== Searching for geocoding client implementation ==="
rg -n -C 5 "class.*GeoCoder|GeoCoderClient" --type java
echo -e "\n=== Checking for GEOCODER_API_KEY documentation ==="
rg -n "GEOCODER_API_KEY" --type md --type txt
echo -e "\n=== Checking all environment configuration files ==="
fd "application.*\.yml" src/main/resources/
echo -e "\n=== Checking geocoder config in all environment files ==="
fd "application.*\.yml" src/main/resources/ --exec grep -l "geocoder" {}
echo -e "\n=== Content of all application-*.yml files ==="
fd "application.*\.yml" src/main/resources/ --exec sh -c 'echo "File: {}"; cat {}' \;Repository: our-campUS/our-campUS-BE
Length of output: 20650
🏁 Script executed:
#!/bin/bash
# Search for Geocoder bean or configuration class definition
echo "=== Searching for Geocoder bean configuration ==="
rg -n "Geocoder|@Bean.*geocoder" --type java -i
echo -e "\n=== Searching for map configuration or properties ==="
rg -n "map\\.geocoder" --type java
echo -e "\n=== Checking for any documentation files ==="
fd "README|SETUP|DEPLOYMENT|INSTALL" -E node_modulesRepository: our-campUS/our-campUS-BE
Length of output: 3704
🏁 Script executed:
# Check if there's any setup guide or deployment documentation
find . -type f \( -name "*.md" -o -name "*.txt" -o -name "SETUP*" -o -name "DEPLOY*" -o -name "*.adoc" \) | head -20Repository: our-campUS/our-campUS-BE
Length of output: 197
🏁 Script executed:
# Verify the complete GeoCoderClient implementation to understand the full context
cat -n src/main/java/com/campus/campus/domain/place/infrastructure/geocoder/GeoCoderClient.javaRepository: our-campUS/our-campUS-BE
Length of output: 1953
개발(dev) 및 운영(prod) 환경에 지오코더 설정 추가 필요 - 배포 차단 이슈
현재 로컬 환경(application-local.yml)에만 지오코더 설정이 있고, 개발 및 운영 환경 설정 파일에는 map.geocoder.api-key 설정이 완전히 누락되어 있습니다. GeoCoderClient는 @component로 등록되며 이 설정값을 필수로 주입받으므로, dev/prod 배포 시 빈 생성 실패 또는 런타임 오류가 발생합니다.
필수 작업:
application-dev.yml과application-prod.yml에 다음 설정 추가:map: geocoder: api-key: ${GEOCODER_API_KEY} # VWorld API
GEOCODER_API_KEY환경 변수를 배포 및 설정 가이드 문서에 명시 (VWorld 지오코딩 서비스용)
🤖 Prompt for AI Agents
In @src/main/resources/application-local.yml around lines 65 - 66,
application-dev.yml and application-prod.yml are missing the required
map.geocoder.api-key setting used by the GeoCoderClient component; add the
map.geocoder.api-key: ${GEOCODER_API_KEY} (under map.geocoder) to both
application-dev.yml and application-prod.yml so the GeoCoderClient can receive
its required property, and update deployment/setup docs to ensure the
GEOCODER_API_KEY environment variable (VWorld API key) is provisioned in dev and
prod.
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.
수정 완료
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: 2
🤖 Fix all issues with AI agents
In
@src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java:
- Around line 63-74: The search method concatenates nowAddress (from
toStringAddress(geocoderRes)) with keyword without null-safety, so if
toStringAddress returns null you end up with "null"+keyword; update
PlaceService.search to guard against null nowAddress returned by
toStringAddress(…) (or null geocoderRes) before building searchWord: check for
null/blank and replace with empty string or a sensible default, trim and/or
insert a separator when both parts exist, and use the safe value when calling
naverMapClient.searchPlaces; refer to the search method, the nowAddress variable
and toStringAddress(geocoderRes) when making the change.
- Around line 108-121: The current findOrCreatePlace method in PlaceService
redundantly checks existing.isPresent() then uses existing.orElseGet(); remove
the explicit if-return and instead return existing.orElseGet(() ->
placeRepository.save(placeMapper.createPlace(place))) directly after retrieving
Optional<Place> existing from placeRepository.findByPlaceKey(placeKey), keeping
the place and placeKey variables and the placeMapper.createPlace(...) call
intact.
🧹 Nitpick comments (1)
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
145-148: 사용되지 않는 변수를 제거하세요.Line 147의
placeName변수는 선언되었지만 Line 148의placeMapper.createPlace(placeInfo)호출에서 사용되지 않습니다.SavedPlaceInfo객체가 이미 장소명을 포함하고 있다면 이 변수는 불필요합니다.♻️ 미사용 변수 제거
place = placeRepository.findByPlaceKey(placeKey) .orElseGet(() -> { //없으면 생성 - String placeName = stripHtml(placeInfo.placeName()); Place newPlace = placeRepository.save(placeMapper.createPlace(placeInfo)); //신규 생성된 경우에만 이미지 저장 migrateImagesToOci(newPlace.getPlaceKey(), placeInfo.imgUrls()); return newPlace; });
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/main/java/com/campus/campus/domain/councilpost/domain/repository/PostImageRepository.javasrc/main/java/com/campus/campus/domain/place/application/service/PlaceService.java
🔇 Additional comments (2)
src/main/java/com/campus/campus/domain/councilpost/domain/repository/PostImageRepository.java (1)
20-26: LGTM! 성능 최적화가 잘 적용되었습니다.이미지 URL만 필요한 경우 전체
PostImage엔티티를 조회하는 대신 URL만 선택하여 반환하므로 불필요한 데이터 전송을 줄일 수 있습니다.src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
244-250: 헬퍼 메서드가 올바르게 구현되었습니다.도로명 주소 또는 지번 주소를 우선적으로 반환하는 로직이 적절합니다. 다만 이 메서드가
null을 반환할 수 있으므로 호출하는 쪽에서 null 체크가 필요합니다(Lines 63-74 참조).
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.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: 6
🤖 Fix all issues with AI agents
In
@src/main/java/com/campus/campus/domain/councilpost/application/dto/request/PostRequest.java:
- Around line 26-44: The place nested object lacks deep validation: add @Valid
to the PostRequest.place field to trigger validation of nested SavedPlaceInfo,
and update the SavedPlaceInfo record to annotate required fields (e.g.,
@NotBlank on placeName and placeKey, @NotNull on coordinate, and validations on
coordinate.latitude/longitude as needed) so PlaceService.findOrCreatePlace() can
safely call place.placeKey(); ensure the controller/validation pipeline will
enforce these constraints.
In
@src/main/java/com/campus/campus/domain/partnership/application/service/PartnershipService.java:
- Around line 133-151: getPartnershipDetail currently calls post.getPlace()
without null-check causing NPE when a post has no place; add a null check after
StudentCouncilPost post = ... to handle a missing place (e.g., if
(post.getPlace() == null) throw new PlaceNotFoundException(); or return an
appropriate error response), and ensure subsequent uses of the Place variable
(place, isLiked(place, user), placeMapper.toPartnershipResponse(...)) only run
when place is non-null; update imports/exception type as needed.
- Around line 103-131: In findPartnerInBounds (PartnershipService) add
null-safety checks for user.getMajor(), user.getCollege(), and user.getSchool()
before accessing their IDs (mirror the null checks used in
getPartnershipPlaces): validate each is non-null and throw the same domain
exception or return an appropriate error if any are null, then proceed to
extract majorId/collegeId/schoolId and call
studentCouncilPostRepository.findPinsInBounds as before.
- Around line 45-101: getPartnershipPlaces may NPE when
user.getMajor()/getCollege()/getSchool() or post.getPlace() are null; add
null-safety: after loading User in getPartnershipPlaces verify user.getMajor(),
user.getCollege(), user.getSchool() and either throw a domain-specific exception
(e.g., MissingUserAffiliationException) or handle missing IDs by passing null/0
to the repository call as intended, and when mapping posts ensure
post.getPlace() is non-null (skip posts with null place or return a safe default
response) before calling place.getCoordinate(), isLiked(post.getPlace(), user)
and getImgUrls(post); update logic that builds the stream (the mapping that
creates AbstractMap.SimpleEntry and subsequent
placeMapper.toPartnershipResponse) to guard against null place and only include
valid entries.
In
@src/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.java:
- Around line 18-19: Fix the punctuation in the GEOCODER_ERROR enum constant: in
ErrorCode (GEOCODER_ERROR) remove the extra period so the message ends with a
single "." to match other messages (change "...오류가 발생하였습니다.." to "...오류가
발생하였습니다.").
In
@src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java:
- Around line 115-124: The findOrCreatePlace method has a duplicated nested
orElseGet call on the Optional 'existing'; replace the nested call with a single
fallback that creates and saves a new Place. Change the return to use
existing.orElseGet(() -> placeRepository.save(placeMapper.createPlace(place)))
so the code checks placeRepository.findByPlaceKey(placeKey) and, if absent,
saves and returns the created entity via placeMapper.createPlace(...) and
placeRepository.save(...).
🧹 Nitpick comments (1)
src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java (1)
215-218: 조건문 로직을 명확하게 개선할 수 있습니다.현재 코드는 short-circuit 평가로 인해 NPE는 발생하지 않지만, 가독성이 떨어집니다. 조건을 분리하여 의도를 명확히 하는 것이 좋습니다.
♻️ 가독성 개선 제안
Place place = post.getPlace(); - if (dto.place() != null && (place == null || !dto.place().placeName().equals(place.getPlaceName()))) { + if (dto.place() != null) { + boolean shouldUpdate = place == null || !dto.place().placeName().equals(place.getPlaceName()); + if (shouldUpdate) { - place = placeService.findOrCreatePlace(dto); + place = placeService.findOrCreatePlace(dto); + } }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
src/main/java/com/campus/campus/domain/councilpost/application/dto/request/PostRequest.javasrc/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.javasrc/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.javasrc/main/java/com/campus/campus/domain/partnership/application/service/PartnershipService.javasrc/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.javasrc/main/java/com/campus/campus/domain/place/application/service/PlaceService.javasrc/main/java/com/campus/campus/global/util/geocoder/GeoUtil.java
🧰 Additional context used
🧬 Code graph analysis (3)
src/main/java/com/campus/campus/global/util/geocoder/GeoUtil.java (1)
src/main/java/com/campus/campus/domain/place/domain/entity/Coordinate.java (1)
Embeddable(5-16)
src/main/java/com/campus/campus/domain/councilpost/application/dto/request/PostRequest.java (1)
src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java (1)
SavedPlaceInfo(9-36)
src/main/java/com/campus/campus/domain/partnership/application/service/PartnershipService.java (5)
src/main/java/com/campus/campus/domain/councilpost/application/exception/PostNotFoundException.java (1)
PostNotFoundException(5-9)src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)src/main/java/com/campus/campus/global/util/geocoder/GeoUtil.java (1)
GeoUtil(3-32)src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.java (2)
Service(43-277)findActivePartnershipForUser(224-260)src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java (1)
toGetActivePartnershipListForUserResponse(65-72)
⏰ 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/main/java/com/campus/campus/global/util/geocoder/GeoUtil.java (1)
3-32: LGTM! 하버사인(Haversine) 공식 구현이 정확합니다.지리적 거리 계산을 위한 유틸리티 클래스가 올바르게 구현되었습니다. 공식이 수학적으로 정확하며, 코드가 명확하고 간결합니다.
src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java (1)
209-276: LGTM! 쿼리 구현이 적절합니다.커서 기반 페이지네이션과 지리적 범위 필터링이 올바르게 구현되었습니다. EntityGraph를 통해
place를 즉시 로딩하여 N+1 문제를 방지한 것도 좋습니다.src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
63-113: LGTM! 지오코딩 통합 및 에러 처리가 적절합니다.주소 기반 검색으로 개선되었으며, 지오코딩 실패 시 키워드만으로 검색하는 폴백 로직이 잘 구현되었습니다.
src/main/java/com/campus/campus/domain/councilpost/application/dto/request/PostRequest.java
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/partnership/application/service/PartnershipService.java
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/partnership/application/service/PartnershipService.java
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java
Show resolved
Hide resolved
… 검증, 게시글 장소 정보 Null 검증 추가
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 (2)
src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java (1)
8-39:coordinate에@NotBlank는 런타임 검증 예외를 유발할 수 있습니다.
@NotBlank는 문자열 계열에만 적용되는데, 현재Coordinate coordinate에 붙어 있어 검증 시UnexpectedTypeException이 날 가능성이 큽니다.@NotNull(+ 필요하면@Valid)로 바꾸는 게 안전합니다.제안 수정(diff)
import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.Valid; public record SavedPlaceInfo( @@ @Schema(description = "위도/경도") - @NotBlank + @NotNull + @Valid Coordinate coordinate,src/main/java/com/campus/campus/domain/councilpost/application/dto/request/PostRequest.java (1)
8-46:@Schema(example=...)가 JSON 형태가 아니라 Swagger 표시가 깨질 수 있습니다.현재 예시 문자열이 객체
{ ... }로 감싸져 있지 않아 “place 필드” 예시로 렌더링이 애매할 수 있습니다. 예시를 유효한 JSON으로 바꾸는 쪽을 추천합니다.제안 수정(diff)
@Schema(example = """ - "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+%EC%A0%9C1%EC%BA%A0%ED%8D%BC%EC%8A%A4?c=37.545947,126.964578,15,0,0,0,dh", - "telephone": "010-1234-1234", - "coordinate": { - "latitude": 0.1, - "longitude": 0.1 - }, - "imgUrls": [ - "string" - ] + { + "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+%EC%A0%9C1%EC%BA%A0%ED%8D%BC%EC%8A%A4?c=37.545947,126.964578,15,0,0,0,dh", + "telephone": "010-1234-1234", + "coordinate": { "latitude": 0.1, "longitude": 0.1 }, + "imgUrls": ["string"] + } """, description = "/search API에서 반환된 결과 중 하나를 선택")
🤖 Fix all issues with AI agents
In
@src/main/java/com/campus/campus/domain/councilpost/application/exception/ErrorCode.java:
- Around line 24-34: PLACE_INFO_NOT_FOUND in the ErrorCode enum uses
HttpStatus.BAD_REQUEST but the semantics described ("resource absent") suggest
it should use HttpStatus.NOT_FOUND; update the ErrorCode enum entry
PLACE_INFO_NOT_FOUND to the correct HttpStatus (NOT_FOUND if the place record is
a missing resource, or keep BAD_REQUEST only if the error truly means
invalid/incomplete request data), and ensure any related tests, API contract
docs, and usages that check ErrorCode/place responses are updated to expect the
chosen HTTP status.
🧹 Nitpick comments (3)
src/main/java/com/campus/campus/domain/councilpost/application/dto/request/PostRequest.java (1)
8-15: (선택) 요청 DTO가response패키지 타입을 직접 참조하는 구조는 분리해두면 더 편합니다.
PostRequest가com.campus.campus.domain.place.application.dto.response.SavedPlaceInfo를 요청 바디로 쓰고 있어 레이어링 관점에서 결합도가 올라갑니다. 시간 되면...dto.request로 동일 스펙 record를 분리(또는 공용 dto 패키지로 이동)하는 방향을 추천합니다.Also applies to: 44-46
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (2)
115-123: 동시성 예외 처리 추가를 권장합니다.
likePlace메서드(146-160줄)에서는DataIntegrityViolationException처리를 통해 동시 생성 시나리오를 처리하고 있습니다.findOrCreatePlace메서드에도 동일한 패턴을 적용하면 동일한placeKey로 동시에 게시글을 생성할 때 발생할 수 있는 예외를 안전하게 처리할 수 있습니다.♻️ 제안하는 수정 사항
@Transactional public Place findOrCreatePlace(PostRequest request) { SavedPlaceInfo place = request.place(); String placeKey = place.placeKey(); - //이미 Place 존재하는지 확인 후 없으면 객체 생성 후 저장 - return placeRepository.findByPlaceKey(placeKey) - .orElseGet(() -> placeRepository.save(placeMapper.createPlace(place))); + try { + //이미 Place 존재하는지 확인 후 없으면 객체 생성 후 저장 + return placeRepository.findByPlaceKey(placeKey) + .orElseGet(() -> placeRepository.save(placeMapper.createPlace(place))); + } catch (DataIntegrityViolationException e) { + //동시 생성으로 unique 제약 위반 시 다시 조회 + return placeRepository.findByPlaceKey(placeKey) + .orElseThrow(PlaceCreationException::new); + } }
149-150: 사용되지 않는 변수를 제거하세요.Line 149에서
placeName변수를 추출하고 있지만, Line 150에서placeMapper.createPlace호출 시 더 이상 사용하지 않습니다.PlaceMapper의 시그니처 변경에 따라 이 변수는 불필요해졌습니다.♻️ 제안하는 수정 사항
.orElseGet(() -> { //없으면 생성 - String placeName = stripHtml(placeInfo.placeName()); Place newPlace = placeRepository.save(placeMapper.createPlace(placeInfo)); //신규 생성된 경우에만 이미지 저장 migrateImagesToOci(newPlace.getPlaceKey(), placeInfo.imgUrls()); return newPlace; });
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
src/main/java/com/campus/campus/domain/councilpost/application/dto/request/PostRequest.javasrc/main/java/com/campus/campus/domain/councilpost/application/exception/AcademicInfoNotSetException.javasrc/main/java/com/campus/campus/domain/councilpost/application/exception/ErrorCode.javasrc/main/java/com/campus/campus/domain/councilpost/application/exception/PlaceInfoNotFoundException.javasrc/main/java/com/campus/campus/domain/partnership/application/service/PartnershipService.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.javasrc/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.javasrc/main/java/com/campus/campus/domain/place/application/service/PlaceService.java
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.java
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/com/campus/campus/domain/councilpost/application/exception/ErrorCode.java (2)
src/main/java/com/campus/campus/domain/council/application/exception/ErrorCode.java (1)
Getter(10-28)src/main/java/com/campus/campus/global/common/exception/ErrorCodeInterface.java (1)
ErrorCodeInterface(5-11)
src/main/java/com/campus/campus/domain/partnership/application/service/PartnershipService.java (5)
src/main/java/com/campus/campus/domain/councilpost/application/exception/AcademicInfoNotSetException.java (1)
AcademicInfoNotSetException(5-9)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/PostNotFoundException.java (1)
PostNotFoundException(5-9)src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)src/main/java/com/campus/campus/global/util/geocoder/GeoUtil.java (1)
GeoUtil(3-32)
🔇 Additional comments (7)
src/main/java/com/campus/campus/domain/councilpost/application/exception/AcademicInfoNotSetException.java (1)
5-9: 예외 패턴 일관성 좋습니다.
ApplicationException에ErrorCode.ACADEMIC_INFO_NOT_SET를 위임하는 형태로 다른 예외들과 동일한 스타일이라 유지보수에 유리합니다.src/main/java/com/campus/campus/domain/councilpost/application/exception/PlaceInfoNotFoundException.java (1)
5-9: 예외 클래스 구성 깔끔합니다.
ErrorCode.PLACE_INFO_NOT_FOUND로만 책임을 위임해서 재사용/표준화 관점에서 좋습니다.src/main/java/com/campus/campus/domain/partnership/application/service/PartnershipService.java (3)
106-130: 지도 경계 내 제휴 장소 조회 로직이 적절합니다.경계 기반 조회는 거리 정렬이 필요 없는 지도 핀 표시에 적합하며, null 체크와 필터링이 올바르게 구현되어 있습니다.
132-154: 상세 조회 로직이 올바르게 구현되어 있습니다.장소 및 좌표에 대한 null 체크, 거리 계산, 응답 매핑이 적절히 처리되고 있습니다.
156-168: 헬퍼 메서드들이 적절하게 구현되어 있습니다.학적 정보 검증, 좋아요 상태 확인, 이미지 URL 조회 로직이 명확하고 재사용 가능하게 구성되어 있습니다.
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (2)
63-113: 지오코딩 기반 검색 통합이 적절하게 구현되어 있습니다.좌표를 주소로 변환하여 검색어를 보강하는 로직이 잘 구현되어 있으며, 지오코딩 실패 시 원래 키워드로 폴백하는 예외 처리가 적절합니다.
246-252: 주소 추출 헬퍼 메서드가 적절하게 구현되어 있습니다.도로명 또는 지번 주소를 우선적으로 추출하는 로직이 명확하며, null 처리가 안전하게 구현되어 있습니다.
src/main/java/com/campus/campus/domain/councilpost/application/exception/ErrorCode.java
Show resolved
Hide resolved
| @Transactional | ||
| public List<PartnershipResponse> getPartnershipPlaces(Long userId, Long cursor, int size, double userLat, | ||
| double userLng) { | ||
| User user = userRepository.findById(userId) | ||
| .orElseThrow(UserNotFoundException::new); | ||
|
|
||
| validateAcademicInfo(user); | ||
|
|
||
| Long majorId = user.getMajor().getMajorId(); | ||
| Long collegeId = user.getCollege().getCollegeId(); | ||
| Long schoolId = user.getSchool().getSchoolId(); | ||
|
|
||
| Pageable pageable = PageRequest.of(0, size); | ||
|
|
||
| //유저가 속한 학생회들의 제휴글(major/college/school) 전부 조회 | ||
| List<StudentCouncilPost> posts = studentCouncilPostRepository.findByUserScopeWithCursor( | ||
| majorId, | ||
| collegeId, | ||
| schoolId, | ||
| PostCategory.PARTNERSHIP, | ||
| CouncilType.MAJOR_COUNCIL, | ||
| CouncilType.COLLEGE_COUNCIL, | ||
| CouncilType.SCHOOL_COUNCIL, | ||
| cursor, | ||
| LocalDateTime.now(), | ||
| pageable | ||
| ); | ||
|
|
||
| return posts.stream() | ||
| .filter(post -> post.getPlace() != null && post.getPlace().getCoordinate() != null) | ||
| .map(post -> { | ||
| Place place = post.getPlace(); | ||
|
|
||
| double distanceMeter = GeoUtil.distanceMeter( | ||
| userLat, userLng, | ||
| place.getCoordinate().latitude(), | ||
| place.getCoordinate().longitude() | ||
| ); | ||
|
|
||
| // post + distance를 함께 묶음 | ||
| return new AbstractMap.SimpleEntry<>(post, distanceMeter); | ||
| }) | ||
| .sorted(Map.Entry.comparingByValue()) // 거리순 정렬 | ||
| .limit(size) | ||
| .map(entry -> { | ||
| StudentCouncilPost post = entry.getKey(); | ||
| double distanceMeter = entry.getValue(); | ||
| double rounded = Math.round(distanceMeter * 100.0) / 100.0; | ||
|
|
||
| return placeMapper.toPartnershipResponse( | ||
| user, | ||
| post, | ||
| post.getPlace(), | ||
| isLiked(post.getPlace(), user), | ||
| getImgUrls(post), | ||
| rounded | ||
| ); | ||
| }) | ||
| .toList(); | ||
| } |
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.
페이지네이션 로직의 일관성 문제를 해결해야 합니다.
현재 구현에서는 DB 쿼리 단계에서 cursor와 pageable을 사용하여 데이터를 가져온 후(60-71줄), 메모리 내에서 거리 기반 정렬을 수행하고 다시 제한하고 있습니다(73-88줄). 이로 인해 다음과 같은 문제가 발생합니다:
- DB에서 cursor 기반으로 [A, B, C, D, E]를 가져옴
- 거리순 정렬 후 [C, A, E, B, D]로 재배치됨
- size만큼 제한하여 [C, A, E]를 반환
- 다음 페이지 요청 시 cursor는 DB 순서 기준으로 E 이후부터 가져오지만, 사용자는 거리순 정렬 기준으로 E 다음을 기대함
- 결과적으로 중복 조회 또는 누락이 발생할 수 있음
권장 해결 방안:
- DB 쿼리에서 거리 계산 및 정렬을 수행하거나 (PostGIS 등 활용)
- 커서 기반 페이지네이션을 offset 기반으로 변경하거나
- 거리 정렬이 필요한 경우 전체 데이터를 조회 후 클라이언트에서 페이지네이션 처리
🔀 변경 내용
📸 스크린샷 (선택)
📎 참고 이슈
관련 이슈 번호 #31
Summary by CodeRabbit
New Features
Refactor
Chores
✏️ Tip: You can customize this high-level summary in your review settings.