Skip to content

[FEAT] 프로필 사진 변경 & 푸드트럭 등록에 필요한 서류 업로드를 위한 presigned URL 발급 api 구현#52

Merged
buzz0331 merged 9 commits into
developfrom
feat/#41-presigned-url
Oct 29, 2025
Merged

[FEAT] 프로필 사진 변경 & 푸드트럭 등록에 필요한 서류 업로드를 위한 presigned URL 발급 api 구현#52
buzz0331 merged 9 commits into
developfrom
feat/#41-presigned-url

Conversation

@buzz0331

@buzz0331 buzz0331 commented Oct 25, 2025

Copy link
Copy Markdown
Contributor

#️⃣연관된 이슈

📝작업 내용

기존 presignedUrl과 같은 로직이라 별다른 특이사항은 없습니다.

원래 구현해두었던 presignedUrl 흐름이

  1. 이미지 갯수에 대한 유효성 검증
  2. 도메인에 따라 알맞은 s3 key 발급
  3. S3Service로부터 presigned url 발급
    이였는데, 이 흐름이 모든 도메인에 공통될 것이라고 판단되어 동작 파라미터화를 이용해서 리팩토링을 해보았습니다. PresignedUrlService 쪽 참고하시면 될 것 같습니다!

스크린샷 (선택)

