-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/#9 장소 검색 후 해당 장소 관련 정보 조회 기능 구현 #26
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장소 검색 및 좋아요 기능을 추가합니다. Naver/Google 클라이언트, Place/PlaceImages/LikedPlace 엔터티·리포지토리, placeKey 생성 유틸, OCI 이미지 업로드 흐름, RestClient/WebClient 설정, 공개 URL("/places/search") 허용이 포함됩니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Controller as PlaceController
participant Service as PlaceService
participant Naver as NaverMapClient
participant Google as GooglePlaceClient
participant RepoP as PlaceRepository
participant RepoPI as PlaceImagesRepository
participant Presigned as PresignedUrlService
participant OCI as OCI Storage
Note over User,Controller: 장소 검색
User->>Controller: GET /places/search?keyword
Controller->>Service: search(keyword)
Service->>Naver: searchPlaces(keyword, 5)
Naver-->>Service: NaverSearchResponse
Service->>Google: fetchImages(name,address,limit)
Google-->>Service: List<ImageUrls>
Service-->>Controller: List<SavedPlaceInfo>
Controller-->>User: 200 PLACE_SEARCH_SUCCESS
Note over User,Controller: 좋아요 토글 (토글/이미지 마이그레이션 포함)
User->>Controller: POST /places/like-place (SavedPlaceInfo)
Controller->>Service: likePlace(savedPlaceInfo, userId)
Service->>RepoP: findByPlaceKey(placeKey)
alt place 없음
Service->>RepoP: save(new Place)
loop each imageUrl
Service->>Google: downloadImage(url)
Google-->>Service: byte[] imageBytes
Service->>Presigned: getPresignedUrl(...)
Presigned-->>Service: presignedUrl
Service->>OCI: PUT presignedUrl (upload image)
Service->>RepoPI: save(PlaceImages)
end
end
Service->>RepoPI: findByPlaceKey(placeKey)
Service->>RepoP: save/remove LikedPlace (toggle)
Service-->>Controller: LikeResponse
Controller-->>User: 200 PLACE_SAVE_SUCCESS
🎯 Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🔗 Possibly related PRs
👥 Suggested reviewers
🐰 Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
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: 16
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (7)
src/main/java/com/campus/campus/domain/place/application/service/PlaceImageCache.java-14-16 (1)
14-16: 반환된 리스트의 가변성 문제
get()메서드가 내부 리스트를 직접 반환하므로, 호출자가 반환된 리스트를 수정하면 캐시의 상태가 의도치 않게 변경될 수 있습니다.불변 리스트를 반환하거나 방어적 복사본을 반환하는 것을 권장합니다.
🔎 불변 리스트 반환 예시
+import java.util.Collections; + public List<String> get(String placeKey) { - return store.get(placeKey); + List<String> cached = store.get(placeKey); + return cached != null ? Collections.unmodifiableList(cached) : null; }src/main/java/com/campus/campus/domain/place/application/dto/response/google/GooglePlaceDetailResponse.java-5-12 (1)
5-12:photos필드에@Nullable애너테이션 추가현재
photos필드는 Google Places API가 사진이 없는 장소에 대해 null을 반환할 수 있음에도 불구하고 null 안전성 애너테이션이 없습니다.GooglePlaceClientImpl의 라인 105에서 null 체크가 수행되고 있으나, 필드 선언에@Nullable애너테이션을 추가하여 계약(contract)을 명시적으로 문서화하세요.public record GooglePlaceDetailResponse( Result result ) { public record Result( @Nullable List<GooglePhoto> photos ) { } }src/main/java/com/campus/campus/domain/place/infrastructure/google/GoogleMapConfig.java-8-9 (1)
8-9: GoogleMapConfig 클래스는 구현하지 않으거나 제거하세요. 또한 프로퍼티 경로 불일치 문제를 수정하세요.GoogleMapConfig는 @configuration만 있고 실제 기능이 없는 빈 클래스입니다. 주석 처리된 코드는 제거해야 하며, 현재 구조에서는 불필요합니다.
실제 Google Places API 키 설정은 GooglePlaceClientImpl에서 생성자 주입으로 처리되고 있습니다. 그러나 프로퍼티 경로 불일치 문제가 있습니다:
application-local.yml:map.google.places.api-key로 정의됨GooglePlaceClientImpl(line 27):${google.places.api-key}를 기대함이 불일치로 인해 런타임에 설정값을 찾을 수 없게 됩니다.
다음 중 하나를 수행하세요:
- GoogleMapConfig 클래스 전체 삭제 (NaverMapConfig와 다른 패턴이고, GooglePlaceClientImpl이 이미 설정 처리 중)
- 또는 GoogleMapConfig를 활용하려면 빈 정의 추가 및 GooglePlaceClientImpl 수정 필요
- application-local.yml의 프로퍼티 경로를
google.places.api-key로 수정하거나, GooglePlaceClientImpl이map.google.places.api-key를 사용하도록 수정src/main/java/com/campus/campus/domain/place/application/service/PlaceImagesService.java-74-81 (1)
74-81: 하드코딩된 Content-Type
"image/jpeg"가 하드코딩되어 있지만, Google에서 가져오는 이미지가 PNG나 WebP일 수도 있습니다. 이미지 포맷을 동적으로 감지하거나, 이미지 다운로드 시 응답 헤더에서 Content-Type을 확인하는 것을 권장합니다.src/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.java-13-13 (1)
13-13: 잘못된 HTTP 상태 코드 사용
ADDRESS_EMPTY에HttpStatus.NO_CONTENT(204)를 사용하고 있습니다. 204는 성공적인 요청이지만 반환할 콘텐츠가 없음을 의미하며, 에러 상황에는 적합하지 않습니다. 유효성 검증 실패에는HttpStatus.BAD_REQUEST(400)를 사용하세요.🔎 수정 제안
-ADDRESS_EMPTY(2600, HttpStatus.NO_CONTENT, "주소는 비어있을 수 없습니다."), +ADDRESS_EMPTY(2600, HttpStatus.BAD_REQUEST, "주소는 비어있을 수 없습니다."),src/main/java/com/campus/campus/domain/place/application/service/MapService.java-42-53 (1)
42-53: 사용되지 않는 변수 및 중복 메서드 호출Line 43의
base변수가 선언만 되고 사용되지 않습니다. Line 52에서mapper.toSavedPlaceInfo(item, placeKey)가 다시 호출되어 불필요한 중복이 발생합니다.🔎 수정 제안
- //네이버 -> 기본 정보 - SavedPlaceInfo base = mapper.toSavedPlaceInfo(item, placeKey); - //구글 -> 이미지 가져오기 List<String> images = placeImagesService.getPlaceImgs( placeKey, name, address ); return mapper.toSavedPlaceInfo(item, placeKey) .withImages(images);src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClientImpl.java-128-142 (1)
128-142: downloadImage에서 타임아웃 설정 누락
downloadImage에서 다른 메서드들(findPlaceId, getPhotoReferences)과 달리.timeout(Duration.ofSeconds(3))이 설정되어 있지 않습니다. 이미지 다운로드 시 무한 대기 상황을 방지하기 위해 동일하게 타임아웃을 설정하시기 바랍니다.
🧹 Nitpick comments (20)
src/main/java/com/campus/campus/domain/place/application/service/PlaceImageCache.java (1)
9-20: Spring Cache 추상화 활용 고려현재 직접 구현한 캐시 대신 Spring의
@Cacheable,@CacheEvict등의 캐싱 추상화를 활용하는 것을 고려해보세요. 이를 통해 다음과 같은 이점을 얻을 수 있습니다:
- 표준화된 캐싱 인터페이스
- 선언적 캐시 관리
- 다양한 캐시 구현체(Caffeine, Redis 등) 간 쉬운 전환
- 캐시 메트릭 및 모니터링 통합
🔎 Spring Cache 활용 예시
application.yml에 캐시 설정:spring: cache: type: caffeine caffeine: spec: maximumSize=1000,expireAfterWrite=1h서비스 레이어에서 직접 사용:
@Service public class PlaceImageService { @Cacheable(value = "placeImages", key = "#placeKey") public List<String> getPlaceImages(String placeKey) { // 실제 이미지 조회 로직 } @CacheEvict(value = "placeImages", key = "#placeKey") public void evictPlaceImages(String placeKey) { // 캐시 무효화 } }src/main/java/com/campus/campus/domain/place/application/dto/response/naver/NaverGeocodeResponse.java (1)
5-14: Checkstyle 경고 억제 대신 코드 포맷팅을 수정하는 것을 고려하세요.여러 checkstyle 규칙(
RegexpMultiline,WhitespaceAround,MethodParamPad)에 대한 경고를 억제하고 있습니다. 경고를 억제하는 대신 코드를 프로젝트의 코딩 스타일 가이드에 맞게 수정하는 것이 유지보수성 측면에서 더 좋습니다.또한, 레코드의 필드 설명은 인라인 주석보다 Javadoc을 사용하는 것이 더 명확할 수 있습니다.
🔎 제안하는 리팩토링
-@SuppressWarnings({"checkstyle:RegexpMultiline", "checkstyle:WhitespaceAround", "checkstyle:MethodParamPad"}) public record NaverGeocodeResponse( List<AddressItem> addresses ) { - @SuppressWarnings({"checkstyle:RegexpMultiline", "checkstyle:MethodParamPad"}) + /** + * @param x longitude (경도) + * @param y latitude (위도) + */ public record AddressItem( - String x, //longitude - String y //latitude + String x, + String y ) { } }src/main/java/com/campus/campus/domain/place/application/dto/response/google/GoogleTextSearchResponse.java (1)
15-16: 불필요한 빈 줄을 제거하세요.파일 끝에 불필요한 빈 줄이 있습니다.
🔎 제안하는 수정
} } - -src/main/java/com/campus/campus/domain/place/domain/entity/Address.java (1)
7-11: 빈 문자열 및 공백 문자열에 대한 검증 추가 권장현재 null만 검증하고 있지만, 빈 문자열(
"") 또는 공백만 있는 문자열(" ")도 유효하지 않은 주소일 가능성이 높습니다.🔎 검증 강화 제안
public Address { - if (value == null) { + if (value == null || value.isBlank()) { throw new InvalidAddressException(); } }
isBlank()는 null, 빈 문자열, 공백만 있는 문자열을 모두 검증합니다.src/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapResponse.java (1)
1-4: 미사용 빈 클래스 - 제거 또는 TODO 추가 필요
NaverMapResponse클래스가 비어있고 현재 PR에서 사용되지 않습니다. 다음 중 하나를 선택하세요:
- 향후 구현 예정이라면 TODO 주석 추가
- 현재 필요하지 않다면 클래스 제거
- 이미 구현이 필요하다면 구현 완료
구현이 필요한 경우 Naver Map API 응답 구조에 맞는 DTO 구현을 도와드릴 수 있습니다. 새 이슈를 생성할까요?
src/main/java/com/campus/campus/global/config/WebClientConfig.java (1)
10-15: Base URL을 외부 설정 파일로 분리하는 것을 권장합니다.하드코딩된 base URL을
application.properties또는application.yml로 외부화하면 환경별(dev, staging, prod) 관리가 용이하고 유지보수성이 향상됩니다.🔎 제안하는 리팩토링
application.yml에 설정 추가:naver: map: api: base-url: https://naveropenapi.apigw.ntruss.com그리고 코드 수정:
+import org.springframework.beans.factory.annotation.Value; + @Configuration public class WebClientConfig { + @Value("${naver.map.api.base-url}") + private String naverMapBaseUrl; + @Bean public WebClient naverMapWebClient() { return WebClient.builder() - .baseUrl("https://naveropenapi.apigw.ntruss.com") + .baseUrl(naverMapBaseUrl) .build(); } }src/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.java (1)
9-9: 주석 처리된 코드를 제거하거나 TODO로 명확히 표시하세요.주석 처리된
existsByUserIdAndPlaceKey메서드가 있습니다. PR 설명에서 좋아요 기능이 아직 구현되지 않았다고 명시되어 있으므로, 향후 구현을 위한 플레이스홀더로 보입니다.다음 중 하나를 권장합니다:
- 현재 사용하지 않는다면 주석을 제거하고 필요할 때 다시 추가
- 향후 구현 예정이라면 TODO 주석과 함께 의도를 명확히 표시
🔎 제안하는 수정 (Option 2 선택 시)
public interface LikedPlacesRepository extends JpaRepository<LikedPlace, Long> { - // boolean existsByUserIdAndPlaceKey(Long userId, String placeKey); + // TODO: 좋아요 중복 체크를 위해 추가 예정 + // boolean existsByUserIdAndPlaceKey(Long userId, String placeKey); }필요하시다면 좋아요 중복 체크 로직 구현을 도와드릴 수 있습니다. 새로운 이슈를 생성할까요?
src/main/java/com/campus/campus/domain/place/infrastructure/GooglePlaceClient.java (1)
5-7: 메서드 시그니처에 검증과 문서화를 추가하는 것을 고려하세요.다음 사항들을 개선하면 API 사용성과 안정성이 향상됩니다:
limit매개변수에 대한 검증 (양수 값 보장)- 반환값이 빈 리스트인지 null인지 명시
- 발생 가능한 예외에 대한 문서화
🔎 제안하는 개선안
+import jakarta.validation.constraints.Positive; + public interface GooglePlaceClient { + /** + * Fetches image URLs for the specified place from Google Places API. + * + * @param name the name of the place + * @param address the address of the place + * @param limit the maximum number of images to fetch (must be positive) + * @return list of image URLs (never null, may be empty if no images found) + * @throws IllegalArgumentException if limit is not positive + * @throws ExternalApiException if the API call fails + */ - List<String> fetchImages(String name, String address, int limit); + List<String> fetchImages(String name, String address, @Positive int limit); }src/main/java/com/campus/campus/domain/place/presentation/PlaceController.java (1)
23-26: Swagger API 문서화를 위한 @tag 추가를 권장합니다.다른 컨트롤러(
PresignedUrlController)와 일관성을 유지하고 API 문서의 가독성을 높이기 위해@Tag어노테이션을 추가하는 것이 좋습니다.🔎 수정 제안
+import io.swagger.v3.oas.annotations.tags.Tag; + @RestController @RequestMapping("/places") @RequiredArgsConstructor +@Tag(name = "Place", description = "장소 검색 및 관리 API") public class PlaceController {src/main/java/com/campus/campus/domain/place/domain/entity/Coordinate.java (1)
5-9: 좌표 값에 대한 유효성 검증 추가를 권장합니다.위도와 경도 값이 유효한 범위(-90
90, -180180)를 벗어날 수 있습니다. 잘못된 좌표 데이터가 데이터베이스에 저장되는 것을 방지하기 위해 검증 로직을 추가하는 것이 좋습니다.🔎 수정 제안
@Embeddable public record Coordinate( double latitude, double longitude ) { + public Coordinate { + if (latitude < -90.0 || latitude > 90.0) { + throw new IllegalArgumentException( + String.format("위도는 -90~90 범위여야 합니다: %f", latitude) + ); + } + if (longitude < -180.0 || longitude > 180.0) { + throw new IllegalArgumentException( + String.format("경도는 -180~180 범위여야 합니다: %f", longitude) + ); + } + } + public static Coordinate fromNaver(double mapx, double mapy) { return new Coordinate( mapy / 10_000_000.0, mapx / 10_000_000.0 ); } }src/main/java/com/campus/campus/domain/place/domain/entity/LikedPlace.java (1)
18-22: 동일 사용자가 같은 장소를 중복 좋아요할 수 있는 문제현재 구조에서는 동일 사용자가 같은 장소에 대해 여러 번 좋아요를 저장할 수 있습니다.
user_id와place_id조합에 대한 유니크 제약조건 추가를 권장합니다.🔎 유니크 제약조건 추가 제안
@Entity -@Table(name = "liked_places") +@Table(name = "liked_places", uniqueConstraints = { + @UniqueConstraint(columnNames = {"user_id", "place_id"}) +}) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED)src/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapClient.java (1)
31-32: NaverMapConfig 필드 직접 접근
naverMapConfig.clientId와 같이 필드에 직접 접근하고 있습니다.NaverMapConfig의 필드가public으로 선언되어 있다면 캡슐화 원칙에 위배됩니다. getter 메서드를 사용하는 것이 좋습니다.src/main/java/com/campus/campus/domain/place/application/service/PlaceKeyGenerator.java (1)
12-22: @component와 static 메서드 조합
PlaceKeyGenerator는@Component로 선언되어 있지만generate()메서드가static입니다. Spring Bean으로 주입받아 사용하거나, static 유틸리티 클래스로만 사용하거나 둘 중 하나를 선택하는 것이 좋습니다. 현재 구조에서는@Component가 불필요합니다.🔎 유틸리티 클래스로 변경 제안
-import org.springframework.stereotype.Component; - import com.campus.campus.domain.place.application.exception.Sha256AlgorithmException; -@Component +@UtilityClass // Lombok 사용 시 public class PlaceKeyGenerator { + // 또는 private 생성자 추가 + private PlaceKeyGenerator() {}src/main/java/com/campus/campus/domain/place/domain/entity/Place.java (2)
14-18: BaseEntity 상속 누락
LikedPlace는BaseEntity를 상속하여 생성일/수정일을 추적하지만,Place는 상속하지 않습니다. 일관성과 감사(audit) 목적으로BaseEntity상속을 고려해보세요.🔎 BaseEntity 상속 추가 제안
+import com.campus.campus.global.entity.BaseEntity; + @Entity @Table(name = "places") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Place { +public class Place extends BaseEntity {
46-55: 필수 필드 검증 누락
LikedPlace생성자는 null 검증을 수행하지만,Place생성자는placeKey가 DB에서nullable = false임에도 검증이 없습니다. 도메인 레벨에서 일관된 검증을 권장합니다.🔎 검증 추가 제안
public Place(String placeName, String placeKey, String placeCategory, String phone, String address, String url, Coordinate coordinate) { + if (placeKey == null || placeKey.isBlank()) { + throw new IllegalArgumentException("placeKey는 필수입니다."); + } this.placeName = placeName; this.placeKey = placeKey;src/main/java/com/campus/campus/domain/place/presentation/PlaceResponseCode.java (1)
13-14: PLACE_SAVE_SUCCESS에 201 CREATED 권장새 장소 저장 성공 시 RESTful 관례상
201 CREATED가 더 적합합니다. 현재200 OK도 동작에는 문제없지만, REST API 표준을 따르려면 변경을 고려해보세요.🔎 HTTP 상태 코드 변경 제안
- PLACE_SAVE_SUCCESS(200, HttpStatus.OK, "장소 저장이 성공적으로 완료되었습니다."), + PLACE_SAVE_SUCCESS(201, HttpStatus.CREATED, "장소 저장이 성공적으로 완료되었습니다."),src/main/java/com/campus/campus/domain/place/application/service/MapService.java (1)
58-64: 예외를 무시하면 디버깅이 어려워집니다
DataIntegrityViolationException을 완전히 무시하고 있습니다. 중복 키로 인한 예외인 경우 이를 의도적으로 무시하는 것이 맞지만, 최소한 디버그 레벨 로깅이나 주석으로 의도를 명확히 해야 합니다.🔎 수정 제안
private void upsertPlace(Place place) { try { placeRepository.save(place); } catch (DataIntegrityViolationException e) { - + // 이미 존재하는 placeKey인 경우 무시 (upsert 의도) + log.debug("Place already exists, skipping: {}", place.getPlaceKey()); } }
@Slf4j어노테이션을 클래스에 추가하거나, 대안으로placeRepository.existsByPlaceKey(placeKey)를 먼저 확인하는 방식도 고려해 보세요.src/main/java/com/campus/campus/domain/place/application/service/PlaceImagesService.java (1)
56-57: 메서드명 오타
migrateImagestoOci에서to가 소문자로 되어 있습니다. 카멜케이스 규칙에 따라migrateImagesToOci로 수정하세요.src/main/java/com/campus/campus/domain/place/domain/entity/PlaceImages.java (1)
29-30: placeKey 필드에 인덱스 추가 권장
PlaceImagesRepository.findByPlaceKey(placeKey)가 자주 호출됩니다. 쿼리 성능을 위해placeKey컬럼에 인덱스를 추가하는 것을 권장합니다.🔎 수정 제안
+@Table(name = "place_images", indexes = @Index(name = "idx_place_key", columnList = "place_key")) -@Table(name = "place_images")또는 Flyway/Liquibase 마이그레이션 스크립트에서 인덱스를 추가하세요.
src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java (1)
41-57: 인스턴스 메서드와 static 메서드 혼용
toSavedPlaceInfo는 인스턴스 메서드인데toEntity와stripHtml은 static입니다. Spring@Component로 관리되는 클래스이므로 일관성을 위해 모두 인스턴스 메서드로 통일하거나, static 메서드들을 별도 유틸리티 클래스로 분리하는 것을 권장합니다.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (42)
.gitignorebuild.gradlegradlewsrc/main/java/com/campus/campus/domain/place/application/dto/response/CoordinateResponse.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/PlaceSaveResponse.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/google/GooglePhoto.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/google/GooglePlaceDetailResponse.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/google/GoogleTextSearchResponse.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/naver/NaverGeocodeResponse.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/naver/NaverSearchResponse.javasrc/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.javasrc/main/java/com/campus/campus/domain/place/application/exception/InvalidAddressException.javasrc/main/java/com/campus/campus/domain/place/application/exception/PlaceNotFoundException.javasrc/main/java/com/campus/campus/domain/place/application/exception/Sha256AlgorithmException.javasrc/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.javasrc/main/java/com/campus/campus/domain/place/application/service/LikePlaceService.javasrc/main/java/com/campus/campus/domain/place/application/service/MapService.javasrc/main/java/com/campus/campus/domain/place/application/service/PlaceImageCache.javasrc/main/java/com/campus/campus/domain/place/application/service/PlaceImagesService.javasrc/main/java/com/campus/campus/domain/place/application/service/PlaceKeyGenerator.javasrc/main/java/com/campus/campus/domain/place/domain/entity/Address.javasrc/main/java/com/campus/campus/domain/place/domain/entity/Coordinate.javasrc/main/java/com/campus/campus/domain/place/domain/entity/ImageSource.javasrc/main/java/com/campus/campus/domain/place/domain/entity/LikedPlace.javasrc/main/java/com/campus/campus/domain/place/domain/entity/Place.javasrc/main/java/com/campus/campus/domain/place/domain/entity/PlaceImages.javasrc/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.javasrc/main/java/com/campus/campus/domain/place/domain/repository/PlaceImagesRepository.javasrc/main/java/com/campus/campus/domain/place/domain/repository/PlaceRepository.javasrc/main/java/com/campus/campus/domain/place/infrastructure/GooglePlaceClient.javasrc/main/java/com/campus/campus/domain/place/infrastructure/google/GoogleMapConfig.javasrc/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClientImpl.javasrc/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapClient.javasrc/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapConfig.javasrc/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapResponse.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/global/config/PermitUrlConfig.javasrc/main/java/com/campus/campus/global/config/WebClientConfig.javasrc/main/java/com/campus/campus/global/oci/application/service/PresignedUrlService.javasrc/main/resources/application-local.yml
🧰 Additional context used
🧬 Code graph analysis (15)
src/main/java/com/campus/campus/domain/place/domain/repository/PlaceRepository.java (7)
src/main/java/com/campus/campus/domain/school/domain/repository/SchoolRepository.java (1)
SchoolRepository(9-11)src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java (1)
StudentCouncilPostRepository(14-34)src/main/java/com/campus/campus/domain/councilpost/domain/repository/PostImageRepository.java (1)
PostImageRepository(10-17)src/main/java/com/campus/campus/domain/school/domain/repository/CollegeRepository.java (1)
CollegeRepository(9-11)src/main/java/com/campus/campus/domain/user/domain/repository/UserRepository.java (1)
UserRepository(11-21)src/main/java/com/campus/campus/domain/school/domain/repository/MajorRepository.java (1)
MajorRepository(9-11)src/main/java/com/campus/campus/domain/mail/domain/repository/EmailVerificationRepository.java (1)
EmailVerificationRepository(10-16)
src/main/java/com/campus/campus/domain/place/application/dto/response/google/GooglePhoto.java (1)
src/main/java/com/campus/campus/global/auth/application/dto/KakaoTokenResponse.java (1)
KakaoTokenResponse(5-13)
src/main/java/com/campus/campus/domain/place/infrastructure/google/GoogleMapConfig.java (1)
src/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapConfig.java (1)
Configuration(6-14)
src/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapClient.java (2)
src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java (1)
Component(15-65)src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClientImpl.java (1)
Component(17-143)
src/main/java/com/campus/campus/domain/place/application/exception/Sha256AlgorithmException.java (8)
src/main/java/com/campus/campus/domain/council/application/exception/NewPasswordIsCurrentPasswordException.java (2)
NewPasswordIsCurrentPasswordException(5-9)NewPasswordIsCurrentPasswordException(6-8)src/main/java/com/campus/campus/domain/council/application/exception/EmailAlreadyExistsException.java (1)
EmailAlreadyExistsException(5-9)src/main/java/com/campus/campus/domain/school/application/exception/SchoolCollegeNotSameException.java (1)
SchoolCollegeNotSameException(6-8)src/main/java/com/campus/campus/global/common/exception/ApplicationException.java (2)
ApplicationException(9-12)ApplicationException(14-17)src/main/java/com/campus/campus/domain/council/application/exception/PrecautionNotAgreeException.java (1)
PrecautionNotAgreeException(5-9)src/main/java/com/campus/campus/domain/council/application/exception/NewPasswordConfirmNotMatchException.java (1)
NewPasswordConfirmNotMatchException(6-8)src/main/java/com/campus/campus/domain/councilpost/application/exception/ThumbnailRequiredException.java (1)
ThumbnailRequiredException(5-9)src/main/java/com/campus/campus/domain/user/application/exception/NicknameNotMatchException.java (1)
NicknameNotMatchException(5-9)
src/main/java/com/campus/campus/domain/place/application/service/PlaceKeyGenerator.java (1)
src/main/java/com/campus/campus/domain/place/application/exception/Sha256AlgorithmException.java (1)
Sha256AlgorithmException(5-9)
src/main/java/com/campus/campus/global/oci/application/service/PresignedUrlService.java (3)
src/main/java/com/campus/campus/global/oci/application/dto/request/PresignedUrlRequestDto.java (1)
PresignedUrlRequestDto(9-28)src/main/java/com/campus/campus/global/oci/application/dto/response/PresignedUrlResponseDto.java (1)
PresignedUrlResponseDto(5-12)src/main/java/com/campus/campus/global/oci/presentation/PresignedUrlController.java (3)
PostMapping(26-34)RestController(18-49)PostMapping(36-47)
src/main/java/com/campus/campus/domain/place/application/service/PlaceImagesService.java (1)
src/main/java/com/campus/campus/domain/place/application/service/MapService.java (1)
Service(17-66)
src/main/java/com/campus/campus/domain/place/application/service/LikePlaceService.java (2)
src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)src/main/java/com/campus/campus/domain/user/application/service/UserService.java (1)
Service(24-57)
src/main/java/com/campus/campus/domain/place/application/dto/response/google/GoogleTextSearchResponse.java (2)
src/main/java/com/campus/campus/global/auth/application/dto/KakaoTokenResponse.java (1)
KakaoTokenResponse(5-13)src/main/java/com/campus/campus/global/auth/application/dto/KakaoUserResponse.java (1)
KakaoUserResponse(5-19)
src/main/java/com/campus/campus/domain/place/presentation/PlaceController.java (2)
src/main/java/com/campus/campus/global/oci/presentation/PresignedUrlController.java (1)
RestController(18-49)src/main/java/com/campus/campus/domain/user/presentation/UserController.java (1)
RestController(18-32)
src/main/java/com/campus/campus/domain/place/domain/entity/Place.java (2)
src/main/java/com/campus/campus/domain/place/domain/entity/LikedPlace.java (1)
Entity(18-46)src/main/java/com/campus/campus/domain/place/domain/entity/PlaceImages.java (1)
Entity(15-56)
src/main/java/com/campus/campus/domain/place/domain/entity/Address.java (1)
src/main/java/com/campus/campus/domain/place/application/exception/InvalidAddressException.java (1)
InvalidAddressException(5-9)
src/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapConfig.java (1)
src/main/java/com/campus/campus/domain/place/infrastructure/google/GoogleMapConfig.java (1)
Configuration(5-10)
src/main/java/com/campus/campus/domain/place/domain/entity/LikedPlace.java (1)
src/main/java/com/campus/campus/domain/place/domain/entity/Place.java (1)
Entity(14-62)
🔇 Additional comments (20)
.gitignore (1)
21-21: 좋은 실천 사항입니다. .env 파일을 무시하는 규칙을 추가했습니다.환경 설정 파일이나 민감한 정보를 포함할 수 있는
.env파일을 버전 관리에서 제외하는 것은 보안 모범 사례입니다. 규칙의 위치(IntelliJ IDEA 섹션)도 적절합니다.src/main/java/com/campus/campus/global/config/PermitUrlConfig.java (1)
27-28: 장소 검색 엔드포인트의 공개 접근 허용 여부를 확인하세요.
/places/search엔드포인트를 인증 없이 공개 접근 가능하도록 설정했습니다. 장소 검색 기능이 비인증 사용자에게도 제공되어야 하는지 확인이 필요합니다. 만약 인증된 사용자만 접근해야 한다면, 이 엔드포인트를 공개 URL 목록에서 제거해야 합니다.build.gradle (1)
71-71: WebFlux 의존성의 사용 목적을 확인하세요.Spring WebFlux 의존성을 추가했습니다. PR의 다른 컴포넌트들이 주로 전통적인 블로킹 방식의 Spring을 사용하고 있는 것으로 보입니다. WebFlux가
WebClient를 통한 외부 API 호출(Google Places API, Naver API)에 사용되는 것인지 확인하세요.만약 단순히 HTTP 클라이언트로만 사용된다면, 프로젝트에 리액티브 프로그래밍이 필요하지 않은 경우
RestTemplate이나RestClient(Spring 6.1+)를 고려할 수도 있습니다. 다만, 이미WebClient를 사용 중이라면 현재 구현을 유지하는 것이 좋습니다.src/main/java/com/campus/campus/domain/place/application/dto/response/CoordinateResponse.java (1)
5-12: 잘 구현되었습니다!좌표 응답 DTO가 깔끔하게 구현되었습니다. Swagger 문서화를 위한
@Schema애너테이션과 예시 값이 잘 작성되어 있어 API 문서의 가독성이 좋습니다.src/main/java/com/campus/campus/domain/place/application/dto/response/google/GoogleTextSearchResponse.java (1)
7-14: 구조가 잘 설계되었습니다.Google Text Search API 응답을 위한 DTO가 잘 구현되었습니다.
@JsonProperty를 사용하여 Google API의 snake_case 네이밍(place_id)을 Java의 camelCase 네이밍(placeId)으로 올바르게 매핑하고 있으며, 기존 코드베이스의 다른 API 응답 DTO들(KakaoTokenResponse,KakaoUserResponse)과 일관된 패턴을 따르고 있습니다.src/main/resources/application-local.yml (2)
58-64: 지도 API 설정 구조가 적절합니다Naver Map API와 Google Places API 자격 증명이 환경 변수로 올바르게 외부화되어 있습니다. 보안 모범 사례를 따르고 있습니다.
16-16: 로컬 환경의ddl-auto: create설정은 적절합니다확인 결과, 환경별 DDL 설정이 올바르게 구성되어 있습니다:
- Local:
create(로컬 개발 환경에서 적절)- Dev:
update(데이터 보존)- Prod:
update(안전성 우선)application-local.yml의 현재 설정은 의도적이며 로컬 개발을 위해 올바르게 구성되었습니다.
src/main/java/com/campus/campus/domain/place/application/dto/response/google/GooglePhoto.java (1)
5-7: Google Places API 사진 참조 DTO가 올바르게 구현되었습니다Jackson
@JsonProperty어노테이션을 사용하여 JSON의 snake_case 필드명을 Java camelCase로 올바르게 매핑하고 있습니다. 코드베이스의 기존 패턴(예:KakaoTokenResponse)을 일관되게 따르고 있습니다.src/main/java/com/campus/campus/domain/place/domain/entity/ImageSource.java (1)
3-6: 이미지 소스 구분이 명확하고 적절합니다
ImageSourceenum이 이미지 저장 전략을 명확하게 구분합니다:
OCI: 좋아요 시 실제 파일 업로드 (영구 저장)이는 PR 목표인 "Google Places API 호출 최소화"와 완벽하게 일치합니다.
src/main/java/com/campus/campus/domain/place/application/exception/PlaceNotFoundException.java (1)
5-8: LGTM!기존 예외 클래스 패턴과 일관되게 구현되었습니다.
src/main/java/com/campus/campus/domain/place/application/exception/Sha256AlgorithmException.java (1)
5-8: LGTM!다른 커스텀 예외들과 동일한 패턴으로 올바르게 구현되었습니다.
src/main/java/com/campus/campus/domain/place/application/exception/InvalidAddressException.java (1)
5-9: 구현이 깔끔합니다.표준적인 예외 패턴을 따르고 있으며, 중앙화된 ErrorCode를 활용하고 있습니다.
src/main/java/com/campus/campus/domain/place/application/dto/response/PlaceSaveResponse.java (1)
3-4: 간결하고 명확한 구현입니다.Response DTO로서 적절한 구조를 가지고 있습니다.
src/main/java/com/campus/campus/domain/place/domain/repository/PlaceImagesRepository.java (1)
1-12: LGTM!표준 Spring Data JPA 리포지토리 인터페이스가 올바르게 구현되었습니다.
findByPlaceKey메서드는 네이밍 규칙을 따르고 있으며, 적절한 반환 타입을 사용하고 있습니다.src/main/java/com/campus/campus/domain/place/domain/entity/Coordinate.java (1)
10-15: LGTM!네이버 지도 API의 좌표 변환 로직이 정확합니다. 네이버는 정수형 좌표를 사용하므로 10,000,000으로 나누는 것이 올바른 변환 방식입니다.
src/main/java/com/campus/campus/domain/place/domain/entity/LikedPlace.java (1)
29-44: LGTM!
FetchType.LAZY사용과 생성자에서의 null 검증이 적절합니다. 도메인 규칙을 생성 시점에 검증하는 방식이 좋습니다.src/main/java/com/campus/campus/domain/place/application/service/PlaceKeyGenerator.java (1)
24-34: LGTM!정규화 로직이 적절합니다. null 처리, HTML 태그 제거, 소문자 변환, 공백/특수문자 제거를 통해 일관된 키 생성이 가능합니다.
src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java (1)
48-59: LGTM!
withImages()메서드가 record의 불변성을 유지하면서 이미지 목록을 업데이트할 수 있는 좋은 패턴입니다.src/main/java/com/campus/campus/domain/place/application/dto/response/naver/NaverSearchResponse.java (1)
5-20: LGTM!Naver 검색 API 응답을 위한 깔끔한 record 정의입니다.
telephone이나link같은 필드는 null일 수 있으므로, 이를 사용하는 곳에서 null 체크가 필요할 수 있습니다.src/main/java/com/campus/campus/domain/place/domain/entity/PlaceImages.java (1)
39-54: LGTM!생성자와
isOciStored(),updateToOci()메서드가 깔끔하게 구현되어 있습니다. 상태 변경이 명확하게 캡슐화되어 있습니다.
src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java
Outdated
Show resolved
Hide resolved
| //장소 저장 | ||
| Place place = Place.create( | ||
| placeInfo.placeName(), | ||
| placeInfo.placeKey(), | ||
| placeInfo.category(), | ||
| placeInfo.telephone(), | ||
| placeInfo.address(), | ||
| placeInfo.link(), | ||
| placeInfo.coordinate() | ||
| ); |
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.
중복 장소 저장 가능성
동일한 placeKey를 가진 장소가 이미 존재하는지 확인하지 않고 매번 새로운 Place를 생성합니다. placeKey에 unique 제약조건이 있으므로 중복 저장 시 DataIntegrityViolationException이 발생할 수 있습니다. 기존 장소 조회 후 없으면 생성하는 로직이 필요합니다.
🔎 중복 체크 로직 추가 제안
+ // 기존 장소 조회 또는 새로 생성
+ Place place = placeRepository.findByPlaceKey(placeInfo.placeKey())
+ .orElseGet(() -> Place.create(
+ placeInfo.placeName(),
+ placeInfo.placeKey(),
+ placeInfo.category(),
+ placeInfo.telephone(),
+ placeInfo.address(),
+ placeInfo.link(),
+ placeInfo.coordinate()
+ ));
- //장소 저장
- Place place = Place.create(
- placeInfo.placeName(),
- placeInfo.placeKey(),
- placeInfo.category(),
- placeInfo.telephone(),
- placeInfo.address(),
- placeInfo.link(),
- placeInfo.coordinate()
- );Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
src/main/java/com/campus/campus/domain/place/application/service/LikePlaceService.java
around lines 31-40, the code always creates a new Place from placeInfo which can
violate the unique placeKey constraint and trigger
DataIntegrityViolationException; change the flow to first attempt to load an
existing Place by placeKey via the PlaceRepository (e.g., findByPlaceKey), and
if present use that instance, otherwise create and save a new Place; to be
robust against race conditions, catch DataIntegrityViolationException on save,
re-query by placeKey and use the existing record if the exception indicates a
duplicate.
| //좋아요 누른 장소 저장 | ||
| likedPlacesRepository.save(new LikedPlace(user, place)); | ||
| return placeRepository.save(place).getPlaceId(); |
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.
영속되지 않은 Place 엔티티를 LikedPlace에서 참조하는 문제
LikedPlace를 저장하기 전에 Place가 먼저 영속화되어야 합니다. 현재 순서에서는 place가 아직 ID가 없는 transient 상태이므로 TransientObjectException이 발생할 수 있습니다.
🔎 저장 순서 수정 제안
- //좋아요 누른 장소 저장
- likedPlacesRepository.save(new LikedPlace(user, place));
- return placeRepository.save(place).getPlaceId();
+ //장소 먼저 저장
+ Place savedPlace = placeRepository.save(place);
+ //좋아요 누른 장소 저장
+ likedPlacesRepository.save(new LikedPlace(user, savedPlace));
+ return savedPlace.getPlaceId();🤖 Prompt for AI Agents
In
src/main/java/com/campus/campus/domain/place/application/service/LikePlaceService.java
around lines 42-44, the code saves a LikedPlace that refers to a transient
(unsaved) Place which can trigger TransientObjectException; first persist the
Place by calling placeRepository.save(place) and capture the returned managed
entity (or its id), then construct and save the LikedPlace with that persisted
Place (or reference by id). Also ensure the method runs in a transactional
context so the saved entities are managed within the same transaction.
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!
| @Component | ||
| public class PlaceImageCache { | ||
|
|
||
| private final Map<String, List<String>> store = new ConcurrentHashMap<>(); |
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.
무제한 캐시로 인한 메모리 누수 위험
현재 구현은 크기 제한, 만료 정책(TTL), 또는 제거 전략(eviction policy)이 없는 무제한 캐시입니다. 프로덕션 환경에서 장소 검색이 계속 발생하면 캐시가 무한정 증가하여 결국 OutOfMemoryError가 발생할 수 있습니다.
다음 중 하나를 구현하는 것을 권장합니다:
- 최대 크기 제한과 LRU(Least Recently Used) 제거 정책
- TTL(Time To Live) 기반 자동 만료
- Caffeine 또는 Guava Cache 같은 검증된 캐싱 라이브러리 사용
🔎 Caffeine을 사용한 개선 예시
build.gradle에 의존성 추가:
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'캐시 구현 개선:
package com.campus.campus.domain.place.application.service;
import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
import org.springframework.stereotype.Component;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
@Component
public class PlaceImageCache {
- private final Map<String, List<String>> store = new ConcurrentHashMap<>();
+ private final Cache<String, List<String>> store = Caffeine.newBuilder()
+ .maximumSize(1000) // 최대 1000개 항목
+ .expireAfterWrite(1, TimeUnit.HOURS) // 1시간 후 자동 만료
+ .build();
public List<String> get(String placeKey) {
- return store.get(placeKey);
+ return store.getIfPresent(placeKey);
}
public void put(String placeKey, List<String> images) {
store.put(placeKey, images);
}
}🤖 Prompt for AI Agents
In
src/main/java/com/campus/campus/domain/place/application/service/PlaceImageCache.java
around lines 9-12, the current ConcurrentHashMap-backed store is an unbounded
cache which risks OOM; replace it with a bounded, eviction-capable cache
(recommend using Caffeine). Add the Caffeine dependency to build.gradle, change
the field to a Caffeine Cache<String, List<String>> built with reasonable
maxSize (e.g. 10_000) and an expireAfterWrite TTL (e.g. 1 hour) or
expireAfterAccess as needed, and update the class methods to use
cache.getIfPresent()/put()/invalidate() instead of the Map APIs; ensure
thread-safety is preserved and add unit tests for eviction/expiry behavior.
src/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapConfig.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/presentation/PlaceController.java
Outdated
Show resolved
Hide resolved
| @GetMapping("/search") | ||
| @Operation(summary = "장소 키워드로 검색", description = "검색 결과 5개 검색되도록 함") | ||
| public CommonResponse<List<SavedPlaceInfo>> getPlaceInfo(@RequestParam String keyword) { | ||
| List<SavedPlaceInfo> searchResponse = mapService.search(keyword); | ||
| return CommonResponse.success(PlaceResponseCode.PLACE_SEARCH_SUCCESS, searchResponse); | ||
| } |
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
검색 키워드 유효성 검증을 추가하세요.
keyword 파라미터에 대한 유효성 검증이 없어 빈 문자열이나 공백만 있는 값이 전달될 수 있습니다. API 호출 시 불필요한 외부 API 호출을 방지하기 위해 검증을 추가해야 합니다.
🔎 수정 제안
+import jakarta.validation.constraints.NotBlank;
+import org.springframework.validation.annotation.Validated;
+
@RestController
@RequestMapping("/places")
@RequiredArgsConstructor
+@Validated
public class PlaceController {
// ...
@GetMapping("/search")
@Operation(summary = "장소 키워드로 검색", description = "검색 결과 5개 검색되도록 함")
- public CommonResponse<List<SavedPlaceInfo>> getPlaceInfo(@RequestParam String keyword) {
+ public CommonResponse<List<SavedPlaceInfo>> getPlaceInfo(
+ @RequestParam @NotBlank(message = "검색 키워드는 필수입니다") String keyword
+ ) {
List<SavedPlaceInfo> searchResponse = mapService.search(keyword);
return CommonResponse.success(PlaceResponseCode.PLACE_SEARCH_SUCCESS, searchResponse);
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
src/main/java/com/campus/campus/domain/place/presentation/PlaceController.java
around lines 32 to 37, the RequestParam 'keyword' is not validated allowing
empty or blank strings which may trigger unnecessary external calls; add
validation to reject null/empty/blank keywords before calling mapService.search
and return an appropriate CommonResponse error (e.g.,
PLACE_SEARCH_INVALID_KEYWORD) with HTTP 400. Implement this by either annotating
the parameter with a validation annotation (e.g., @NotBlank) and enabling
validation or explicitly checking StringUtils.hasText(keyword) and returning the
error response when validation fails; only call mapService.search when the
keyword is valid.
src/main/java/com/campus/campus/domain/place/presentation/PlaceController.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/global/oci/application/service/PresignedUrlService.java
Show resolved
Hide resolved
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.
민서님 수고하셨습니다!!
리뷰가 조금 많은텐데 죄송합니다... 저의 개인적인 생각와 수정이 필요할 것 같은 부분들을 전부 확인하다보니 조금 리뷰가 많아진 것 같아요!! 그래도 코드 하나하나 확인하며 리뷰를 달았으니 한번씩 확인부탁드려요ㅠㅠㅠ
또한 상세 리뷰로 남기기 애매하여 통합리뷰로 추가적으로 이야기 남길 부분이 있어 여기에 남길게요!!
- 현재 서비스 코드의 경우 LikePlaceService, MapService, PlaceImagesSerivce이루어져 있어요!! 2개의 api를 구현하기 위해 3개의 서비스 클래스를 만드는 것은 코드를 오히려 보기 어렵지 않을까 하는 생각이 들었어요. 서비스 클래스의 경우 PlaceSerivce 하나로 통일하는 것에 대해서는 어떻게 생각하시나요??
- PlaceKeyGenerator의 경우 Service 보다는 Util에 가깝지 않을까 하는 생각이 들었어요!! application 내 util 디렉토리를 만들어 PlaceKeyGenerator의 위치를 옮기는 것에 대해 어떻게 생각하시나요??
src/main/java/com/campus/campus/domain/place/application/dto/response/LikeResponse.java
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java
Outdated
Show resolved
Hide resolved
| List<String> imgUrls | ||
|
|
||
| ) { | ||
| public static String buildNaverPlaceUrl(NaverSearchResponse.Item item) { |
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.
이 부분의 경우 mapper로 분리하는 것이 현재 컨벤션에 맞다고 생각해요!!
src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/dto/response/google/GooglePhoto.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/service/MapService.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/service/MapService.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/service/LikePlaceService.java
Outdated
Show resolved
Hide resolved
| //좋아요 누른 장소 저장 | ||
| likedPlacesRepository.save(new LikedPlace(user, place)); | ||
| return placeRepository.save(place).getPlaceId(); |
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 해당 이슈 부분 확인부탁드려요!! 아직 좋아요 기능이 구현이 제대로 안된 상태라면 구현 시 확인 부탁드려요!!
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClientImpl.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: 6
♻️ Duplicate comments (4)
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClientImpl.java (2)
52-58:limit파라미터가 무시되고 있으며, API 키가 로그에 노출됩니다.
- Line 53에서
limit(3)이 하드코딩되어 메서드 파라미터limit이 사용되지 않습니다.- Line 57에서
imageUrls를 로깅하면 각 URL에 포함된 Google API 키가 로그에 기록됩니다.🔎 수정 제안
List<String> imageUrls = photoRefs.stream() - .limit(3) + .limit(limit) .map(this::buildPhotoUrl) .toList(); - log.info("imageUrls: {}", imageUrls); + log.info("Fetched {} image URLs for place", imageUrls.size()); return imageUrls;
78-86: 빈 결과 리스트에 대한IndexOutOfBoundsException위험
response.results()가 비어있거나 null인 경우get(0)호출 시 예외가 발생합니다.🔎 수정 제안
if (response == null) { return null; } + if (response.results() == null || response.results().isEmpty()) { + return null; + } + String placeId = response.results().get(0).placeId();src/main/java/com/campus/campus/global/oci/application/service/PresignedUrlService.java (1)
114-135: 입력 검증 누락 및 예외 원인 정보 손실과거 리뷰에서 지적된 입력 검증이 여전히 누락되어 있습니다. 또한 예외 발생 시 원인(cause)이 전달되지 않아 디버깅이 어려울 수 있습니다.
🔎 수정 제안
public void uploadToOci( String uploadUrl, // presigned PUT URL byte[] imageBytes, // 업로드할 이미지 바이트 String contentType // image/jpeg 등 ) { + if (uploadUrl == null || uploadUrl.isBlank()) { + throw new IllegalArgumentException("uploadUrl은 필수입니다"); + } + if (imageBytes == null || imageBytes.length == 0) { + throw new IllegalArgumentException("imageBytes는 필수입니다"); + } + if (contentType == null || contentType.isBlank()) { + throw new IllegalArgumentException("contentType은 필수입니다"); + } try { restClient .put() .uri(uploadUrl) - .contentType(MediaType.parseMediaType(contentType))// 업로드 완료까지 대기 + .contentType(MediaType.parseMediaType(contentType)) .body(imageBytes) .retrieve() .toBodilessEntity(); log.info("[OCI] upload success. uploadUrl={}", uploadUrl); } catch (Exception e) { log.error("[OCI] upload failed. uploadUrl={}", uploadUrl, e); - throw new IllegalStateException("OCI upload failed"); + throw new IllegalStateException("OCI upload failed", e); } }src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java (1)
36-39: 좌표 파싱 시 예외 처리 필요
item.mapy()와item.mapx()가 null이거나 유효하지 않은 숫자 형식일 경우NumberFormatException이 발생합니다. Naver API 응답의 안정성을 보장할 수 없으므로 예외 처리를 추가해주세요.🔎 수정 제안
+ try { Coordinate.fromNaver( Double.parseDouble(item.mapy()), //위도 Double.parseDouble(item.mapx()) //경도 - ), + ) + } catch (NumberFormatException | NullPointerException e) { + throw new IllegalArgumentException("Invalid coordinate data from Naver API", e); + }또는 공통 헬퍼 메서드로 추출:
private Coordinate parseCoordinate(NaverSearchResponse.Item item) { try { return Coordinate.fromNaver( Double.parseDouble(item.mapy()), Double.parseDouble(item.mapx()) ); } catch (NumberFormatException | NullPointerException e) { throw new IllegalArgumentException("Invalid coordinate data from Naver API", e); } }
🧹 Nitpick comments (5)
src/main/java/com/campus/campus/global/config/WebClientConfig.java (2)
11-12: 클래스명과 생성 빈의 불일치클래스명이
WebClientConfig이지만 실제로는RestClient빈을 생성하고 있습니다. 클래스명을RestClientConfig로 변경하는 것이 더 명확합니다.🔎 제안하는 수정
-public class WebClientConfig { +public class RestClientConfig {
14-23: readTimeout 설정 추가 권장현재
connectTimeout만 설정되어 있어, 연결 후 서버가 응답을 느리게 보내는 경우 무한정 대기할 수 있습니다.readTimeout도 함께 설정하는 것을 권장합니다.🔎 제안하는 수정
@Bean public RestClient restClient() { return RestClient.builder() .requestFactory(new JdkClientHttpRequestFactory( HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) + .build() - .build() + )) - )) .build(); }참고: Java 11+의
HttpClient는readTimeout을 요청별로 설정해야 하므로, 필요시RestClient.builder().defaultRequest()에서 설정하거나 각 호출 시점에 timeout을 지정하는 방식도 고려해보세요.src/main/java/com/campus/campus/domain/place/application/util/PlaceKeyGenerator.java (1)
12-15:@Component와 정적 메서드의 조합이 일관성이 없습니다.모든 메서드가
static으로 선언되어 있어 인스턴스 상태가 필요 없습니다.@Component를 제거하고 순수 유틸리티 클래스로 만들거나, 메서드를 인스턴스 메서드로 변경하여 Spring Bean으로 주입받는 방식 중 하나를 선택하세요.🔎 유틸리티 클래스로 변경 제안
-import org.springframework.stereotype.Component; - import com.campus.campus.domain.place.application.exception.Sha256AlgorithmException; -@Component public class PlaceKeyGenerator { + + private PlaceKeyGenerator() { + // 유틸리티 클래스 + }src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
38-38: 구현체 대신 인터페이스를 주입하세요.
GooglePlaceClientImpl대신GooglePlaceClient인터페이스를 주입하는 것이 DIP(의존성 역전 원칙)를 따르고 테스트 용이성을 높입니다.🔎 수정 제안
-import com.campus.campus.domain.place.infrastructure.google.GooglePlaceClientImpl; +import com.campus.campus.domain.place.infrastructure.GooglePlaceClient; ... - private final GooglePlaceClientImpl googleClient; + private final GooglePlaceClient googleClient;src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java (1)
68-70: static 메서드를 인스턴스 메서드로 변경 고려팀 컨벤션에 따라 mapper의 모든 메서드는 인스턴스 메서드여야 합니다. 이 유틸리티 메서드도 일관성을 위해
static을 제거하는 것을 고려해주세요.참고: 현재 정규식
<[^>]*>는 대부분의 HTML 태그를 제거하지만, 중첩되거나 비정상적인 HTML(예:<b>text<i>nested</b></i>)에서는 완벽하지 않을 수 있습니다. 현재 Naver API 응답 형식에서는 문제없어 보이지만, 향후 더 강력한 HTML 파싱이 필요하다면 JSoup 같은 라이브러리 사용을 고려할 수 있습니다.🔎 수정 제안
- public static String stripHtml(String text) { + public String stripHtml(String text) { return text.replaceAll("<[^>]*>", ""); }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (27)
src/main/java/com/campus/campus/domain/place/application/dto/response/LikeResponse.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/google/GooglePhoto.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/google/GooglePlaceDetailResponse.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/google/GoogleTextSearchResponse.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/naver/NaverSearchResponse.javasrc/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.javasrc/main/java/com/campus/campus/domain/place/application/exception/NaverMapAPIException.javasrc/main/java/com/campus/campus/domain/place/application/exception/Sha256AlgorithmException.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/application/util/PlaceKeyGenerator.javasrc/main/java/com/campus/campus/domain/place/domain/entity/Coordinate.javasrc/main/java/com/campus/campus/domain/place/domain/entity/ImageSource.javasrc/main/java/com/campus/campus/domain/place/domain/entity/LikedPlace.javasrc/main/java/com/campus/campus/domain/place/domain/entity/Place.javasrc/main/java/com/campus/campus/domain/place/domain/entity/PlaceImages.javasrc/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.javasrc/main/java/com/campus/campus/domain/place/domain/repository/PlaceImagesRepository.javasrc/main/java/com/campus/campus/domain/place/domain/repository/PlaceRepository.javasrc/main/java/com/campus/campus/domain/place/infrastructure/GooglePlaceClient.javasrc/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClientImpl.javasrc/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapClient.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/global/config/WebClientConfig.javasrc/main/java/com/campus/campus/global/oci/application/service/PresignedUrlService.java
✅ Files skipped from review due to trivial changes (2)
- src/main/java/com/campus/campus/domain/place/application/dto/response/google/GooglePlaceDetailResponse.java
- src/main/java/com/campus/campus/domain/place/application/dto/response/naver/NaverSearchResponse.java
🚧 Files skipped from review as they are similar to previous changes (12)
- src/main/java/com/campus/campus/domain/place/domain/repository/PlaceRepository.java
- src/main/java/com/campus/campus/domain/place/domain/entity/ImageSource.java
- src/main/java/com/campus/campus/domain/place/application/exception/Sha256AlgorithmException.java
- src/main/java/com/campus/campus/domain/place/infrastructure/GooglePlaceClient.java
- src/main/java/com/campus/campus/domain/place/domain/repository/PlaceImagesRepository.java
- src/main/java/com/campus/campus/domain/place/application/dto/response/google/GooglePhoto.java
- src/main/java/com/campus/campus/domain/place/domain/entity/LikedPlace.java
- src/main/java/com/campus/campus/domain/place/domain/entity/Coordinate.java
- src/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.java
- src/main/java/com/campus/campus/domain/place/presentation/PlaceController.java
- src/main/java/com/campus/campus/domain/place/presentation/PlaceResponseCode.java
- src/main/java/com/campus/campus/domain/place/application/dto/response/google/GoogleTextSearchResponse.java
🧰 Additional context used
🧬 Code graph analysis (9)
src/main/java/com/campus/campus/domain/place/application/exception/NaverMapAPIException.java (9)
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/MajorNotFoundException.java (1)
MajorNotFoundException(5-9)src/main/java/com/campus/campus/domain/user/application/exception/NicknameNotMatchException.java (1)
NicknameNotMatchException(5-9)src/main/java/com/campus/campus/domain/school/application/exception/CollegeNotFoundException.java (1)
CollegeNotFoundException(5-9)src/main/java/com/campus/campus/domain/school/application/exception/SchoolNotFoundException.java (1)
SchoolNotFoundException(5-9)src/main/java/com/campus/campus/domain/council/application/exception/EmailAlreadyExistsException.java (1)
EmailAlreadyExistsException(5-9)src/main/java/com/campus/campus/global/oci/exception/OciPresignedUrlCreateFailException.java (1)
OciPresignedUrlCreateFailException(5-9)src/main/java/com/campus/campus/domain/mail/application/exception/VerificationCodeNotMatchException.java (1)
VerificationCodeNotMatchException(5-9)src/main/java/com/campus/campus/domain/council/application/exception/NewPasswordIsCurrentPasswordException.java (1)
NewPasswordIsCurrentPasswordException(5-9)
src/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.java (2)
src/main/java/com/campus/campus/domain/user/domain/repository/UserRepository.java (1)
UserRepository(11-21)src/main/java/com/campus/campus/domain/school/domain/repository/MajorRepository.java (2)
MajorRepository(9-11)findBySchool_SchoolIdAndMajorNameStartingWith(10-10)
src/main/java/com/campus/campus/global/oci/application/service/PresignedUrlService.java (2)
src/main/java/com/campus/campus/global/oci/application/dto/request/PresignedUrlRequestDto.java (1)
PresignedUrlRequestDto(9-28)src/main/java/com/campus/campus/global/oci/application/dto/response/PresignedUrlResponseDto.java (1)
PresignedUrlResponseDto(5-12)
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)
src/main/java/com/campus/campus/domain/place/application/dto/response/LikeResponse.java (8)
src/main/java/com/campus/campus/domain/school/application/dto/response/CollegeFindResponse.java (1)
CollegeFindResponse(5-12)src/main/java/com/campus/campus/domain/councilpost/application/dto/response/PostListItemResponseDto.java (1)
PostListItemResponseDto(8-18)src/main/java/com/campus/campus/global/auth/application/dto/KakaoTokenResponse.java (1)
KakaoTokenResponse(5-13)src/main/java/com/campus/campus/domain/school/application/dto/response/MajorFindResponse.java (1)
MajorFindResponse(5-12)src/main/java/com/campus/campus/domain/user/application/dto/response/UserFirstProfileResponse.java (1)
Builder(6-17)src/main/java/com/campus/campus/domain/council/application/dto/response/StudentCouncilFindIdResponse.java (1)
Builder(6-11)src/main/java/com/campus/campus/global/oci/application/dto/response/PresignedUrlResponseDto.java (1)
PresignedUrlResponseDto(5-12)src/main/java/com/campus/campus/global/util/jwt/application/dto/response/TokenReissueResponse.java (1)
TokenReissueResponse(5-12)
src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java (5)
src/main/java/com/campus/campus/domain/school/application/dto/response/CollegeFindResponse.java (1)
CollegeFindResponse(5-12)src/main/java/com/campus/campus/domain/school/application/dto/response/MajorFindResponse.java (1)
MajorFindResponse(5-12)src/main/java/com/campus/campus/domain/councilpost/application/dto/response/PostListItemResponseDto.java (1)
PostListItemResponseDto(8-18)src/main/java/com/campus/campus/domain/user/application/dto/response/UserFirstProfileResponse.java (1)
Builder(6-17)src/main/java/com/campus/campus/global/oci/application/dto/response/PresignedUrlResponseDto.java (1)
PresignedUrlResponseDto(5-12)
src/main/java/com/campus/campus/domain/place/domain/entity/PlaceImages.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/councilpost/domain/entity/PostImage.java (1)
Entity(18-36)
src/main/java/com/campus/campus/domain/place/application/util/PlaceKeyGenerator.java (1)
src/main/java/com/campus/campus/domain/place/application/exception/Sha256AlgorithmException.java (1)
Sha256AlgorithmException(5-9)
src/main/java/com/campus/campus/domain/place/domain/entity/Place.java (1)
src/main/java/com/campus/campus/domain/place/domain/entity/PlaceImages.java (1)
Entity(16-46)
⏰ 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 (11)
src/main/java/com/campus/campus/domain/place/domain/entity/Place.java (2)
17-27: LGTM! 엔티티 구조가 깔끔하게 개선되었습니다.과거 리뷰 피드백대로
@Builder와@AllArgsConstructor를 추가하여 엔티티 생성 로직을 Mapper로 분리한 점이 좋습니다.
34-36: PlaceImages의 placeKey는 필요한 설계 패턴입니다검증 결과,
PlaceImages의placeKey필드는 불필요한 중복이 아닙니다. 코드 분석 결과 다음과 같은 이유로 의도된 설계입니다:
Place 엔티티 생성 전 PlaceImages 생성:
getPlaceImgs()메서드에서 외부 API(Google)로부터 이미지를 먼저 조회하고 저장합니다. 이때 Place 엔티티는 아직 생성되지 않을 수 있습니다.독립적 조회 필요성:
placeImagesRepository.findByPlaceKey(placeKey)를 통해 Place 관계를 로드하지 않고도 플레이스 키로 직접 이미지를 조회합니다. 이는 성능 최적화에 해당합니다.두 단계 프로세스: PlaceService에서 이미지 생성 후 별도로 Place를 조회/생성하는 워크플로우를 지원합니다.
src/main/java/com/campus/campus/domain/place/application/exception/NaverMapAPIException.java (1)
5-9: LGTM! 예외 클래스 구현이 올바릅니다.기존 코드베이스의 다른 예외 클래스들(
MajorNotFoundException,SchoolNotFoundException등)과 동일한 패턴을 따르고 있어 일관성이 유지되고 있습니다.src/main/java/com/campus/campus/domain/place/application/dto/response/LikeResponse.java (1)
3-5: LGTM! 과거 리뷰 피드백이 반영되었습니다.이전 리뷰에서 논의된 대로
likedboolean 값이 추가되어 좋아요 상태를 명확하게 반환하고 있습니다.src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java (1)
9-35: LGTM! 깔끔한 DTO 구조로 개선되었습니다.과거 리뷰에서 논의된 대로 비즈니스 로직이 mapper로 분리되어 순수한 데이터 구조만 남았습니다. Schema 어노테이션도 잘 작성되어 있습니다.
참고: 과거 리뷰에서 언급된 네이버 지도 URL 좌표 순서 이슈는 이제 mapper에서 처리되고 있을 것으로 보이니,
PlaceMapper에서Coordinate.fromNaver()호출 시 위도(mapy), 경도(mapx) 순서가 올바르게 전달되는지 확인 부탁드립니다.src/main/java/com/campus/campus/domain/place/application/util/PlaceKeyGenerator.java (2)
24-34: 정규화 로직이 잘 구현되었습니다.HTML 태그 제거, 소문자 변환, 공백 제거, 특수문자 제거 순서로 일관된 키 생성이 가능합니다. null 체크도 적절합니다.
36-45: SHA-256 해싱 구현이 적절합니다.UTF-8 인코딩과 HexFormat을 사용한 구현이 깔끔합니다. Java 명세상 SHA-256은 항상 지원되므로 예외 발생은 극히 드물지만, 방어적 예외 처리가 적절합니다.
src/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.java (1)
9-14: LGTM!복합 키(userId, placeKey) 기반 조회 메서드가 적절히 정의되어 있습니다. Spring Data JPA 명명 규칙을 따르고 있습니다.
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
125-165: 좋아요 토글 로직이 적절하게 구현되었습니다.기존 좋아요 확인, Place 존재 여부 확인, 신규 생성 시에만 이미지 마이그레이션 수행 등 비즈니스 로직이 명확합니다.
src/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapClient.java (1)
29-51: API 호출 로직이 적절히 구현되었으며, 타임아웃도 정상 설정되어 있습니다.
onStatus를 통한 에러 처리가 추가되어 과거 리뷰의 에러 처리 누락 문제가 해결되었습니다. RestClient의 타임아웃도WebClientConfig에서connectTimeout(Duration.ofSeconds(10))으로 설정되어 있으므로 모든 주입된 RestClient 인스턴스에 타임아웃이 적용됩니다.src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java (1)
58-63: LGTM!
PlaceImages엔티티 생성 로직이 간결하고 명확합니다.
src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java
Outdated
Show resolved
Hide resolved
| public static Place createPlace( | ||
| SavedPlaceInfo savedPlaceInfo | ||
| ) { | ||
| return Place.builder() | ||
| .placeKey(savedPlaceInfo.placeKey()) | ||
| .placeName(stripHtml(savedPlaceInfo.placeName())) | ||
| .placeCategory(savedPlaceInfo.category()) | ||
| .phone(savedPlaceInfo.telephone()) | ||
| .address(savedPlaceInfo.address()) | ||
| .naverPlaceUrl(savedPlaceInfo.link()) | ||
| .coordinate(savedPlaceInfo.coordinate()) | ||
| .build(); | ||
| } |
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.
static 메서드 제거 및 중복 HTML 제거 로직 정리
- 컨벤션 위반: 팀 컨벤션에 따라
static키워드를 제거하고 인스턴스 메서드로 변경해주세요. - 중복 처리: Line 49에서
stripHtml(savedPlaceInfo.placeName())을 호출하고 있지만,placeName은 이미toSavedPlaceInfo메서드의 line 30에서stripHtml가 적용된 상태입니다. 중복 호출을 제거해주세요.
🔎 수정 제안
- public static Place createPlace(
+ public Place createPlace(
SavedPlaceInfo savedPlaceInfo
) {
return Place.builder()
.placeKey(savedPlaceInfo.placeKey())
- .placeName(stripHtml(savedPlaceInfo.placeName()))
+ .placeName(savedPlaceInfo.placeName())
.placeCategory(savedPlaceInfo.category())
.phone(savedPlaceInfo.telephone())
.address(savedPlaceInfo.address())
.naverPlaceUrl(savedPlaceInfo.link())
.coordinate(savedPlaceInfo.coordinate())
.build();
}📝 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 static Place createPlace( | |
| SavedPlaceInfo savedPlaceInfo | |
| ) { | |
| return Place.builder() | |
| .placeKey(savedPlaceInfo.placeKey()) | |
| .placeName(stripHtml(savedPlaceInfo.placeName())) | |
| .placeCategory(savedPlaceInfo.category()) | |
| .phone(savedPlaceInfo.telephone()) | |
| .address(savedPlaceInfo.address()) | |
| .naverPlaceUrl(savedPlaceInfo.link()) | |
| .coordinate(savedPlaceInfo.coordinate()) | |
| .build(); | |
| } | |
| public Place createPlace( | |
| SavedPlaceInfo savedPlaceInfo | |
| ) { | |
| return Place.builder() | |
| .placeKey(savedPlaceInfo.placeKey()) | |
| .placeName(savedPlaceInfo.placeName()) | |
| .placeCategory(savedPlaceInfo.category()) | |
| .phone(savedPlaceInfo.telephone()) | |
| .address(savedPlaceInfo.address()) | |
| .naverPlaceUrl(savedPlaceInfo.link()) | |
| .coordinate(savedPlaceInfo.coordinate()) | |
| .build(); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java
around lines 44 to 56, change the static createPlace method to an instance
method (remove the static keyword) to follow team convention and remove the
redundant HTML stripping by using savedPlaceInfo.placeName() directly (since
stripHtml is already applied in toSavedPlaceInfo). Update any callers to invoke
the instance method on a PlaceMapper instance and ensure imports/tests are
adjusted accordingly.
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java
Outdated
Show resolved
Hide resolved
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "place_id") | ||
| private Place place; | ||
|
|
||
| @Column(name = "place_key", nullable = false) | ||
| private String placeKey; | ||
|
|
||
| @Column(name = "image_url", nullable = false, columnDefinition = "TEXT") | ||
| private String imageUrl; | ||
|
|
||
| public PlaceImages(String placeKey, String imageUrl) { | ||
| this.placeKey = placeKey; | ||
| this.imageUrl = imageUrl; | ||
| } |
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.
place 연관관계가 설정되지 않아 항상 null입니다.
PlaceImages 엔티티에 @ManyToOne 관계로 place 필드가 정의되어 있지만, 생성자에서 이 필드를 초기화하지 않습니다. 이로 인해 place_id 외래 키가 항상 null로 저장되어 Place와의 연관관계가 깨집니다.
🔎 수정 제안
- public PlaceImages(String placeKey, String imageUrl) {
+ public PlaceImages(Place place, String placeKey, String imageUrl) {
+ this.place = place;
this.placeKey = placeKey;
this.imageUrl = imageUrl;
}또는 place 필드가 불필요하다면 제거를 고려하세요:
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "place_id")
- private Place place;🤖 Prompt for AI Agents
In src/main/java/com/campus/campus/domain/place/domain/entity/PlaceImages.java
around lines 26 to 39, the PlaceImages constructor does not set the @ManyToOne
place field so the place_id FK remains null; update the constructor (or add an
overloaded constructor/factory) to accept a Place parameter and assign it to
this.place (and if Place has a collection of images, also add/remove this image
to that collection to keep the bidirectional relationship in sync), or if the
association is truly unused, remove the place field and its JPA annotations;
ensure changes maintain nullability and persistence behavior.
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapClient.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: 4
♻️ Duplicate comments (1)
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
97-98:@Transactional어노테이션이 동작하지 않습니다.
protected메서드에@Transactional을 적용했지만, 동일 클래스 내부(likePlace)에서 호출되므로 Spring 프록시를 거치지 않아 트랜잭션이 적용되지 않습니다.likePlace가 이미@Transactional이므로 이 메서드의 어노테이션은 제거하고private으로 변경하세요.🔎 수정 제안
-@Transactional -protected void migrateImagestoOci(String placeKey, List<String> imageUrls) { +private void migrateImagestoOci(String placeKey, List<String> imageUrls) {
🧹 Nitpick comments (4)
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (4)
46-46: 검색 결과 제한 값을 설정 가능하게 만드는 것을 고려하세요.검색 결과 제한이 하드코딩되어 있습니다. 이를 설정 파일이나 메서드 파라미터로 변경하면 유연성이 향상됩니다.
110-110: 컨텐츠 타입을 동적으로 결정하는 것을 고려하세요.
image/jpeg로 하드코딩되어 있으나, Google에서 제공하는 이미지가 PNG나 다른 형식일 수 있습니다. 가능하다면 이미지의 실제 타입을 감지하거나 URL에서 추출하여 사용하세요.
127-132: placeInfo 유효성 검증을 추가하세요.
placeInfo의 필수 필드(name, address, placeKey 등)에 대한 null 체크나 유효성 검증이 없습니다. 잘못된 데이터로 인한 런타임 오류를 방지하기 위해 검증 로직을 추가하세요.🔎 유효성 검증 추가 예시
@Transactional public LikeResponse likePlace(SavedPlaceInfo placeInfo, Long userId) { + if (placeInfo == null || placeInfo.placeKey() == null || placeInfo.placeKey().isBlank()) { + throw new IllegalArgumentException("Invalid place info"); + } + User user = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new);
140-140: 좋아요 취소 시 placeId 대신 null을 반환하는 것이 혼란스러울 수 있습니다.좋아요를 취소할 때
placeId를null로 반환하고 있습니다. 클라이언트 입장에서는 어떤 장소의 좋아요가 취소되었는지 알기 어려울 수 있습니다. 실제placeId를 반환하거나, 별도의 응답 구조를 사용하는 것을 고려하세요.🔎 수정 제안
if (likedPlace.isPresent()) { //이미 좋아요 상태->좋아요 취소 + Long placeId = likedPlace.get().getPlace().getPlaceId(); likedPlacesRepository.delete(likedPlace.get()); - return new LikeResponse(null, false); + return new LikeResponse(placeId, false); }
📜 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/PlaceService.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (2)
src/main/java/com/campus/campus/domain/user/application/exception/UserNotFoundException.java (1)
UserNotFoundException(5-9)src/main/java/com/campus/campus/global/oci/application/service/PresignedUrlService.java (1)
Slf4j(22-107)
⏰ 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/place/application/service/PlaceService.java (2)
1-43: 의존성 주입 및 구조가 적절합니다.서비스 계층의 구조와 의존성 주입이 Spring 모범 사례를 따르고 있습니다.
91-95: DB 조회 로직이 명확합니다.간단하고 명확한 이미지 URL 조회 구현입니다.
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
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java
Outdated
Show resolved
Hide resolved
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.
src/main/java/com/campus/campus/domain/place/domain/repository/PlaceRepository.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/infrastructure/GooglePlaceClient.java
Outdated
Show resolved
Hide resolved
...om/campus/campus/domain/place/application/dto/response/google/GooglePlaceDetailResponse.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/domain/entity/ImageSource.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/domain/entity/LikedPlace.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: 1
♻️ Duplicate comments (4)
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClientImpl.java (3)
50-53:limit파라미터가 여전히 무시되고 있습니다Line 51에서
.limit(3)이 하드코딩되어 있어, 메서드 파라미터로 전달된limit값이 사용되지 않습니다.🔎 수정 제안
List<String> imageUrls = photoRefs.stream() - .limit(3) + .limit(limit) .map(this::buildPhotoUrl) .toList();
55-55: 로그에 API 키 노출 위험
imageUrls를 로깅하면 각 URL에 포함된 Google API 키가 로그에 기록됩니다. 프로덕션 환경에서 보안 문제가 될 수 있습니다.🔎 수정 제안
-log.info("imageUrls: {}", imageUrls); +log.info("Fetched {} image URLs for place", imageUrls.size());
76-84: 빈 결과 리스트에 대한 IndexOutOfBoundsException 위험
response.results()가 비어있거나 null인 경우get(0)호출 시 예외가 발생합니다.🔎 수정 제안
if (response == null) { return null; } +if (response.results() == null || response.results().isEmpty()) { + return null; +} + String placeId = response.results().get(0).placeId();src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
48-72:response및response.items()에 대한 null 체크 필요Naver API 호출 결과가 null이거나
items()가 null/비어있는 경우 NPE 또는 빈 스트림 처리 문제가 발생할 수 있습니다.🔎 수정 제안
public List<SavedPlaceInfo> search(String keyword) { //네이버에서 특정 장소 기본정보 받아오기 NaverSearchResponse response = naverMapClient.searchPlaces(keyword, 5); + if (response == null || response.items() == null || response.items().isEmpty()) { + return List.of(); + } + return response.items().stream() .map(item -> {
🧹 Nitpick comments (2)
src/main/java/com/campus/campus/domain/place/domain/entity/LikedPlace.java (1)
21-44: 엔티티 설계가 잘 되어 있습니다.유니크 제약조건
(user_id, place_id)으로 중복 좋아요를 방지하고, LAZY fetch 타입으로 N+1 문제를 예방했습니다. BaseEntity 상속으로 감사(audit) 필드도 잘 관리됩니다.한 가지 제안:
@AllArgsConstructor에(access = AccessLevel.PROTECTED)를 추가하면 빌더 패턴만 사용하도록 강제할 수 있습니다.@Builder -@AllArgsConstructor +@AllArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED)src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
145-174: 이미지 마이그레이션 로직이 개선되었습니다.이전 리뷰 피드백이 잘 반영되었습니다:
- 개별 이미지 에러 처리 추가
- OCI URL(
presigned.imageUrl())이 DB에 저장됨- 불필요한
@Transactional제거됨내부에서만 호출되므로
protected를private로 변경하는 것이 좋습니다:-protected void migrateImagestoOci(String placeKey, List<String> imageUrls) { +private void migrateImagestoOci(String placeKey, List<String> imageUrls) {
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
src/main/java/com/campus/campus/domain/place/application/dto/response/google/GooglePhoto.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/google/GooglePlaceDetailResponse.javasrc/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.javasrc/main/java/com/campus/campus/domain/place/application/exception/PlaceCreationException.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/entity/LikedPlace.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/google/GooglePlaceClientImpl.javasrc/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapClient.java
🚧 Files skipped from review as they are similar to previous changes (5)
- src/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapClient.java
- src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java
- src/main/java/com/campus/campus/domain/place/application/dto/response/google/GooglePlaceDetailResponse.java
- src/main/java/com/campus/campus/domain/place/application/dto/response/google/GooglePhoto.java
- src/main/java/com/campus/campus/domain/place/domain/repository/PlaceRepository.java
🧰 Additional context used
🧬 Code graph analysis (5)
src/main/java/com/campus/campus/domain/place/application/exception/PlaceCreationException.java (4)
src/main/java/com/campus/campus/domain/council/application/exception/EmailAlreadyExistsException.java (1)
EmailAlreadyExistsException(5-9)src/main/java/com/campus/campus/global/common/exception/ApplicationException.java (3)
Getter(5-18)ApplicationException(9-12)ApplicationException(14-17)src/main/java/com/campus/campus/domain/councilnotice/application/exception/NoticeNotFoundException.java (1)
NoticeNotFoundException(5-9)src/main/java/com/campus/campus/domain/school/application/exception/CollegeNotFoundException.java (1)
CollegeNotFoundException(5-9)
src/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.java (4)
src/main/java/com/campus/campus/domain/user/domain/repository/UserRepository.java (1)
UserRepository(11-21)src/main/java/com/campus/campus/domain/school/domain/repository/CollegeRepository.java (1)
CollegeRepository(9-11)src/main/java/com/campus/campus/domain/school/domain/repository/SchoolRepository.java (1)
SchoolRepository(9-11)src/main/java/com/campus/campus/domain/school/domain/repository/MajorRepository.java (1)
MajorRepository(9-11)
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (3)
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/global/oci/application/service/PresignedUrlService.java (1)
Slf4j(22-107)
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClientImpl.java (2)
src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java (1)
Component(19-92)src/main/java/com/campus/campus/domain/place/infrastructure/naver/NaverMapClient.java (1)
Component(12-56)
src/main/java/com/campus/campus/domain/place/domain/entity/LikedPlace.java (1)
src/main/java/com/campus/campus/domain/place/domain/entity/Place.java (1)
Entity(17-57)
⏰ 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 (6)
src/main/java/com/campus/campus/domain/place/application/exception/PlaceCreationException.java (1)
5-9: LGTM!예외 클래스가 기존 패턴을 올바르게 따르고 있으며,
ApplicationException을 적절히 확장하고 중앙화된 에러 코드를 사용하고 있습니다.src/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.java (1)
1-23: 이전 리뷰 코멘트 관련: 개행 처리이전 리뷰에서 요청된 개행 처리가 현재 코드에는 적절히 반영된 것으로 보입니다. 코드 포맷팅이 올바르게 되어 있습니다.
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClientImpl.java (1)
129-144: 타임아웃 추가가 잘 적용되었습니다.이전 리뷰에서 지적된 타임아웃 누락 이슈가 10초 타임아웃 설정으로 해결되었습니다.
src/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.java (1)
9-14: LGTM!Spring Data JPA 쿼리 메서드 네이밍 컨벤션을 잘 따르고 있으며, 기존 리포지토리 패턴(MajorRepository, CollegeRepository 등)과 일관성이 있습니다.
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (2)
119-137: Race condition 처리가 잘 구현되었습니다.이전 리뷰에서 지적된 동시성 문제가
DataIntegrityViolationException처리와 재조회 패턴으로 잘 해결되었습니다.
160-164: 이슈 없음:uploadToOci메서드가PresignedUrlService에 존재합니다.
presignedUrlService.uploadToOci(presigned.uploadUrl(), bytes, "image/jpeg")는 올바른 메서드 호출입니다. 해당 메서드는PresignedUrlService.java의 114~118줄에 정의되어 있습니다.Likely an incorrect or invalid review comment.
| @Getter | ||
| @AllArgsConstructor | ||
| public enum ErrorCode implements ErrorCodeInterface { | ||
| ADDRESS_EMPTY(2600, HttpStatus.NO_CONTENT, "주소는 비어있을 수 없습니다."), |
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.
HTTP 상태 코드를 BAD_REQUEST로 변경해야 합니다.
ADDRESS_EMPTY는 클라이언트 입력 유효성 검증 실패를 나타내므로, HttpStatus.NO_CONTENT (204) 대신 HttpStatus.BAD_REQUEST (400)를 사용해야 합니다. NO_CONTENT는 성공적인 요청에 대해 반환할 본문이 없을 때 사용되는 상태 코드입니다.
🔎 제안하는 수정사항
- ADDRESS_EMPTY(2600, HttpStatus.NO_CONTENT, "주소는 비어있을 수 없습니다."),
+ ADDRESS_EMPTY(2600, HttpStatus.BAD_REQUEST, "주소는 비어있을 수 없습니다."),📝 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.
| ADDRESS_EMPTY(2600, HttpStatus.NO_CONTENT, "주소는 비어있을 수 없습니다."), | |
| ADDRESS_EMPTY(2600, HttpStatus.BAD_REQUEST, "주소는 비어있을 수 없습니다."), |
🤖 Prompt for AI Agents
In
src/main/java/com/campus/campus/domain/place/application/exception/ErrorCode.java
around line 13, the ADDRESS_EMPTY enum value uses HttpStatus.NO_CONTENT but
should represent a client validation failure; change its HttpStatus from
NO_CONTENT to HttpStatus.BAD_REQUEST so the enum entry becomes
ADDRESS_EMPTY(2600, HttpStatus.BAD_REQUEST, "주소는 비어있을 수 없습니다.").
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.
수고하셨습니다!!
마지막 리뷰 3개만 확인해주시면 감사하겠습니다!!
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.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: 1
♻️ Duplicate comments (2)
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (1)
48-72: 외부 API 호출에 대한 null 체크와 에러 처리가 여전히 누락되어 있습니다.Line 50의
naverMapClient.searchPlaces()와 Line 52의response.items()에 대한 null 체크가 없어 NPE 위험이 있습니다. 이전 리뷰에서 "Addressed"로 표시되었으나 코드에는 반영되지 않은 것으로 보입니다.🔎 수정 제안
public List<SavedPlaceInfo> search(String keyword) { //네이버에서 특정 장소 기본정보 받아오기 NaverSearchResponse response = naverMapClient.searchPlaces(keyword, 5); + + if (response == null || response.items() == null) { + return List.of(); + } return response.items().stream() .map(item -> { String name = placeMapper.stripHtml(item.title()); String address = item.roadAddress(); //placeKey 생성 String placeKey = PlaceKeyGenerator.generate(name, address); //구글 -> 이미지 가져오기 List<String> images = getPlaceImgs( placeKey, name, address ); //이미지도 함께 저장 return placeMapper.toSavedPlaceInfo(item, placeKey, images); }) .toList(); }src/main/java/com/campus/campus/domain/place/domain/entity/PlaceImages.java (1)
30-38: place 연관관계와 placeKey가 중복되어 있습니다.이 엔티티는
@ManyToOne Place place(FK)와String placeKey(중복된 키)를 동시에 가지고 있습니다.현재 코드 사용 패턴(
PlaceService.javaLine 167)을 보면placeMapper.createPlaceImages(placeKey, presigned.imageUrl())로Place엔티티 없이 생성하므로place필드는 항상 null이 되어place_idFK가 설정되지 않습니다.유사한 엔티티(
PostImage,NoticeImage)는 FK 관계만 유지하고 키를 중복 저장하지 않습니다. 다음 중 하나를 선택하세요:
- 권장:
place필드를 제거하고placeKey만 사용 (현재 사용 패턴에 맞음)Place엔티티를 생성자에 추가하고 양방향 관계를 제대로 유지🔎 수정 제안 (옵션 1 - place 필드 제거)
@Entity @Table(name = "place_images") @Getter @Builder @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class PlaceImages { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long placeImagesId; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "place_id") - private Place place; - @Column(name = "place_key", nullable = false) private String placeKey; @Column(name = "image_url", nullable = false, columnDefinition = "TEXT") private String imageUrl; }
🧹 Nitpick comments (2)
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java (2)
36-56: limit 파라미터가 사용되지 않고 하드코딩되어 있습니다.메서드 시그니처에서
int limit파라미터를 받지만, Line 51에서.limit(3)으로 하드코딩되어 있습니다. 파라미터를 사용하거나 제거하는 것이 일관성을 위해 좋습니다.🔎 수정 제안
public List<String> fetchImages(String name, String address, int limit) { String placeId = findPlaceId(name, address); if (placeId == null) { return List.of(); } //placee details -> photo reference List<String> photoRefs = getPhotoReferences(placeId); if (photoRefs.isEmpty()) { return List.of(); } //imageURL 생성 List<String> imageUrls = photoRefs.stream() - .limit(3) + .limit(limit) .map(this::buildPhotoUrl) .toList(); log.info("imageUrls: {}", imageUrls); return imageUrls; }
43-43: 주석에 오타가 있습니다."placee details" → "place details"
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/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/entity/PlaceImages.javasrc/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.javasrc/test/resources/application-test.yml
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.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/test/resources/application-test.yml
🧬 Code graph analysis (2)
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (3)
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/global/oci/application/service/PresignedUrlService.java (1)
Slf4j(22-107)
src/main/java/com/campus/campus/domain/place/domain/entity/PlaceImages.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/councilpost/domain/entity/PostImage.java (1)
Entity(18-36)src/main/java/com/campus/campus/domain/councilnotice/domain/entity/NoticeImage.java (1)
Entity(19-36)
🔇 Additional comments (8)
src/test/resources/application-test.yml (1)
36-43: 테스트 설정이 프로젝트 컨벤션을 잘 따르고 있습니다.Naver Maps API와 Google Places API를 위한 더미 설정값들이 올바르게 추가되었습니다. 테스트 환경에서 더미 값을 사용하는 것은 프로젝트의 기존 방식과 일치하며, 실제 API 키가 노출되지 않아 안전합니다.
Based on learnings, 이 프로젝트는 테스트 설정 파일에서 OAuth 및 JWT 시크릿과 같이 임의의 더미 값을 사용하는 것이 확립된 패턴입니다.
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java (3)
16-31: LGTM!WebClient 초기화 및 API 키 주입 방식이 적절합니다.
87-114: LGTM!Null 체크가 철저하게 구현되어 있고, 타임아웃도 적절히 설정되어 있습니다.
129-144: LGTM!이전 리뷰에서 지적된 타임아웃 누락 문제가 Line 137에 10초 타임아웃 추가로 해결되었습니다. 에러 처리도 적절합니다.
src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java (4)
34-47: LGTM!의존성 주입 구조가 명확하고 적절합니다.
92-99: LGTM!DB 조회 로직이 간결하고 명확합니다.
119-137: LGTM!
orElseGet패턴과DataIntegrityViolationException처리를 통한 동시성 제어가 잘 구현되어 있습니다.placeKey의 unique 제약 조건을 활용한 race condition 해결 방식이 적절합니다.
145-174: LGTM!개별 이미지 업로드 실패를 try-catch로 처리하여 부분 실패 시에도 나머지 이미지 처리를 계속하는 방식이 적절합니다. OCI URL 저장도 정확하게 구현되어 있습니다 (Line 167).
| private String findPlaceId(String name, String address) { | ||
| String query = name + " " + address; | ||
|
|
||
| GoogleTextSearchResponse response = webClient.get() | ||
| .uri(uriBuilder -> uriBuilder | ||
| .path("/textsearch/json") | ||
| .queryParam("query", query) | ||
| .queryParam("key", apiKey) | ||
| .build()) | ||
| .retrieve() | ||
| .bodyToMono(GoogleTextSearchResponse.class) | ||
| .timeout(Duration.ofSeconds(3)) | ||
| .block(); | ||
|
|
||
| if (response == null) { | ||
| return null; | ||
| } | ||
|
|
||
| String placeId = response.results().get(0).placeId(); | ||
| if (placeId == null || placeId.isBlank()) { | ||
| return null; | ||
| } | ||
| return 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.
IndexOutOfBoundsException 위험이 있습니다.
Line 80에서 response.results().get(0)를 호출하기 전에 results()가 비어있지 않은지 확인하지 않습니다. Google API가 빈 결과를 반환할 경우 예외가 발생합니다.
🔎 수정 제안
private String findPlaceId(String name, String address) {
String query = name + " " + address;
GoogleTextSearchResponse response = webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/textsearch/json")
.queryParam("query", query)
.queryParam("key", apiKey)
.build())
.retrieve()
.bodyToMono(GoogleTextSearchResponse.class)
.timeout(Duration.ofSeconds(3))
.block();
- if (response == null) {
+ if (response == null || response.results() == null || response.results().isEmpty()) {
return null;
}
String placeId = response.results().get(0).placeId();
if (placeId == null || placeId.isBlank()) {
return null;
}
return placeId;
}🤖 Prompt for AI Agents
In
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java
around lines 62 to 84, the code assumes response.results() has at least one
element which can throw IndexOutOfBoundsException; update the method to first
check that response is not null and response.results() is not null and not empty
(or use safe retrieval like findFirst on the list) before accessing index 0, and
return null (or handle appropriately) when results are empty or the first
result's placeId is null/blank; ensure any used collections are null-safe and
keep the existing timeout/error behavior.
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
♻️ Duplicate comments (2)
src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java (2)
58-70: 이전 리뷰에서 지적된 중복 HTML 제거 로직이 해결되지 않았습니다.Line 63에서
stripHtml(savedPlaceInfo.placeName())를 호출하고 있지만,placeName은 이미toSavedPlaceInfo메서드의 Line 44에서stripHtml가 적용된 상태입니다. 중복 호출을 제거해주세요.🔎 수정 제안
public Place createPlace( SavedPlaceInfo savedPlaceInfo ) { return Place.builder() .placeKey(savedPlaceInfo.placeKey()) - .placeName(stripHtml(savedPlaceInfo.placeName())) + .placeName(savedPlaceInfo.placeName()) .placeCategory(savedPlaceInfo.category()) .phone(savedPlaceInfo.telephone()) .address(savedPlaceInfo.address()) .naverPlaceUrl(savedPlaceInfo.link()) .coordinate(savedPlaceInfo.coordinate()) .build(); }
50-53: 좌표 파싱 시 예외 처리가 누락되었습니다.Lines 51-52에서
item.mapy()와item.mapx()를 파싱할 때NumberFormatException또는NullPointerException이 발생할 수 있습니다.buildNaverPlaceUrl메서드에서 동일한 값들을 파싱할 때는 예외 처리가 되어 있지만, 여기서는 처리되지 않아 일관성이 없습니다.🔎 수정 제안
public SavedPlaceInfo toSavedPlaceInfo(NaverSearchResponse.Item item, String placeKey, List<String> images) { - - return new SavedPlaceInfo( - stripHtml(item.title()), - placeKey, - item.address(), - item.category(), - buildNaverPlaceUrl(item), - item.telephone(), - Coordinate.fromNaver( - Double.parseDouble(item.mapy()), //위도 - Double.parseDouble(item.mapx()) //경도 - ), - images - ); + try { + return new SavedPlaceInfo( + stripHtml(item.title()), + placeKey, + item.address(), + item.category(), + buildNaverPlaceUrl(item), + item.telephone(), + Coordinate.fromNaver( + Double.parseDouble(item.mapy()), //위도 + Double.parseDouble(item.mapx()) //경도 + ), + images + ); + } catch (NumberFormatException | NullPointerException e) { + throw new NaverMapAPIException(); + } }
📜 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/mapper/PlaceMapper.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java (1)
src/main/java/com/campus/campus/domain/place/application/exception/NaverMapAPIException.java (1)
NaverMapAPIException(5-9)
| public String buildNaverPlaceUrl(NaverSearchResponse.Item item) { | ||
| try { | ||
| return String.format( | ||
| "https://map.naver.com/v5/search/%s?c=%f,%f,15,0,0,0,dh", | ||
| URLEncoder.encode(item.title(), StandardCharsets.UTF_8), | ||
| Double.parseDouble(item.mapy()), | ||
| Double.parseDouble(item.mapx()) | ||
| ); | ||
| } catch (Exception e) { | ||
| throw new NaverMapAPIException(); | ||
| } | ||
| } |
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.
이전 리뷰에서 지적된 HTML 태그 제거 문제가 해결되지 않았습니다.
Line 26에서 item.title()을 URL 인코딩하기 전에 HTML 태그를 제거해야 합니다. 현재 코드는 <b>카페</b> 같은 제목이 태그를 포함한 채로 인코딩됩니다. 또한 Line 30에서 일반적인 Exception을 잡는 것보다 구체적인 예외(NumberFormatException, NullPointerException)를 잡는 것이 더 명확합니다.
🔎 수정 제안
public String buildNaverPlaceUrl(NaverSearchResponse.Item item) {
try {
return String.format(
"https://map.naver.com/v5/search/%s?c=%f,%f,15,0,0,0,dh",
- URLEncoder.encode(item.title(), StandardCharsets.UTF_8),
+ URLEncoder.encode(stripHtml(item.title()), StandardCharsets.UTF_8),
Double.parseDouble(item.mapy()),
Double.parseDouble(item.mapx())
);
- } catch (Exception e) {
+ } catch (NumberFormatException | NullPointerException e) {
throw new NaverMapAPIException();
}
}🤖 Prompt for AI Agents
In
src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java
around lines 22 to 33, the title is URL-encoded without stripping HTML tags and
the catch block is too broad; first sanitize item.title() by removing HTML tags
(e.g., use item.title() == null ? "" : item.title().replaceAll("<[^>]*>", ""))
before calling URLEncoder.encode, and validate/handle nulls for mapx/mapy; then
replace the generic catch(Exception) with specific catches
(NumberFormatException and NullPointerException) and propagate the original
exception into the NaverMapAPIException (e.g., new NaverMapAPIException(e)) so
errors are explicit.
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: 3
♻️ Duplicate comments (1)
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java (1)
76-84: IndexOutOfBoundsException 위험이 여전히 존재합니다.Line 80에서
response.results().get(0)를 호출하기 전에results()가 null이 아니고 비어있지 않은지 확인하지 않습니다. Google API가 빈 결과를 반환하면 런타임 예외가 발생합니다.🔎 수정 제안
- if (response == null) { + if (response == null || response.results() == null || response.results().isEmpty()) { return null; } String placeId = response.results().get(0).placeId();
🧹 Nitpick comments (1)
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java (1)
24-31: 테스트 용이성을 위해 WebClient 주입을 고려하세요.생성자에서 WebClient를 직접 생성하고 있어 단위 테스트 시 외부 API 호출을 모킹하기 어렵습니다. WebClient 또는 WebClient.Builder를 주입받는 방식을 고려하세요.
🔎 개선 제안
public GooglePlaceClient( - @Value("${map.google.places.api-key}") String apiKey + @Value("${map.google.places.api-key}") String apiKey, + WebClient.Builder webClientBuilder ) { this.apiKey = apiKey; - this.webClient = WebClient.builder() + this.webClient = webClientBuilder .baseUrl(BASE_URL) .build(); }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java
🔇 Additional comments (3)
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java (3)
1-1: 클래스명은 현재 상태가 올바릅니다.이전 리뷰 코멘트에서 "GogglePlaceClient"로 변경을 제안했으나, 이는 오타로 보입니다. 현재의
GooglePlaceClient가 올바른 명명입니다.
87-114: LGTM!null/empty 체크가 철저하게 구현되어 있고, 안전하게 빈 리스트를 반환합니다.
116-124: LGTM!URL 생성 로직이 간결하고 명확합니다.
| /* | ||
| * 장소 이름 + 주소를 기준으로 google places에서 이미지 URL 목록을 가져옴 | ||
| */ | ||
| public List<String> fetchImages(String name, String address, int limit) { |
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.
limit 파라미터가 사용되지 않습니다.
메서드 시그니처에서 limit 파라미터를 받고 있지만, 실제로는 Line 51에서 하드코딩된 값 3을 사용하고 있습니다. 파라미터를 사용하거나 제거해야 합니다.
🔎 수정 제안
- List<String> imageUrls = photoRefs.stream()
- .limit(3)
- .map(this::buildPhotoUrl)
- .toList();
+ List<String> imageUrls = photoRefs.stream()
+ .limit(limit)
+ .map(this::buildPhotoUrl)
+ .toList();Also applies to: 51-51
🤖 Prompt for AI Agents
In
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java
around lines 36 and 51, the method fetchImages(String name, String address, int
limit) accepts a limit parameter but the implementation uses a hardcoded 3;
update the method to use the passed-in limit (replace the hardcoded 3 with the
limit variable), validate the limit (e.g., coerce to a sensible default or
enforce limit > 0 and a max cap) before use, and remove the parameter if you
decide not to support variable limits.
| return List.of(); | ||
| } | ||
|
|
||
| //placee details -> photo reference |
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.
주석의 오타를 수정하세요.
"placee"는 "place"의 오타입니다.
🔎 수정 제안
- //placee details -> photo reference
+ //place details -> photo reference🤖 Prompt for AI Agents
In
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java
around line 43, there is a typo in the inline comment ("placee" should be
"place"); update the comment to read "place details -> photo reference" (or
otherwise correct "placee" to "place") to fix the spelling error.
| } catch (Exception e) { | ||
| log.error("Google image download failed. url={}", imageUrl, e); | ||
| throw new IllegalStateException("Google image download failed"); |
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.
예외의 원인(cause)을 보존하세요.
IllegalStateException을 던질 때 원본 예외를 cause로 전달하지 않아 디버깅이 어려워질 수 있습니다.
🔎 수정 제안
} catch (Exception e) {
log.error("Google image download failed. url={}", imageUrl, e);
- throw new IllegalStateException("Google image download failed");
+ throw new IllegalStateException("Google image download failed", e);
}📝 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.
| } catch (Exception e) { | |
| log.error("Google image download failed. url={}", imageUrl, e); | |
| throw new IllegalStateException("Google image download failed"); | |
| public byte[] downloadImage(String imageUrl) { | |
| try { | |
| return webClient | |
| .get() | |
| .uri(imageUrl) | |
| .retrieve() | |
| .bodyToMono(byte[].class) | |
| .timeout(Duration.ofSeconds(10)) | |
| .block(); | |
| } catch (Exception e) { | |
| log.error("Google image download failed. url={}", imageUrl, e); | |
| throw new IllegalStateException("Google image download failed", e); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java
around lines 140 to 142, the catch block logs the error but throws a new
IllegalStateException without preserving the original exception as its cause;
change the throw to include the caught exception as the cause (e.g., new
IllegalStateException("Google image download failed: " + imageUrl, e)) so the
original stacktrace and root cause are retained for debugging.
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
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/main/resources/application-dev.ymlsrc/main/resources/application-prod.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/main/resources/application-dev.ymlsrc/main/resources/application-prod.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 (1)
src/main/resources/application-dev.yml (1)
58-64: API 키 설정이 올바르게 암호화되어 추가되었습니다.Naver Maps와 Google Places API 키 설정이 기존 파일의 암호화 패턴(ENC())을 따르고 있으며, YAML 구조도 올바릅니다. 다만 암호화된 값들이 정상적으로 복호화되고 유효한 API 자격 증명인지 확인이 필요합니다.
다음 사항을 확인해주세요:
- 애플리케이션 실행 시 암호화된 값들이 정상적으로 복호화되는지 확인
- NaverMapClient와 GooglePlaceClient에서
@Value("${map.naver.client-id}")등의 프로퍼티 경로가 이 설정과 일치하는지 확인/places/search엔드포인트를 호출하여 API 통합이 정상 작동하는지 테스트


🔀 변경 내용
장소 검색 후 해당 장소 관련 정보 (이미지 포함) 조회 후 DB에 저장하는 기능을 구현하였습니다.
google places 검색 후 이미지 URL을 DB에 저장하였습니다. OCI에 저장하는 이미지는 특정 유저가 해당 장소에 좋아요를 누르면 DB에 저장된 google URL을 OCI에 저장하는 식으로 구현할 예정입니다. google places API 호출 횟수를 최소화하기 위해 해당 방안을 채택하였는데, 더 좋은 아이디어 있으시면 말씀해주시면 감사하겠습니다..! (아직 좋아요는 구현 중이어서 /places/search 만 봐주시면 됩니다.)
✅ 작업 항목
📸 스크린샷 (선택)
📎 참고 이슈
관련 이슈 번호 #9
Summary by CodeRabbit
New Features
Chores
Style
✏️ Tip: You can customize this high-level summary in your review settings.