💬리뷰 요구사항(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

Summary by CodeRabbit

  • 새로운 기능

    • 사용자 프로필 및 소유자 음식트럭 문서용 이미지 업로드 사전 서명 URL API 추가
  • 리팩토링

    • 이미지 사전 서명 URL 생성 로직을 중앙 서비스로 통합
    • 파일 키 생성 유틸리티 및 이미지 DTO 재구성
  • 문서

    • Swagger 응답 설명에 사전 서명 URL 항목 추가
  • 기타(Chores)

    • 즐겨찾기 응답에 메뉴 카테고리 포함 및 개발 환경의 스키마 자동화 설정 변경

@coderabbitai

coderabbitai Bot commented Oct 25, 2025

Copy link
Copy Markdown

Walkthrough

PresignedUrlService를 추가해 이미지 업로드용 presigned URL 생성 로직을 중앙화하고, ImageRequest/ImageResponse DTO의 패키지를 공통 storage 패키지로 이동했으며 사용자·오너용 presigned URL 엔드포인트와 KeyUtils의 키 생성 헬퍼를 추가했습니다.

Changes

Cohort / File(s) Change Summary
DTO 패키지 이동
src/main/java/konkuk/chacall/global/common/storage/dto/ImageRequest.java, src/main/java/konkuk/chacall/global/common/storage/dto/ImageResponse.java
ImageRequest/ImageResponse의 패키지 선언을 도메인 패키지에서 konkuk.chacall.global.common.storage.dto로 이동(네임스페이스 변경)
프리사인 URL 서비스 추가
src/main/java/konkuk/chacall/global/common/storage/presign/PresignedUrlService.java
새 서비스 추가: 이미지 수량 검증, 확장자 검증, S3 presign 생성 및 ImageResponse 반환을 담당하는 generatePresignedUrls 메서드 추가
KeyUtils 확장 및 시그니처 변경
src/main/java/konkuk/chacall/global/common/storage/util/KeyUtils.java
buildMenuImageKey 파라미터명 변경(foodTruckIduserId), buildUserProfileImageKeybuildFoodTruckDocumentImageKey 추가
서비스 리팩토링: FoodTruck 이미지
src/main/java/konkuk/chacall/domain/foodtruck/application/image/FoodTruckImageService.java
인라인 presign 로직 제거 및 PresignedUrlService 사용으로 대체, DTO 임포트 경로를 글로벌 DTO로 업데이트
사용자·오너 이미지 엔드포인트 추가
src/main/java/konkuk/chacall/domain/user/application/UserService.java, .../presentation/UserController.java, src/main/java/konkuk/chacall/domain/owner/application/OwnerService.java, .../presentation/OwnerController.java
사용자 프로필 및 오너 푸드트럭 문서용 presigned URL 생성 메서드 및 컨트롤러 엔드포인트 추가(presignedUrlService 의존성 주입, 상수 추가)
FoodTruckController/Service 임포트 확장
src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java, src/main/java/konkuk/chacall/domain/foodtruck/presentation/FoodTruckController.java
여러 DTO(FoodTruckMenuRequest 등) 및 글로벌 ImageRequest/ImageResponse 임포트 추가(기존 시그니처/로직 변경 없음)
PdfService 패키지 이동
src/main/java/konkuk/chacall/global/common/storage/pdf/PdfService.java
PdfService 패키지를 storagestorage.pdf로 이동, S3Service import 추가
Swagger/에러 코드 업데이트
src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java, src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java
Swagger 응답 항목(GET_USER_PROFILE_PRESIGEND_URL 등) 추가 및 ErrorCode에 Image 관련 주석 블록 추가(주석만)
응답 DTO 확장
src/main/java/konkuk/chacall/domain/member/presentation/dto/response/SavedFoodTruckResponse.java
List<String> menuCategories 필드 추가 및 팩토리 매핑 반영
엔티티 DDL 주석 → ColumnDefault
src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruck.java
canChangeViewedStatus@ColumnDefault("false") 추가(DDL 기본값 지정)
설정 변경
src/main/resources/application-dev.yml
Hibernate DDL auto를 create에서 update로 변경

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant UserController
    participant UserService
    participant PresignedUrlService
    participant S3Service

    Client->>UserController: POST /me/images (ImageRequest)
    UserController->>UserService: createUserImagePresignedUrl(request, userId)
    UserService->>UserService: 사용자 존재 확인
    UserService->>PresignedUrlService: generatePresignedUrls(request, userId, USER_PROFILE_IMAGE_MAX_COUNT, KeyUtils::buildUserProfileImageKey)
    PresignedUrlService->>PresignedUrlService: 수량/확장자 검증
    loop 각 확장자
        PresignedUrlService->>S3Service: presign 요청 (key)
        S3Service-->>PresignedUrlService: presignedUrl 및 publicUrl 반환
    end
    PresignedUrlService-->>UserService: ImageResponse
    UserService-->>UserController: ImageResponse
    UserController-->>Client: BaseResponse<ImageResponse>
Loading
sequenceDiagram
    participant Client
    participant OwnerController
    participant OwnerService
    participant PresignedUrlService
    participant S3Service

    Client->>OwnerController: POST /me/food-truck-documents/images (ImageRequest)
    OwnerController->>OwnerService: createFoodTruckDocumentPresignedUrls(request, ownerId)
    OwnerService->>OwnerService: 오너 존재 확인
    OwnerService->>PresignedUrlService: generatePresignedUrls(request, ownerUserId, 6, KeyUtils::buildFoodTruckDocumentImageKey)
    PresignedUrlService->>PresignedUrlService: 수량/확장자 검증
    loop 각 확장자
        PresignedUrlService->>S3Service: presign 요청 (key)
        S3Service-->>PresignedUrlService: presignedUrl 및 publicUrl 반환
    end
    PresignedUrlService-->>OwnerService: ImageResponse
    OwnerService-->>OwnerController: ImageResponse
    OwnerController-->>Client: BaseResponse<ImageResponse>
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • 집중 검토 항목:
    • PresignedUrlService의 확장자 검사와 예외(ErrorCode) 일관성
    • KeyUtils의 경로/파라미터 변경이 호출부에 미치는 영향(특히 menu 키)
    • DTO 패키지 이동으로 인한 import 누락·컴파일 오류 가능성
    • OwnerService/UserService에 추가된 DI 필드(빈 등록) 및 관련 단위 테스트

Possibly related PRs

Suggested reviewers

  • ksg1227

Poem

토끼가 말하네:
키를 짓고 URL을 빚어 S3 문을 두드리니 🐇🔑
반짝이는 링크가 열리고, 사진과 서류가 춤추네 ✨
프로필도 문서도 날아가요, 당근 하나면 충분해 🥕

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning PR에 presigned URL 기능 구현과 무관한 변경사항들이 포함되어 있습니다. SavedFoodTruckResponse에 menuCategories 필드가 추가되었는데, 이는 푸드트럭 응답 데이터 구조 변경으로 presigned URL 기능과 직접적인 관련이 없습니다. FoodTruck 엔티티의 canChangeViewedStatus 필드 어노테이션 변경도 데이터베이스 설정 변경으로 해당 기능과 무관합니다. 이러한 변경사항들은 이슈 #41의 정의된 목표 범위를 벗어나 있습니다. SavedFoodTruckResponse의 menuCategories 추가와 FoodTruck의 canChangeViewedStatus 어노테이션 변경을 별도의 PR로 분리하거나, 이러한 변경사항이 presigned URL 기능 구현에 필요한 변경사항임을 설명해 주시기 바랍니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed PR 제목 "[FEAT] 프로필 사진 변경 & 푸드트럭 등록에 필요한 서류 업로드를 위한 presigned URL 발급 api 구현"은 변경사항의 주요 목적을 명확하고 구체적으로 설명합니다. 제목은 사용자 프로필 이미지와 푸드트럭 문서 업로드를 위한 presigned URL API 구현이라는 핵심 기능을 정확히 반영하고 있으며, 간결하고 읽기 쉬운 형식으로 작성되어 있습니다. 팀원이 커밋 히스토리를 검토할 때 주요 변경사항을 쉽게 파악할 수 있습니다.
Linked Issues Check ✅ Passed 연결된 이슈 #41의 목표는 프로필 사진 변경 및 푸드트럭 등록에 필요한 파일 업로드를 위한 presigned URL 발급 API 구현입니다. PR의 변경사항은 이 목표를 충족합니다. UserService와 UserController에 프로필 이미지용 presigned URL API가 추가되었고 [#41], OwnerService와 OwnerController에 푸드트럭 문서용 presigned URL API가 추가되었습니다 [#41]. 또한 공통 로직을 중앙화하기 위해 PresignedUrlService가 도입되어 리팩토링 목표도 달성했습니다 [#41].
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#41-presigned-url

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 948dd84 and 39fc1fb.

📒 Files selected for processing (1)
  • src/main/resources/application-dev.yml (1 hunks)
🔇 Additional comments (1)
src/main/resources/application-dev.yml (1)

13-13: 개발 환경 DDL 자동 생성 전략 변경 확인

Hibernate DDL 자동 처리 방식이 create에서 update로 변경되었습니다. 이는 애플리케이션 재시작 시 기존 데이터를 보존하면서 스키마만 업데이트하는 방식으로, 개발 환경에서는 일반적으로 적절한 접근입니다.

다만 다음을 확인하기를 권장합니다:

  1. 마이그레이션 도구 사용 여부: 프로젝트에서 Flyway나 Liquibase와 같은 스키마 마이그레이션 도구를 사용 중이라면, spring.jpa.hibernate.ddl-autovalidate 또는 none으로 설정하여 Hibernate와 마이그레이션 도구의 충돌을 피해야 합니다.

  2. 다른 환경 설정과의 일관성: 프로덕션(prod) 및 스테이징(staging) 환경의 설정에서는 어떤 값이 사용되는지 확인하여 환경별 정책이 일관되고 의도된 것인지 검토해주세요.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
src/main/java/konkuk/chacall/domain/foodtruck/application/image/FoodTruckImageService.java (1)

18-25: 메뉴 이미지 업로드 최대 개수 정책 명시/적용 제안

FoodTruck 이미지는 9장 제한이 있으나 메뉴 이미지는 제한이 null입니다. 무제한이 의도라면 주석으로 정책을 명확히 남기고, 아니라면 상수(예: MAX_MENU_IMAGE_COUNT)로 제한값을 적용해 주세요.

src/main/java/konkuk/chacall/domain/user/presentation/UserController.java (1)

74-76: 상수명 철자 표준화(PRESIGNED)

GET_USER_PROFILE_PRESIGEND_URL의 “PRESIGEND” 철자를 “PRESIGNED”로 통일하면 가독성과 일관성이 좋아집니다. 상수 정의부 및 참조부 일괄 변경을 검토해 주세요.

src/main/java/konkuk/chacall/global/common/storage/presign/PresignedUrlService.java (1)

3-3: 사용하지 않는 import를 제거하세요.

User 클래스는 이 파일에서 사용되지 않습니다. 불필요한 import를 제거하여 코드를 깔끔하게 유지하세요.

다음 diff를 적용하세요:

-import konkuk.chacall.domain.user.domain.model.User;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f46c320 and 9c0e9b2.

📒 Files selected for processing (15)
  • src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java (1 hunks)
  • src/main/java/konkuk/chacall/domain/foodtruck/application/image/FoodTruckImageService.java (1 hunks)
  • src/main/java/konkuk/chacall/domain/foodtruck/presentation/FoodTruckController.java (1 hunks)
  • src/main/java/konkuk/chacall/domain/owner/application/OwnerService.java (4 hunks)
  • src/main/java/konkuk/chacall/domain/owner/presentation/OwnerController.java (2 hunks)
  • src/main/java/konkuk/chacall/domain/reservation/application/status/ReservationStatusService.java (1 hunks)
  • src/main/java/konkuk/chacall/domain/user/application/UserService.java (3 hunks)
  • src/main/java/konkuk/chacall/domain/user/presentation/UserController.java (2 hunks)
  • src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java (1 hunks)
  • src/main/java/konkuk/chacall/global/common/storage/dto/ImageRequest.java (1 hunks)
  • src/main/java/konkuk/chacall/global/common/storage/dto/ImageResponse.java (1 hunks)
  • src/main/java/konkuk/chacall/global/common/storage/pdf/PdfService.java (1 hunks)
  • src/main/java/konkuk/chacall/global/common/storage/presign/PresignedUrlService.java (1 hunks)
  • src/main/java/konkuk/chacall/global/common/storage/util/KeyUtils.java (1 hunks)
  • src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/konkuk/chacall/domain/foodtruck/application/image/FoodTruckImageService.java (2)
src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java (1)
  • RequiredArgsConstructor (22-69)
src/main/java/konkuk/chacall/global/common/storage/presign/PresignedUrlService.java (1)
  • Service (16-57)
src/main/java/konkuk/chacall/global/common/storage/presign/PresignedUrlService.java (2)
src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java (1)
  • RequiredArgsConstructor (22-69)
src/main/java/konkuk/chacall/domain/foodtruck/application/image/FoodTruckImageService.java (1)
  • Service (11-35)
🪛 GitHub Actions: CI with Gradle
src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java

[error] 8-8: ErrorCode is not abstract and does not override abstract method getMessage() in ResponseCode

src/main/java/konkuk/chacall/domain/owner/presentation/OwnerController.java

[error] 309-309: Cannot find symbol: SwaggerResponseDescription.OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGEND_URL.

🔇 Additional comments (15)
src/main/java/konkuk/chacall/domain/reservation/application/status/ReservationStatusService.java (1)

10-10: LGTM! 패키지 이동에 따른 import 경로 업데이트가 정확합니다.

PdfService가 pdf 하위 패키지로 이동한 것을 반영한 변경사항으로, 기능상 영향이 없습니다.

src/main/java/konkuk/chacall/domain/foodtruck/presentation/FoodTruckController.java (1)

8-8: LGTM! DTO 패키지 이동에 따른 import 경로 업데이트가 정확합니다.

ImageRequest와 ImageResponse가 도메인별 패키지에서 global storage dto 패키지로 중앙화된 것을 반영한 변경사항입니다.

Also applies to: 15-15

src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java (1)

9-9: LGTM! DTO 중앙화에 따른 import 업데이트가 올바릅니다.

ImageRequest/ImageResponse가 global storage dto 패키지로 이동한 것을 반영하여, 전체 코드베이스에서 일관된 DTO 참조가 가능합니다.

Also applies to: 13-13

src/main/java/konkuk/chacall/global/common/storage/pdf/PdfService.java (1)

1-1: LGTM! 패키지 구조 개선 및 명시적 import 추가가 적절합니다.

PdfService를 pdf 하위 패키지로 이동하여 storage 관련 서비스들의 모듈화가 개선되었습니다. S3Service import를 명시적으로 추가한 것도 코드 가독성 향상에 기여합니다.

Also applies to: 5-5

src/main/java/konkuk/chacall/global/common/storage/dto/ImageRequest.java (1)

1-1: LGTM! DTO의 글로벌 패키지 이동이 적절합니다.

ImageRequest를 도메인별 패키지에서 global storage dto 패키지로 이동한 것은 좋은 리팩토링입니다. 이 DTO가 여러 도메인(푸드트럭, 사용자 프로필 등)에서 공통으로 사용되므로, 중앙화를 통해 재사용성이 향상되고 코드 중복이 제거됩니다.

src/main/java/konkuk/chacall/global/common/storage/dto/ImageResponse.java (1)

1-1: LGTM! 응답 DTO의 글로벌 패키지 이동이 적절합니다.

ImageResponse를 global storage dto 패키지로 이동하여 ImageRequest와 함께 중앙화한 것은 일관성 있는 리팩토링입니다. presigned URL 응답 구조가 여러 도메인에서 동일하게 사용되므로, 글로벌 패키지 배치가 합리적입니다.

src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java (1)

32-36: LGTM! Swagger 응답 정의가 적절하게 추가되었습니다.

사용자 프로필 presigned URL 발급 엔드포인트에 대한 Swagger 응답 정의가 올바르게 추가되었습니다. 포함된 에러 코드들(USER_NOT_FOUND, INVALID_IMAGE_COUNT, INVALID_FILE_EXTENSION)은 해당 API의 검증 로직을 정확히 반영하며, 기존 패턴(GET_FOOD_TRUCK_PRESIGEND_URL 등)과도 일관성이 있습니다.

src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java (1)

82-84: 원래 리뷰 코멘트는 잘못되었습니다.

ErrorCode enum은 Lombok의 @Getter 어노테이션을 통해 ResponseCode 인터페이스에서 요구하는 getMessage()getCode() 메서드를 이미 구현하고 있습니다.

확인 결과:

  • message 필드 → getMessage() 자동 생성
  • code 필드 → getCode() 자동 생성
  • 코드베이스 전역에서 errorCode.getMessage()를 정상적으로 호출 중 (BusinessException, GlobalExceptionHandler, ErrorResponse 등)

PR의 변경사항(라인 82-84 Javadoc 주석 추가)은 순수 문서화이며, 컴파일 오류는 존재하지 않습니다.

Likely an incorrect or invalid review comment.

src/main/java/konkuk/chacall/domain/foodtruck/application/image/FoodTruckImageService.java (3)

15-16: 중앙 PresignedUrlService 위임 리팩터링 좋습니다.

의존성 주입과 공통 로직 위임으로 중복이 잘 제거되었습니다.


19-24: 리뷰 의견은 부정확합니다. 코드에는 문제가 없습니다.

검증 결과, PresignedUrlService의 필드명은 정상적으로 s3Service(카멜케이스)로 선언되어 있으며, 코드 전체에서 s3_service(언더스코어) 형태는 발견되지 않습니다. 의존성 주입도 @RequiredArgsConstructor로 정상 설정되어 있고, 실제 사용(line 43-44)도 올바릅니다. 컴파일 실패 가능성은 없습니다.

Likely an incorrect or invalid review comment.


28-33: 메뉴 이미지 키 파티셔닝 구조의 일관성 검토 필요

현재 구현을 검증한 결과, review comment의 우려는 타당합니다:

  • 실제 상황: Menu 도메인은 foodTruckId 기반으로 조직화되어 있으며, 메뉴 조회/수정/삭제 시 항상 foodTruckId로 필터링됩니다.
  • 불일치: 이미지 업로드는 userId 기반 파티셔닝 사용 (menus/{userId}/{timestamp}/{uuid})
  • 영향: 메뉴는 로직상 푸드트럭에 속하는 자원이므로, 저장소 구조도 foodTruckId 기반이 더 일관성 있을 수 있습니다.

변경 필요성 검토 사항:

  1. FoodTruckImageService.createMenuImagePresignedUrl 메서드가 현재 foodTruckId 파라미터를 받지 않음
  2. PresignedUrlService.generatePresignedUrlsuserId 기반만 지원하므로, 이 설정을 먼저 변경해야 함
  3. 변경 시 모든 관련 호출처(UserService, OwnerService 등) 함께 수정 필요

권장사항: 설계 회의에서 현재 userId 기반 통일이 의도적인지 확인하고, 필요시 foodTruckId 기반으로 점진적 마이그레이션을 검토하시기 바랍니다.

src/main/java/konkuk/chacall/domain/user/application/UserService.java (1)

54-64: 프로필 이미지 presigned URL 발급 로직 적절합니다.

존재 검증 후 최대 1장 제한과 키 빌더 적용이 명확합니다. 클래스 레벨 readOnly 트랜잭션과도 정합적입니다.

src/main/java/konkuk/chacall/domain/owner/application/OwnerService.java (2)

38-40: LGTM!

PresignedUrlService 의존성 추가와 푸드트럭 서류 이미지 최대 개수 상수 정의가 적절합니다. 상수명이 명확하고 값도 합리적입니다.


212-222: LGTM!

푸드트럭 서류 이미지용 presigned URL 생성 로직이 잘 구현되었습니다. 기존 패턴(검증 → 위임)을 따르고 있으며, FoodTruckImageService의 구현과도 일관성이 있습니다.

src/main/java/konkuk/chacall/global/common/storage/presign/PresignedUrlService.java (1)

44-55: LGTM!

스트림을 사용한 presigned URL 생성 로직이 깔끔하게 구현되었습니다. 각 확장자에 대해 URL을 생성하고 ImageInfo 객체로 매핑하는 흐름이 명확합니다.

@@ -1,5 +1,6 @@
package konkuk.chacall.domain.owner.application;

import jakarta.validation.Valid;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

@Valid import 사용 여부를 확인하세요.

이 PR의 변경사항에서 @Valid 어노테이션이 사용되는 것이 보이지 않습니다. 실제로 사용되지 않는다면 불필요한 import일 수 있습니다.

다음 스크립트로 해당 어노테이션의 사용 여부를 확인하세요:


🏁 Script executed:

#!/bin/bash
# Description: Check if @Valid annotation is actually used in OwnerService

rg -n "@Valid" src/main/java/konkuk/chacall/domain/owner/application/OwnerService.java

Length of output: 0


🏁 Script executed:

#!/bin/bash
# View the OwnerService.java file to confirm the import and check for any @Valid usage
cat src/main/java/konkuk/chacall/domain/owner/application/OwnerService.java | head -50

Length of output: 2396


🏁 Script executed:

#!/bin/bash
# Check full file and search for @Valid usage one more time to be thorough
wc -l src/main/java/konkuk/chacall/domain/owner/application/OwnerService.java
rg -n "@Valid" src/main/java/konkuk/chacall/domain/owner/application/OwnerService.java || echo "No @Valid found in file"

Length of output: 99


jakarta.validation.Valid import를 제거하세요.

파일의 223줄 전체를 검색한 결과, @Valid 어노테이션이 어디에도 사용되지 않습니다. 3번 줄의 import 문은 불필요하므로 삭제해야 합니다.

🤖 Prompt for AI Agents
src/main/java/konkuk/chacall/domain/owner/application/OwnerService.java lines 3:
remove the unused import "import jakarta.validation.Valid;" since a project-wide
search shows @Valid is not used anywhere in this file; delete that import line
(or run your IDE's optimize/organize imports) and rebuild to ensure no
compilation issues.

summary = "푸드트럭 관련 서류 업로드를 위한 presigned URL 발급",
description = "푸드트럭 관련 서류 업로드를 위한 presigned URL을 발급받습니다."
)
@ExceptionDescription(SwaggerResponseDescription.OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGEND_URL)

@coderabbitai coderabbitai Bot Oct 25, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

CI 컴파일 오류: SwaggerResponseDescription 상수 미존재

SwaggerResponseDescription.OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGEND_URL 심볼을 찾을 수 없어 CI가 실패합니다. 상수명 오타(PRESIGNED) 여부 확인 후 하나를 선택해 주세요.

  • 이미 존재하는 상수명으로 교체(권장): 예) OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGNED_URL
  • 또는 SwaggerResponseDescription에 동일 키를 신규 정의

빠른 수정을 위한 예시(교체 가정):

-@ExceptionDescription(SwaggerResponseDescription.OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGEND_URL)
+@ExceptionDescription(SwaggerResponseDescription.OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGNED_URL)

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Actions: CI with Gradle

[error] 309-309: Cannot find symbol: SwaggerResponseDescription.OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGEND_URL.

🤖 Prompt for AI Agents
In src/main/java/konkuk/chacall/domain/owner/presentation/OwnerController.java
around line 309, the annotation references a non-existent constant
SwaggerResponseDescription.OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGEND_URL due to a
typo; replace that reference with the correct existing constant name (e.g.,
OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGNED_URL) or alternatively add the exact
PRESIGEND key to SwaggerResponseDescription. Update the reference to the correct
constant name to match the enum/class definition, rebuild to confirm CI passes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 한 번만 확인부탁드립니다~

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check all constants in SwaggerResponseDescription.java
cat src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java | grep -E "^\s*(OWNER_|GET_)" | head -40

Length of output: 1504


🏁 Script executed:

#!/bin/bash
# Search for any FOOD_TRUCK_DOCUMENT related constants
rg -n "FOOD_TRUCK_DOCUMENT" --type=java

Length of output: 451


🏁 Script executed:

#!/bin/bash
# Check for PRESIGNED vs PRESIGEND typos in the codebase
rg -n "PRESIGEND" --type=java

Length of output: 1112


@ksg1227 네, 확인했습니다!

문제 확인:
OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGEND_URL 상수가 SwaggerResponseDescription.java에 정의되어 있지 않아 컴파일 오류가 발생하고 있습니다.

해결 방법:
SwaggerResponseDescription.java 파일에 해당 상수를 추가해야 합니다. 기존 패턴을 참고하여 다음과 같이 추가하시면 됩니다:

OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGEND_URL(new LinkedHashSet<>(Set.of(
    OWNER_NOT_FOUND,
    INVALID_IMAGE_COUNT,
    INVALID_FILE_EXTENSION
))),

참고 사항:
코드베이스 전반에 걸쳐 "PRESIGNED"가 아닌 "PRESIGEND"로 일관되게 사용되고 있습니다:

  • GET_USER_PROFILE_PRESIGEND_URL
  • GET_FOOD_TRUCK_PRESIGEND_URL
  • GET_MENU_PRESIGEND_URL

기존 코드와의 일관성을 위해 PRESIGEND 철자를 유지하는 것이 좋을 것 같습니다. 혹시 전체적으로 PRESIGNED로 수정하고 싶으시다면 별도로 알려주세요!


🧬 Code Graph Analysis Results

[src/main/java/konkuk/chacall/domain/owner/application/reservation/OwnerReservationService.java, lines 27-90]

@RequiredArgsConstructor
@Service
public class OwnerReservationService {

    private final ReservationRepository reservationRepository;
    private final UserRepository userRepository;

    public CursorPagingResponse<OwnerReservationHistoryResponse> getOwnerReservations(Long ownerId, ReservationViewType viewType, Long lastCursor, int pageSize) {
        // 예약 목록 조회
        Slice<Reservation> ownerReservationSlice = findReservations(ownerId, viewType.getStatuses(), lastCursor, pageSize);
        List<Reservation> ownerReservationList = ownerReservationSlice.getContent();

        // 손님 ID 리스트로 User 정보 한 번에 조회
        Map<Long, User> customerMap = getCustomerMap(ownerReservationList);

        // 미리 조회한 User 정보를 사용해 DTO 로 변환
        List<OwnerReservationHistoryResponse> responses = mapToReservationHistory(ownerReservationList, customerMap);

        return CursorPagingResponse.of(responses, OwnerReservationHistoryResponse::reservationId, ownerReservationSlice.hasNext());
    }

    public OwnerReservationDetailResponse getReservationDetail(Long ownerId, Long reservationId) {
        // ID로 예약 정보와 연관된 모든 데이터 한 번에 조회
        Reservation reservation = reservationRepository.findByIdWithDetails(reservationId)
                .orElseThrow(() -> new EntityNotFoundException(ErrorCode.RESERVATION_NOT_FOUND));

        reservation.validateFoodTruckOwner(ownerId);

        // DTO 로 변환하여 반환
        return OwnerReservationDetailResponse.of(reservation, reservation.getMember());
    }

    /**
     * 예약 목록 조회
     */
    private Slice<Reservation> findReservations(Long ownerId, Set<ReservationStatus> statuses, Long lastCursor, int pageSize) {
        return reservationRepository
                .findOwnerReservationsByStatusWithCursor(ownerId, statuses, lastCursor, PageRequest.of(0, pageSize));
    }

    /**
     * 예약 목록에 포함된 손님 정보 Map 조회
     */
    private Map<Long, User> getCustomerMap(List<Reservation> reservations) {
        List<Long> customerIds = reservations.stream()
                .map(reservation -> reservation.getMember().getUserId())
                .toList();

        return userRepository.findAllByUserIdInAndRoleAndStatus(customerIds, Role.MEMBER, BaseStatus.ACTIVE).stream()
                .collect(Collectors.toMap(User::getUserId, user -> user));
    }

    /**
     * 예약 목록과 손님 정보를 조합하여 DTO 리스트로 변환
     */
    private List<OwnerReservationHistoryResponse> mapToReservationHistory(List<Reservation> reservations, Map<Long, User> customerMap) {
        return reservations.stream()
                .map(reservation -> {
                    User customer = customerMap.get(reservation.getMember().getUserId());
                    return OwnerReservationHistoryResponse.of(reservation, customer);
                })
                .toList();
    }
}

[src/main/java/konkuk/chacall/domain/owner/application/myfoodtruck/MyFoodTruckService.java, lines 30-144]

@RequiredArgsConstructor
@Service
public class MyFoodTruckService {

    private final FoodTruckRepository foodTruckRepository;
    private final FoodTruckServiceAreaRepository foodTruckServiceAreaRepository;
    private final MenuRepository menuRepository;
    private final ReservationRepository reservationRepository;
    private final SavedFoodTruckRepository savedFoodTruckRepository;
    private final AvailableDateRepository availableDateRepository;
    private final RatingRepository ratingRepository;
    private final FoodTruckDocumentRepository foodTruckDocumentRepository;

    public CursorPagingResponse<MyFoodTruckResponse> getMyFoodTrucks(CursorPagingRequest request, Long ownerId) {
        // 1. 커서 기반으로 푸드트럭 Slice 조회
        Slice<FoodTruck> foodTruckSlice = findFoodTrucks(ownerId, request.cursor(), request.size());
        List<FoodTruck> foodTrucks = foodTruckSlice.getContent();

        // 2. 호출 가능 지역 정보 Map 조회
        Map<Long, List<FoodTruckServiceArea>> serviceAreaMap = getServiceAreaMap(foodTrucks);

        // 3. DTO 리스트 생성
        List<MyFoodTruckResponse> responses = mapToMyFoodTruckResponse(foodTrucks, serviceAreaMap);

        return CursorPagingResponse.of(responses, MyFoodTruckResponse::foodTruckId, foodTruckSlice.hasNext());
    }

    public void deleteMyFoodTruck(Long ownerId, Long foodTruckId) {
        // 푸드트럭 조회 및 소유권 확인
        FoodTruck foodTruck = foodTruckRepository.findById(foodTruckId)
                .orElseThrow(() -> new EntityNotFoundException(ErrorCode.FOOD_TRUCK_NOT_FOUND));

        foodTruck.validateOwner(ownerId);

        // 푸드트럭 호출 가능 지역 삭제
        foodTruckServiceAreaRepository.deleteAllByFoodTruckId(foodTruckId);

        // 푸드트럭 메뉴 삭제
        menuRepository.deleteAllByFoodTruckId(foodTruckId);

        // 푸드트럭 가능한 일정대 삭제
        availableDateRepository.deleteAllByFoodTruckId(foodTruckId);

        // 푸드트럭 관련 평점 삭제
        ratingRepository.deleteAllByFoodTruckId(foodTruckId);

        // 푸드트럭 관련 예약 삭제
        reservationRepository.deleteAllByFoodTruckId(foodTruckId);

        // 푸드트럭 저장한 푸드트럭 삭제
        savedFoodTruckRepository.deleteAllByFoodTruckId(foodTruckId);

        // 푸드트럭 삭제
        foodTruckRepository.delete(foodTruck);
    }

    /**
     * 커서 기반으로 페이징된 푸드트럭 목록 조회
     */
    private Slice<FoodTruck> findFoodTrucks(Long ownerId, Long lastCursor, int pageSize) {
        return foodTruckRepository.findByOwnerUserIdWithCursor(ownerId, lastCursor, PageRequest.of(0, pageSize));
    }

    /**
     * 푸드트럭 목록에 포함된 호출 가능 지역 정보 Map 조회
     */
    private Map<Long, List<FoodTruckServiceArea>> getServiceAreaMap(List<FoodTruck> foodTrucks) {
        List<Long> foodTruckIds = foodTrucks.stream()
                .map(FoodTruck::getFoodTruckId)
                .toList();

        List<FoodTruckServiceArea> serviceAreas = foodTruckServiceAreaRepository
                .findAllWithRegionByFoodTruckIdIn(foodTruckIds);

        return serviceAreas.stream()
                .collect(Collectors.groupingBy(sa -> sa.getFoodTruck().getFoodTruckId()));
    }

    public void updateFoodTruckViewedStatus(Long ownerId, Long foodTruckId, UpdateFoodTruckViewedStatusRequest request) {

        // 본인 소유인지, 푸드트럭이 승인 완료된 상태인지 검증
        FoodTruck foodTruck = foodTruckRepository.findByFoodTruckIdAndOwnerIdAndFoodTruckStatus(foodTruckId, ownerId, FoodTruckStatus.APPROVED)
                .orElseThrow(() -> new BusinessException(ErrorCode.FOOD_TRUCK_NOT_APPROVED));

        // 메뉴 표시 여부 변경 및 상태 전이 검증
        foodTruck.changeViewedStatus(request.status());
    }

    public void createNewFoodTruck(User owner, FoodTruckCreateRequest request) {
        // 1. 비어 있는 푸드트럭 엔티티 생성 (null 필드 초기화)
        FoodTruck foodTruck = FoodTruck.createEmptyFoodTruck(owner, request.name());
        foodTruckRepository.save(foodTruck);

        // 2. 서류 등록 로직 (사업자등록증 + 기타 5장)
        FoodTruckDocument business = FoodTruckDocument.create(foodTruck, DocumentType.BUSINESS_REGISTRATION, request.businessRegistrationUrl());
        foodTruckDocumentRepository.save(business);

        List<FoodTruckDocument> otherDocs = request.otherDocumentUrls().stream()
                .map(url -> FoodTruckDocument.create(foodTruck, DocumentType.OTHER, url))
                .toList();

        foodTruckDocumentRepository.saveAll(otherDocs);
    }

    private List<MyFoodTruckResponse> mapToMyFoodTruckResponse(List<FoodTruck> foodTrucks, Map<Long, List<FoodTruckServiceArea>> serviceAreaMap) {
        return foodTrucks.stream()
                .map(foodTruck -> {
                    List<FoodTruckServiceArea> serviceAreas = serviceAreaMap.getOrDefault(foodTruck.getFoodTruckId(), List.of());
                    return MyFoodTruckResponse.of(foodTruck, serviceAreas);
                })
                .toList();
    }


}

[src/main/java/konkuk/chacall/domain/owner/application/OwnerService.java, lines 28-223]

@RequiredArgsConstructor
@Service
@Transactional(readOnly = true)
public class OwnerService {

    private final BankAccountService bankAccountService;
    private final ChatTemplateService chatTemplateService;
    private final OwnerReservationService ownerReservationService;
    private final MyFoodTruckService myFoodTruckService;
    private final MyFoodTruckMenuService myFoodTruckMenuService;
    private final PresignedUrlService presignedUrlService;

    private static final int FOOD_TRUCK_DOCUMENT_IMAGE_MAX_COUNT = 6;

    // 파사드에서 사장님 검증을 거침으로써 서비스 로직에서는 사장님 검증에 신경쓰지 않도록 책임 분리
    private final OwnerValidator ownerValidator;

    @Transactional
    public void registerBankAccount(RegisterBankAccountRequest request, Long ownerId) {
        // 사장님인지 먼저 검증
        User owner = ownerValidator.validateAndGetOwner(ownerId);

        // 검증된 유저 정보를 넘겨 계좌 등록 로직 호출
        bankAccountService.registerBankAccount(request, owner);
    }

    public BankAccountResponse getBankAccount(Long ownerId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 계좌 조회 로직 호출
        return bankAccountService.getBankAccount(ownerId);
    }

    @Transactional
    public void updateBankAccount(Long ownerId, Long bankAccountId, UpdateBankAccountRequest request) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 계좌 수정 로직 호출
        bankAccountService.updateBankAccount(ownerId, bankAccountId, request);
    }

    @Transactional
    public void deleteBankAccount(Long ownerId, Long bankAccountId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 계좌 삭제 로직 호출
        bankAccountService.deleteBankAccount(ownerId, bankAccountId);
    }

    @Transactional
    public void registerChatTemplate(RegisterChatTemplateRequest request, Long ownerId) {
        // 사장님인지 먼저 검증
        User owner = ownerValidator.validateAndGetOwner(ownerId);

        // 자주 쓰는 채팅 등록 로직 호출
        chatTemplateService.registerChatTemplate(request, owner);
    }

    public List<ChatTemplateResponse> getChatTemplates(Long ownerId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 자주 쓰는 채팅 목록 조회 로직 호출
        return chatTemplateService.getChatTemplates(ownerId);
    }

    @Transactional
    public void updateChatTemplate(UpdateChatTemplateRequest request, Long ownerId, Long chatTemplateId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 자주 쓰는 채팅 수정 로직 호출
        chatTemplateService.updateChatTemplate(request, chatTemplateId);
    }

    @Transactional
    public void deleteChatTemplate(Long ownerId, Long chatTemplateId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 자주 쓰는 채팅 삭제 로직 호출
        chatTemplateService.deleteChatTemplate(chatTemplateId);
    }

    public CursorPagingResponse<OwnerReservationHistoryResponse> getOwnerReservations(GetReservationHistoryRequest request, Long ownerId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 예약 내역 조회 로직 호출
        CursorPagingRequest cursorPagingRequest = request.pagingOrDefault(SortType.NEWEST);
        return ownerReservationService.getOwnerReservations(ownerId, request.viewType(), cursorPagingRequest.cursor(), cursorPagingRequest.size());
    }

    public OwnerReservationDetailResponse getReservationDetail(Long ownerId, Long reservationId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 예약 내역 상세 조회 로직 호출
        return ownerReservationService.getReservationDetail(ownerId, reservationId);
    }

    public CursorPagingResponse<MyFoodTruckResponse> getMyFoodTrucks(CursorPagingRequest request, Long ownerId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 - 나의 푸드트럭 목록 조회 로직 호출
        CursorPagingRequest cursorPagingRequest = request.pagingOrDefault(SortType.NEWEST);
        return myFoodTruckService.getMyFoodTrucks(cursorPagingRequest, ownerId);
    }

    @Transactional
    public void deleteMyFoodTruck(Long ownerId, Long foodTruckId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 - 나의 푸드트럭 삭제 로직 호출
        myFoodTruckService.deleteMyFoodTruck(ownerId, foodTruckId);
    }

    @Transactional
    public void updateFoodTruckViewedStatus(Long ownerId, Long foodTruckId, UpdateFoodTruckViewedStatusRequest request) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 - 나의 푸드트럭 표시 상태 변경
        myFoodTruckService.updateFoodTruckViewedStatus(ownerId, foodTruckId, request);
    }

    public CursorPagingResponse<MyFoodTruckMenuResponse> getMyFoodTruckMenus(Long ownerId, Long foodTruckId, MyFoodTruckMenuRequest request) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 - 나의 푸드트럭 메뉴 조회
        return myFoodTruckMenuService.getMyFoodTruckMenus(ownerId, foodTruckId, request);
    }

    @Transactional
    public void registerMenu(Long ownerId, Long foodTruckId, RegisterMenuRequest request) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 - 나의 푸드트럭 메뉴 추가
        myFoodTruckMenuService.registerMenu(ownerId, foodTruckId, request);
    }

    @Transactional
    public void updateMenuStatus(Long ownerId, Long foodTruckId, Long menuId, UpdateMenuStatusRequest request) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 - 나의 푸드트럭 메뉴 상태 변경
        myFoodTruckMenuService.updateMenuStatus(ownerId, foodTruckId, menuId, request);
    }

    @Transactional
    public void updateMenu(Long ownerId, Long foodTruckId, Long menuId, UpdateMenuRequest request) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 - 나의 푸드트럭 메뉴 수정
        myFoodTruckMenuService.updateMenu(ownerId, foodTruckId, menuId, request);
    }

    @Transactional
    public void deleteMenu(Long ownerId, Long foodTruckId, Long menuId) {
        // 사장님인지 먼저 검증
        ownerValidator.validateAndGetOwner(ownerId);

        // 사장님 - 나의 푸드트럭 메뉴 삭제
        myFoodTruckMenuService.deleteMenu(ownerId, foodTruckId, menuId);
    }

    @Transactional
    public void createNewFoodTruck(Long ownerId, FoodTruckCreateRequest request) {
        // 사장님인지 먼저 검증
        User owner = ownerValidator.validateAndGetOwner(ownerId);

        // 푸드트럭 최초 등록
        myFoodTruckService.createNewFoodTruck(owner, request);
    }

    public ImageResponse createFoodTruckDocumentPresignedUrls(ImageRequest request, Long ownerId) {
        // 사장님인지 먼저 검증
        User owner = ownerValidator.validateAndGetOwner(ownerId);

        return presignedUrlService.generatePresignedUrls(
                request,
                owner.getUserId(),
                FOOD_TRUCK_DOCUMENT_IMAGE_MAX_COUNT,
                KeyUtils::buildFoodTruckDocumentImageKey
        );
    }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어라 왜 CI가 실패하나 봤더니 이것 때문이었군요. 수정하겠습니다~

Copy link
Copy Markdown

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!

Comment on lines +31 to +36
if (maxCountOrNull != null && extensions.size() > maxCountOrNull) {
throw new BusinessException(
ErrorCode.INVALID_IMAGE_COUNT,
new IllegalArgumentException("이미지 개수는 1 이상 " + maxCountOrNull + " 이하이어야 합니다. 입력: " + extensions.size())
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

검증 로직과 에러 메시지가 불일치합니다.

에러 메시지는 "이미지 개수는 1 이상"이라고 명시하지만, 실제 검증 로직은 최대값만 검증하고 최소값(1개 이상)은 검증하지 않습니다. 빈 리스트가 허용되는 것이 의도된 동작이라면 에러 메시지를 수정해야 하고, 그렇지 않다면 최소값 검증을 추가해야 합니다.

방안 1: 최소값 검증 추가 (빈 리스트를 허용하지 않는 경우)

     // 1) 개수 검증
+    if (extensions.isEmpty()) {
+        throw new BusinessException(
+                ErrorCode.INVALID_IMAGE_COUNT,
+                new IllegalArgumentException("이미지는 최소 1개 이상이어야 합니다.")
+        );
+    }
     if (maxCountOrNull != null && extensions.size() > maxCountOrNull) {

방안 2: 에러 메시지 수정 (빈 리스트를 허용하는 경우)

         throw new BusinessException(
                 ErrorCode.INVALID_IMAGE_COUNT,
-                new IllegalArgumentException("이미지 개수는 1 이상 " + maxCountOrNull + " 이하이어야 합니다. 입력: " + extensions.size())
+                new IllegalArgumentException("이미지 개수는 " + maxCountOrNull + " 이하이어야 합니다. 입력: " + extensions.size())
         );
📝 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.

Suggested change
if (maxCountOrNull != null && extensions.size() > maxCountOrNull) {
throw new BusinessException(
ErrorCode.INVALID_IMAGE_COUNT,
new IllegalArgumentException("이미지 개수는 1 이상 " + maxCountOrNull + " 이하이어야 합니다. 입력: " + extensions.size())
);
}
if (extensions.isEmpty()) {
throw new BusinessException(
ErrorCode.INVALID_IMAGE_COUNT,
new IllegalArgumentException("이미지는 최소 1개 이상이어야 합니다.")
);
}
if (maxCountOrNull != null && extensions.size() > maxCountOrNull) {
throw new BusinessException(
ErrorCode.INVALID_IMAGE_COUNT,
new IllegalArgumentException("이미지 개수는 1 이상 " + maxCountOrNull + " 이하이어야 합니다. 입력: " + extensions.size())
);
}
Suggested change
if (maxCountOrNull != null && extensions.size() > maxCountOrNull) {
throw new BusinessException(
ErrorCode.INVALID_IMAGE_COUNT,
new IllegalArgumentException("이미지 개수는 1 이상 " + maxCountOrNull + " 이하이어야 합니다. 입력: " + extensions.size())
);
}
if (maxCountOrNull != null && extensions.size() > maxCountOrNull) {
throw new BusinessException(
ErrorCode.INVALID_IMAGE_COUNT,
new IllegalArgumentException("이미지 개수는 " + maxCountOrNull + " 이하이어야 합니다. 입력: " + extensions.size())
);
}
🤖 Prompt for AI Agents
In
src/main/java/konkuk/chacall/global/common/storage/presign/PresignedUrlService.java
around lines 31-36, the thrown BusinessException message claims "이미지 개수는 1 이상
..." but the code only checks the maximum; either add a minimum check for
non-empty list or change the message to not state a minimum. To fix: if empty
lists should be rejected, add a check like if (extensions.isEmpty()) throw
BusinessException with ErrorCode.INVALID_IMAGE_COUNT and an
IllegalArgumentException stating the actual allowed range; otherwise, remove "1
이상" from the message so it only mentions the maximum (or reword to allow zero),
and ensure the ErrorCode used remains appropriate.

Comment on lines +44 to +46
var imageInfos = extensions.stream()
.map(extension -> {
final String keyWithExt = baseKey + "." + extension.toLowerCase();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

중복 확장자 처리에 대한 검증이 필요합니다.

현재 구현은 요청에 동일한 확장자가 여러 개 포함된 경우(예: ["jpg", "png", "jpg"]) 동일한 S3 키를 생성하여 나중에 생성된 URL이 이전 것을 덮어쓰게 됩니다. 요청 시 확장자 중복을 방지하는 검증 로직을 추가하거나, 각 파일에 고유 식별자를 부여하는 방안을 고려하세요.

다음 스크립트로 ImageRequest가 중복 확장자를 방지하는지 확인하세요:


🏁 Script executed:

#!/bin/bash
# Description: Check if ImageRequest validates unique extensions

# Find ImageRequest class definition and validation logic
ast-grep --pattern 'class ImageRequest {
  $$$
}'

# Also search for any validation related to duplicate extensions
rg -n -A 5 -B 5 "fileExtensions|ImageRequest" src/main/java/konkuk/chacall/global/common/storage/dto/ImageRequest.java

Length of output: 384


검증을 진행하기 위해 추가 확인이 필요합니다. 중복 확장자 검증 로직이 다른 곳에 있는지 확인하겠습니다.


🏁 Script executed:

#!/bin/bash
# Check for custom validators or additional validation logic

# Search for any custom validators on fileExtensions field
rg -n -B 2 -A 2 "fileExtensions" src/main/java/konkuk/chacall/global/common/storage/dto/ImageRequest.java

# Search for duplicate/unique validation in AllowedFileExtension class
rg -n -A 10 "checkAllowedExtension" src/main/java/konkuk/chacall/global/common/storage/presign/PresignedUrlService.java

# Search for any custom validator annotations that might check for duplicates
rg -n "@.*Valid|Duplicate|Unique" src/main/java/konkuk/chacall/global/common/storage/dto/ImageRequest.java

# Check the AllowedFileExtension utility class
fd -n "AllowedFileExtension" src/ --type f

# Search for controller or service level validation
rg -n -B 5 -A 10 "imageRequest|ImageRequest" src/main/java/konkuk/chacall/global/common/storage/presign/PresignedUrlService.java | head -50

Length of output: 2153


🏁 Script executed:

#!/bin/bash
# Find and inspect AllowedFileExtension class

fd AllowedFileExtension src/

# If found, show its implementation
fd AllowedFileExtension src/ -x cat {}

# Also check if .distinct() is used anywhere in the PresignedUrlService
rg -n "distinct|Stream|stream" src/main/java/konkuk/chacall/global/common/storage/presign/PresignedUrlService.java

Length of output: 1143


중복 확장자로 인한 S3 키 충돌 문제가 확인되었습니다.

검증 결과, AllowedFileExtension.checkAllowedExtension()은 파일 확장자의 허용 여부만 검증하며 중복을 검사하지 않습니다. 요청에 동일한 확장자가 포함된 경우(예: ["jpg", "png", "jpg"]) 현재 코드는 동일한 S3 키를 중복으로 생성하여 나중의 URL이 이전 것을 덮어씁니다.

해결 방안:

  • ImageRequest.fileExtensions에 중복 검증 로직 추가 (예: @UniqueElements 등), 또는
  • PresignedUrlService 44-46줄 스트림에 .distinct() 추가
🤖 Prompt for AI Agents
In
src/main/java/konkuk/chacall/global/common/storage/presign/PresignedUrlService.java
around lines 44-46, the code maps requested extensions to S3 keys without
removing duplicates, causing identical keys (and overwritten URLs) when the same
extension appears multiple times; fix by de-duplicating extensions before
mapping (e.g., add .distinct() to the stream chain) or alternatively enforce
uniqueness at the request level (add validation such as @UniqueElements to
ImageRequest.fileExtensions) so only unique extensions produce presigned URLs.

Comment on lines +33 to +40
public static String buildMenuImageKey(Long userId) {
String timestamp = LocalDateTime.now().format(DATE_TIME_FORMATTER); // ex) 20250923-163045
String uuid = UUID.randomUUID().toString();
return String.format("menus/%d/%s/%s",
foodTruckId,
userId,
timestamp,
uuid); // menus/{userId}/{timestamp}/{uuid}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

메뉴 이미지 키의 기준 ID 정책 확인 필요

현재 menus/{userId}/... 형태입니다. 메뉴가 푸드트럭 소속이라면 foodTruckId 기반으로 키를 만들도록 정책 재확인 부탁드립니다. 필요 시 메서드 분리 또는 주석으로 의도 명시 권장.


🏁 Script executed:

#!/bin/bash
# buildMenuImageKey 호출부 주변 인자 명 확인
rg -nC2 'KeyUtils\.buildMenuImageKey\(' --type=java

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Try broader search for buildMenuImageKey without strict dot notation
rg -n 'buildMenuImageKey' --type=java

Length of output: 268


🏁 Script executed:

#!/bin/bash
# Search for Menu entity to understand relationships
fd -type f -name '*Menu*.java' | head -20

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Search for foodTruckId in storage/key related code
rg -n 'foodTruckId' --type=java -C2 | head -40

Length of output: 4409


🏁 Script executed:

#!/bin/bash
# Check KeyUtils class for all methods
cat src/main/java/konkuk/chacall/global/common/storage/util/KeyUtils.java

Length of output: 2370


🏁 Script executed:

#!/bin/bash
# Check FoodTruckImageService context around line 32
cat src/main/java/konkuk/chacall/domain/foodtruck/application/image/FoodTruckImageService.java

Length of output: 1264


🏁 Script executed:

#!/bin/bash
# Find Menu entity
find . -name '*.java' -type f | xargs grep -l 'class Menu' | head -10

Length of output: 239


🏁 Script executed:

#!/bin/bash
# Search for menu image upload/save operations to understand relationship
rg -n 'buildMenuImageKey|saveMenuImage' --type=java -B5 -A5

Length of output: 2373


🏁 Script executed:

#!/bin/bash
# Check Menu entity definition
cat src/main/java/konkuk/chacall/domain/foodtruck/domain/model/Menu.java

Length of output: 2224


🏁 Script executed:

#!/bin/bash
# Search for menu operations to understand ownership model
rg -n 'class Menu|foodTruckId|new Menu' src/main/java/konkuk/chacall/domain/foodtruck/domain/model/Menu.java -A2

Length of output: 56


🏁 Script executed:

#!/bin/bash
# Check where createMenuImagePresignedUrl is called from
rg -n 'createMenuImagePresignedUrl' --type=java -B5 -A5

Length of output: 4894


메뉴 이미지 저장소 키 구조 설계 오류 - foodTruckId 기반으로 변경 필요

Menu 엔티티가 FoodTruck에 종속된 구조(@ManyToOne 관계)임에도, 현재 메뉴 이미지 키는 userId 기반으로 생성되고 있습니다. 메뉴는 사용자가 아닌 푸드트럭에 소속되므로, 저장소 키는 menus/{foodTruckId}/{timestamp}/{uuid} 형태로 변경되어야 합니다.

문제점:

  • createMenuImagePresignedUrl(ImageRequest request, User owner) 메서드가 foodTruckId 정보를 받지 않음
  • buildMenuImageKey 메서드 시그니처 수정 필요: buildMenuImageKey(Long foodTruckId)
  • FoodTruckImageService와 호출 경로 전체 검토 필요
🤖 Prompt for AI Agents
In src/main/java/konkuk/chacall/global/common/storage/util/KeyUtils.java around
lines 33 to 40, the menu image key is incorrectly built using userId; change the
API to use foodTruckId instead by renaming the method signature to
buildMenuImageKey(Long foodTruckId) and formatting the key as
"menus/{foodTruckId}/{timestamp}/{uuid}". Update any callers (including
createMenuImagePresignedUrl and FoodTruckImageService) to pass the foodTruckId
(add it to createMenuImagePresignedUrl parameters if missing), propagate the new
parameter through the call chain, and adjust tests and usages to reflect the new
key structure.

Comment on lines +51 to +57
public static String buildFoodTruckDocumentImageKey(Long userId) {
String timestamp = LocalDateTime.now().format(DATE_TIME_FORMATTER); // ex) 20250923-163045
String uuid = UUID.randomUUID().toString();
return String.format("foodtruck-documents/%d//%s/%s",
userId,
timestamp,
uuid); // menus/{foodTruckId}/{timestamp}/{uuid}
uuid); // foodtruck-documents/{userId}/{timestamp}/{uuid}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

버그: foodtruck-documents 키에 이중 슬래시 포함됨

String.format("foodtruck-documents/%d//%s/%s", ...) 에서 “//”로 인해 의도치 않은 키가 생성됩니다. 한 개의 슬래시로 수정해 주세요.

-        return String.format("foodtruck-documents/%d//%s/%s",
+        return String.format("foodtruck-documents/%d/%s/%s",
                 userId,
                 timestamp,
                 uuid); // foodtruck-documents/{userId}/{timestamp}/{uuid}
🤖 Prompt for AI Agents
In src/main/java/konkuk/chacall/global/common/storage/util/KeyUtils.java around
lines 51 to 57, the format string uses a double slash ("//") which produces an
unintended empty path segment; change the format pattern to use a single slash
so it becomes "foodtruck-documents/%d/%s/%s", ensuring the returned key follows
the intended foodtruck-documents/{userId}/{timestamp}/{uuid} structure.

ksg1227
ksg1227 previously approved these changes Oct 25, 2025

@ksg1227 ksg1227 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다! 수고하셨습니다~

추가적으로 현재 커밋 메세지를 확인해보면, 앞의 2개의 커밋에 대해 #49 로 이슈 번호가 명세되어있는 것으로 확인됩니다. 혹시 의도된 것인지 궁금합니다!

summary = "푸드트럭 관련 서류 업로드를 위한 presigned URL 발급",
description = "푸드트럭 관련 서류 업로드를 위한 presigned URL을 발급받습니다."
)
@ExceptionDescription(SwaggerResponseDescription.OWNER_GET_FOOD_TRUCK_DOCUMENT_PRESIGEND_URL)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 한 번만 확인부탁드립니다~

Comment on lines +18 to +28
public class PresignedUrlService {

private final S3Service s3Service;

public ImageResponse generatePresignedUrls(
ImageRequest request,
Long userId,
Integer maxCountOrNull,
Function<Long, String> baseKeyBuilder
) {
final List<String> extensions = request.fileExtensions();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여러 도메인별로 presignedUrl 을 따로따로 만들어줘야하다보니 이런 별도의 서비스로 분리하는 것 좋은 것 같습니다 굿굿

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c0e9b2 and 1470a2d.

📒 Files selected for processing (1)
  • src/main/java/konkuk/chacall/domain/member/presentation/dto/response/SavedFoodTruckResponse.java (3 hunks)
🔇 Additional comments (2)
src/main/java/konkuk/chacall/domain/member/presentation/dto/response/SavedFoodTruckResponse.java (2)

6-7: LGTM!

필요한 import가 올바르게 추가되었습니다.


17-18: 필드 추가 확인됨

menuCategories 필드가 적절한 Swagger 문서화와 함께 추가되었습니다. Record에 List 필드를 사용할 때는 불변성을 고려하는 것이 좋지만, 현재 구현은 정상 작동합니다.

@buzz0331

Copy link
Copy Markdown
Contributor Author

확인했습니다! 수고하셨습니다~

추가적으로 현재 커밋 메세지를 확인해보면, 앞의 2개의 커밋에 대해 #49 로 이슈 번호가 명세되어있는 것으로 확인됩니다. 혹시 의도된 것인지 궁금합니다!

허헣,, 사실 49번 이슈 브랜치를 수정하다가 뒤에 이슈 번호를 수정하지 않고 커밋했습니다,, 딱 들켰네요 😅
담부터 제대로 확인하고 커밋하겠습니다,,

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java (1)

32-36: 구현이 올바르며 에러 코드 조합이 적절합니다.

프로필 사진 업로드 시나리오에 필요한 에러 코드들이 적절히 포함되어 있습니다.

참고사항: 상수명에 오타가 있습니다 (PRESIGENDPRESIGNED). 다만 이는 기존 코드(line 218, 224 등)에서도 동일하게 사용되고 있는 패턴이므로, 별도의 리팩토링 작업으로 일괄 수정하는 것을 고려해볼 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1470a2d and 4edc8e8.

📒 Files selected for processing (1)
  • src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java (2 hunks)
⏰ 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/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java (1)

137-142: 구현이 올바르며 에러 코드 조합이 적절합니다.

푸드트럭 서류 업로드를 위한 Owner 권한 검증과 파일 유효성 검사에 필요한 에러 코드들이 적절히 포함되어 있습니다. 기존 GET_FOOD_TRUCK_PRESIGEND_URL (line 218)과 동일한 에러 코드 세트를 사용하고 있어 일관성이 유지됩니다.

ksg1227
ksg1227 previously approved these changes Oct 26, 2025
…-url

# Conflicts:
#	src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java
#	src/main/java/konkuk/chacall/domain/foodtruck/application/image/FoodTruckImageService.java
#	src/main/java/konkuk/chacall/domain/foodtruck/presentation/FoodTruckController.java
#	src/main/java/konkuk/chacall/domain/member/presentation/dto/response/SavedFoodTruckResponse.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruck.java (1)

48-50: 데이터베이스 기본값 추가는 좋지만, nullable 제약 조건도 추가하는 것을 권장합니다.

@ColumnDefault("false")를 추가한 것은 DB 레벨에서 기본값을 보장하는 좋은 개선입니다. 다만 다른 유사한 필드들(foodTruckStatus, foodTruckViewedStatus)과의 일관성을 위해 @Column(nullable = false)도 함께 추가하는 것을 권장합니다.

다음 diff를 적용하여 일관성을 개선할 수 있습니다:

     @Builder.Default
     @ColumnDefault("false")
+    @Column(nullable = false)
     private Boolean canChangeViewedStatus = false;

이렇게 하면:

  • 애플리케이션 레벨 (@Builder.Default, 필드 초기화)
  • 데이터베이스 레벨 (@ColumnDefault, nullable = false)

모든 레벨에서 null 값을 방지하고 기본값을 보장할 수 있습니다.

src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java (1)

6-15: 불필요한 중복 import를 제거하세요.

6-8번 라인과 10-12번 라인에서 명시적으로 import한 FoodTruck 관련 DTO들이 14-15번 라인의 wildcard import(.*)와 중복됩니다. Wildcard import가 이미 해당 패키지의 모든 클래스를 포함하므로, 명시적 import는 불필요합니다.

한편, ImageRequestImageResponseglobal.common.storage.dto 패키지로 이동시킨 리팩토링은 재사용성 측면에서 좋은 개선입니다.

다음과 같이 중복 import를 제거하는 것을 권장합니다:

-import konkuk.chacall.domain.foodtruck.presentation.dto.request.FoodTruckMenuRequest;
-import konkuk.chacall.domain.foodtruck.presentation.dto.request.FoodTruckNameDuplicateCheckRequest;
-import konkuk.chacall.domain.foodtruck.presentation.dto.request.FoodTruckSearchRequest;
 import konkuk.chacall.global.common.storage.dto.ImageRequest;
-import konkuk.chacall.domain.foodtruck.presentation.dto.response.FoodTruckMenuResponse;
-import konkuk.chacall.domain.foodtruck.presentation.dto.response.FoodTruckNameDuplicateCheckResponse;
-import konkuk.chacall.domain.foodtruck.presentation.dto.response.FoodTruckResponse;
 import konkuk.chacall.global.common.storage.dto.ImageResponse;
 import konkuk.chacall.domain.foodtruck.presentation.dto.request.*;
 import konkuk.chacall.domain.foodtruck.presentation.dto.response.*;
src/main/java/konkuk/chacall/domain/foodtruck/application/image/FoodTruckImageService.java (1)

22-27: 필드 선언 순서를 개선하면 가독성이 향상됩니다.

Java 코딩 컨벤션에 따라 상수(MAX_FOOD_TRUCK_IMAGE_COUNT)는 일반적으로 인스턴스 필드보다 먼저 선언됩니다.

다음 diff를 적용하여 필드 순서를 개선할 수 있습니다:

-    private final PresignedUrlService presignedUrlService;
-    private static final int MAX_FOOD_TRUCK_IMAGE_COUNT = 9;
-
-    private final FoodTruckRepository foodTruckRepository;
-    private final S3Service s3Service;
-    private final CdnUrlResolver cdnUrlResolver;
+    private static final int MAX_FOOD_TRUCK_IMAGE_COUNT = 9;
+
+    private final PresignedUrlService presignedUrlService;
+    private final FoodTruckRepository foodTruckRepository;
+    private final S3Service s3Service;
+    private final CdnUrlResolver cdnUrlResolver;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4edc8e8 and 948dd84.

📒 Files selected for processing (7)
  • src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java (1 hunks)
  • src/main/java/konkuk/chacall/domain/foodtruck/application/image/FoodTruckImageService.java (1 hunks)
  • src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruck.java (2 hunks)
  • src/main/java/konkuk/chacall/domain/foodtruck/presentation/FoodTruckController.java (1 hunks)
  • src/main/java/konkuk/chacall/domain/member/presentation/dto/response/SavedFoodTruckResponse.java (3 hunks)
  • src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java (1 hunks)
  • src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/konkuk/chacall/domain/foodtruck/presentation/FoodTruckController.java
  • src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java
  • src/main/java/konkuk/chacall/domain/member/presentation/dto/response/SavedFoodTruckResponse.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/konkuk/chacall/domain/foodtruck/application/image/FoodTruckImageService.java (2)
src/main/java/konkuk/chacall/domain/foodtruck/application/FoodTruckService.java (1)
  • RequiredArgsConstructor (24-81)
src/main/java/konkuk/chacall/global/common/storage/presign/PresignedUrlService.java (1)
  • Service (16-57)
🔇 Additional comments (5)
src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java (1)

87-89: LGTM! 코드 구성이 개선되었습니다.

이미지 관련 에러 코드를 명확하게 그룹화하는 문서화 주석을 추가했습니다. 파일 내 다른 도메인 섹션들과 일관된 패턴을 따르고 있어 가독성과 유지보수성이 향상되었습니다.

src/main/java/konkuk/chacall/domain/foodtruck/domain/model/FoodTruck.java (1)

10-10: LGTM! 필요한 임포트가 정확히 추가되었습니다.

Hibernate의 @ColumnDefault 어노테이션 사용을 위해 올바른 임포트가 추가되었습니다.

src/main/java/konkuk/chacall/domain/foodtruck/application/image/FoodTruckImageService.java (3)

9-12: 리팩토링이 올바르게 적용되었습니다.

DTO를 글로벌 storage 패키지로 이동하고 중앙화된 PresignedUrlService를 추가한 것이 PR 목표와 일치합니다.


29-36: 중앙화된 서비스로의 위임이 깔끔하게 구현되었습니다.

PresignedUrlService를 활용하여 검증 로직과 URL 생성 로직을 재사용하고, 푸드트럭 이미지 개수를 9개로 제한하는 것이 명확합니다.


38-45: 메뉴 이미지 개수 제한이 없는 것이 의도대로 구현되었습니다.

maxCountOrNullnull을 전달하여 메뉴 이미지는 개수 제한 없이 업로드할 수 있도록 구현되었습니다. 중앙화된 서비스 활용이 적절합니다.

@buzz0331 buzz0331 merged commit 11a880e into develop Oct 29, 2025
2 checks passed
@buzz0331 buzz0331 deleted the feat/#41-presigned-url branch October 29, 2025 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 프로필 사진 변경 & 푸드트럭 등록에 필요한 서류 업로드를 위한 presigned URL 발급 api 구현

2 participants