Skip to content

feat: 팀 서비스 이미지 관리 기능 추가 - #218

Merged
eeun1225 merged 4 commits into
devfrom
feat/team-images
Jun 24, 2026
Merged

eeun1225 merged 4 commits into
devfrom
feat/team-images

Conversation

@eeun1225

@eeun1225 eeun1225 commented May 31, 2026 •

Copy link
Copy Markdown
Contributor

📌 Related Issue

🚨 해결하려는 문제가 무엇인가요?

  • 팀 서비스에 이미지를 연결해 관리할 수 있는 구조가 없었습니다.

⭐️ 어떻게 해결했나요?

OCI Object Storage 기반 이미지 저장 구조를 도입해 팀 서비스 이미지를 안정적으로 관리할 수 있도록 했습니다.

  • OCI Object Storage 연동 (Instance Principal 기반 인증)
  • 팀 서비스 이미지 저장을 위한 TeamServiceImageEntity를 추가했습니다.
  • 어드민 팀 서비스 생성/수정/삭제 시 썸네일 이미지를 함께 처리하도록 했습니다.
    • 생성: 썸네일 업로드 후 DB 저장
    • 수정: 신규 업로드 시 기존 이미지 교체 + 이전 이미지 삭제
    • 삭제: 연결된 이미지 삭제
  • 트랜잭션 안정성 보장
    • rollback 시 업로드된 신규 이미지 자동 삭제
    • commit 이후 이전 이미지 삭제 처리
  • 어드민 서비스 상세, 역대 서비스 목록/상세 응답에 thumbnailImageUrl을 포함했습니다.

🤔 어떤 부분에 집중하여 리뷰해야 할까요?

🗒️ 참고자료

RCA 룰

  • R: 꼭 반영해 주세요. 적극적으로 고려해 주세요. (Request changes)
  • C: 웬만하면 반영해 주세요. (Comment)
  • A: 반영해도 좋고 넘어가도 좋습니다. 그냥 사소한 의견입니다. (Approve)

Summary by CodeRabbit

릴리스 노트

  • 새 기능
    • 팀 서비스에 썸네일 이미지 업로드 기능 추가
    • 관리자 서비스 생성/수정 시 썸네일 이미지 함께 처리 가능
    • 역대 서비스 목록 및 상세 페이지에 썸네일 이미지 URL 표시
    • 파일 업로드 크기 제한 설정 (최대 10MB)

@eeun1225 eeun1225 self-assigned this May 31, 2026
@eeun1225 eeun1225 added the 🖌️ feature New feature or request label May 31, 2026
@coderabbitai

coderabbitai Bot commented May 31, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

팀 서비스에 썸네일 이미지 기능을 추가한다. OCI Object Storage 인프라(인터페이스, 구현체, 트랜잭션 동기화 삭제)를 도입하고, team_service_images 테이블과 관련 영속 계층을 신설한다. 어드민 API를 multipart/form-data로 전환하고 역대 서비스 응답에 썸네일 URL을 반영하며 통합 테스트를 확장한다.

Changes

팀 서비스 썸네일 이미지 및 OCI 객체 스토리지 연동

Layer / File(s) Summary
OCI SDK 의존성 및 환경별 배포 설정
gradle/libs.versions.toml, build.gradle.kts, src/main/resources/application-dev-oci.yaml, src/main/resources/application-prod-oci.yaml, src/main/resources/application-local.yaml, src/main/resources/application-test.yaml, .github/workflows/yappu-oci-*.yaml
Gradle에 OCI Object Storage SDK와 Jersey3 의존성을 추가하고, 환경별 YAML에 multipart 크기 제한 및 object-storage 설정 블록을 구성하며 CD 워크플로우에 시크릿 주입 항목을 추가한다.
ObjectStorageManager 인터페이스, OCI 구현체, 트랜잭션 동기화 삭제
src/main/kotlin/co/yappuworld/external/storage/ObjectStorageManager.kt, src/main/kotlin/co/yappuworld/external/storage/OciObjectStorageProperties.kt, src/main/kotlin/co/yappuworld/external/storage/OciObjectStorageConfig.kt, src/main/kotlin/co/yappuworld/external/storage/OciObjectStorageManager.kt, src/main/kotlin/co/yappuworld/external/storage/MockObjectStorageManager.kt, src/main/kotlin/co/yappuworld/external/storage/ObjectStorageTransactionSynchronizer.kt
ObjectStorageManager 인터페이스와 설정 바인딩을 정의하고, object-storage.enabled 조건에 따른 OCI/Mock 구현체를 추가한다. ObjectStorageTransactionSynchronizer로 트랜잭션 커밋/롤백 이후 object key 삭제를 등록하는 흐름을 구현한다.
team_service_images 스키마, 엔티티, 리포지토리, find/command 서비스
src/main/resources/db/migration/V6__add_team_service_image_table.sql, src/main/resources/schema.sql, src/main/kotlin/co/yappuworld/team/infrastructure/entity/TeamServiceImageEntity.kt, src/main/kotlin/co/yappuworld/team/infrastructure/jpa/TeamServiceImageRepository.kt, src/main/kotlin/co/yappuworld/team/infrastructure/TeamServiceImageFindService.kt, src/main/kotlin/co/yappuworld/team/infrastructure/TeamServiceImageCommandService.kt
DB 마이그레이션과 schema.sql에 team_service_images 테이블을 추가하고, TeamServiceImageEntity JPA 엔티티와 리포지토리(썸네일 조회/프로젝션 쿼리 포함), 조회/커맨드 서비스를 신설한다.
어드민 썸네일 업로드/수정/삭제 플로우
src/main/kotlin/co/yappuworld/team/domain/vo/TeamError.kt, src/main/kotlin/co/yappuworld/team/client/dto/request/AdminTeamServiceUpdateRequest.kt, src/main/kotlin/co/yappuworld/team/client/dto/response/AdminTeamServiceDetailResponse.kt, src/main/kotlin/co/yappuworld/team/client/presentation/AdminTeamServiceApi.kt, src/main/kotlin/co/yappuworld/team/client/presentation/AdminTeamServiceController.kt, src/main/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageService.kt
TeamError에 이미지 검증 오류 코드를 추가하고, 요청/응답 DTO에 썸네일 필드를 확장한다. 어드민 API를 multipart/form-data로 전환하며, 관리 서비스에 썸네일 생성/갱신/삭제 헬퍼와 removeThumbnail 플래그 기반 분기 로직을 구현한다.
AdminTeamService 팀 삭제 시 이미지 연동 삭제
src/main/kotlin/co/yappuworld/team/client/application/AdminTeamService.kt
생성자에 이미지 관련 서비스와 ObjectStorageTransactionSynchronizer를 추가하고, deleteTeam에서 deleteTeamServices로 위임해 서비스 이미지 엔티티를 먼저 삭제한 뒤 커밋 이후 object key 삭제를 등록하도록 흐름을 변경한다.
역대 서비스 목록/상세 썸네일 URL 반영
src/main/kotlin/co/yappuworld/team/client/dto/response/HistoricalServicePageResponse.kt, src/main/kotlin/co/yappuworld/team/client/dto/response/HistoricalServiceDetailResponse.kt, src/main/kotlin/co/yappuworld/team/client/application/HistoricalServiceService.kt, src/main/kotlin/co/yappuworld/team/client/presentation/HistoricalServiceApi.kt
역대 서비스 응답 DTO 팩토리 메서드에 thumbnailImageUrl 인자를 추가하고, 서비스 계층에서 썸네일 object key를 일괄 조회해 public URL로 변환 후 응답에 포함하도록 변경한다. Swagger 예시도 실제 URL로 갱신한다.
픽스처, 가짜 스토리지, 통합 테스트 확장
src/test/kotlin/co/yappuworld/support/fixture/TeamFixture.kt, src/test/kotlin/co/yappuworld/support/storage/FakeObjectStorageManager.kt, src/test/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageServiceTest.kt, src/test/kotlin/co/yappuworld/team/client/application/AdminTeamServiceTest.kt, src/test/kotlin/co/yappuworld/team/client/application/HistoricalServiceServiceTest.kt
TeamServiceImageEntity 픽스처와 업로드/삭제 키를 추적하는 FakeObjectStorageManager를 추가한다. 어드민 서비스 관리 테스트에서 썸네일 정상/오류/롤백 시나리오를 검증하고, 팀 삭제 테스트에서 커밋/롤백에 따른 스토리지 삭제 여부를 확인하며, 역대 서비스 테스트에서 thumbnailImageUrl 단언을 URL 값으로 갱신한다.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AdminTeamServiceController
  participant AdminTeamServiceManageService
  participant ObjectStorageManager
  participant TeamServiceImageCommandService
  participant ObjectStorageTransactionSynchronizer

  rect rgba(70, 130, 180, 0.5)
    Note over Client, ObjectStorageTransactionSynchronizer: 팀 서비스 생성(썸네일 포함)
    Client->>AdminTeamServiceController: POST /team-services (multipart: request + thumbnailImage)
    AdminTeamServiceController->>AdminTeamServiceManageService: createTeamService(request, thumbnailImage)
    AdminTeamServiceManageService->>AdminTeamServiceManageService: buildObjectKey(file, serviceId) — 확장자/MIME/크기 검증
    AdminTeamServiceManageService->>ObjectStorageManager: upload(file, objectKey)
    AdminTeamServiceManageService->>ObjectStorageTransactionSynchronizer: registerDeleteOnRollback(newKey)
    AdminTeamServiceManageService->>TeamServiceImageCommandService: save(thumbnailEntity)
    AdminTeamServiceController-->>Client: 201 Created (Location 헤더)
  end

  rect rgba(34, 139, 34, 0.5)
    Note over Client, ObjectStorageTransactionSynchronizer: 팀 서비스 수정(새 썸네일로 교체)
    Client->>AdminTeamServiceController: PUT /team-services (multipart: request + thumbnailImage)
    AdminTeamServiceController->>AdminTeamServiceManageService: updateTeamService(request, thumbnailImage)
    AdminTeamServiceManageService->>ObjectStorageManager: upload(file, newObjectKey)
    AdminTeamServiceManageService->>ObjectStorageTransactionSynchronizer: registerDeleteOnRollback(newKey)
    AdminTeamServiceManageService->>TeamServiceImageCommandService: updateObjectKey(thumbnailEntity, newKey)
    AdminTeamServiceManageService->>ObjectStorageTransactionSynchronizer: registerDeleteAfterCommit(oldKey)
    AdminTeamServiceController-->>Client: 204 No Content
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • YAPP-admin/yappu-world-server#210: 동일한 AdminTeamService.kt의 팀 서비스 삭제 흐름을 변경하며, 이 PR은 해당 삭제 로직을 이미지 오브젝트 스토리지 연동 삭제까지 확장한다.
  • YAPP-admin/yappu-world-server#216: 역대 서비스 목록/상세 API를 도입한 PR로, 이 PR에서 HistoricalServiceService와 응답 DTO 팩토리 메서드에 썸네일 URL 반영을 추가한다.

Suggested reviewers

  • devmizz
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목 '팀 서비스 이미지 관리 기능 추가'는 PR의 핵심 변경사항인 팀 서비스 이미지 관리 기능 추가를 명확하고 간결하게 요약합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/team-images

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: 1

🧹 Nitpick comments (2)
src/main/resources/db/migration/V6__add_team_service_image_table.sql (1)

1-11: 조회 패턴에 맞는 인덱스 및 썸네일 유일성 고려.

findByTeamServiceAndIsThumbnailTrue / findByTeamServiceIdInAndIsThumbnailTrue 조회가 TEAM_SERVICE_ID, IS_THUMBNAIL 기준으로 동작합니다. TEAM_SERVICE_ID(또는 (TEAM_SERVICE_ID, IS_THUMBNAIL)) 인덱스를 추가하면 조회 성능에 도움이 됩니다. 또한 findThumbnailUrls의 associateBy는 서비스당 썸네일이 둘 이상일 경우 마지막 값만 남기므로, 서비스당 썸네일을 하나로 보장하려면 부분 유니크 제약/인덱스 도입을 검토해 보세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/resources/db/migration/V6__add_team_service_image_table.sql` around
lines 1 - 11, Add an index and a uniqueness guard for thumbnail lookups: create
an index on TEAM_SERVICE_IMAGES for the lookup columns (at least
TEAM_SERVICE_ID, or better the composite (TEAM_SERVICE_ID, IS_THUMBNAIL)) to
speed findByTeamServiceAndIsThumbnailTrue and
findByTeamServiceIdInAndIsThumbnailTrue, and enforce
at-most-one-thumbnail-per-service by adding a uniqueness constraint/index that
applies only when IS_THUMBNAIL = 1 (e.g., a function-based unique index or
equivalent filtered/partial unique index on TEAM_SERVICE_ID where IS_THUMBNAIL =
1) so findThumbnailUrls' associateBy won’t drop duplicates; implement these
changes in the TEAM_SERVICE_IMAGES DDL (adjust
V6__add_team_service_image_table.sql) alongside the FK constraint.
src/test/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageServiceTest.kt (1)

104-141: ⚡ Quick win

업데이트 시 썸네일 생성 시나리오 테스트 추가를 고려해보세요.

현재 테스트는 기존 썸네일을 수정하거나 삭제하는 경우를 검증하지만, 썸네일이 없던 서비스에 새로 썸네일을 추가하는 경우는 검증하지 않습니다. updateThumbnail 로직에서 thumbnail?.updateImageUrl(it) ?: createThumbnail(service, it) 부분의 createThumbnail 분기를 명시적으로 검증하는 테스트를 추가하면 더욱 완전한 커버리지를 확보할 수 있습니다.

제안하는 추가 테스트 시나리오
scenario("썸네일이 없던 서비스에 썸네일을 추가한다") {
    val team = teamRepository.save(getTeamEntityFixture())
    val service = teamServiceRepository.save(getTeamServiceEntityFixture(team = team))
    
    adminTeamServiceManageService.updateTeamService(
        updateRequest(
            id = service.id,
            teamId = team.id,
            thumbnailImageUrl = "https://image.yapp.co.kr/new.png"
        )
    )
    
    val thumbnail = teamServiceImageRepository.findByTeamServiceAndIsThumbnailTrue(service)
    thumbnail?.imageUrl shouldBe "https://image.yapp.co.kr/new.png"
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageServiceTest.kt`
around lines 104 - 141, Test coverage misses the branch where updateThumbnail
creates a new thumbnail; add a scenario to AdminTeamServiceManageServiceTest
that calls adminTeamServiceManageService.updateTeamService with a
thumbnailImageUrl when the TeamService has no existing thumbnail to assert
createThumbnail branch behavior. Specifically, add a scenario similar to the
others that saves a team and service without a thumbnail, invokes
updateTeamService (use the existing updateRequest helper) with thumbnailImageUrl
set (e.g. "https://image.yapp.co.kr/new.png"), then assert that
teamServiceImageRepository.findByTeamServiceAndIsThumbnailTrue(service)?.imageUrl
equals that URL to validate the updateThumbnail -> createThumbnail path is
exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/resources/db/migration/V6__add_team_service_image_table.sql`:
- Line 4: The IMAGE_URL column in the migration uses VARCHAR2(3000) but
TeamServiceImageEntity (length = 2000) and schema.sql (varchar(2000)) use 2000;
update the migration's IMAGE_URL definition from VARCHAR2(3000) to
VARCHAR2(2000) so all three places (V6__add_team_service_image_table.sql
IMAGE_URL, TeamServiceImageEntity length, and schema.sql) use the same 2000
length.

---

Nitpick comments:
In `@src/main/resources/db/migration/V6__add_team_service_image_table.sql`:
- Around line 1-11: Add an index and a uniqueness guard for thumbnail lookups:
create an index on TEAM_SERVICE_IMAGES for the lookup columns (at least
TEAM_SERVICE_ID, or better the composite (TEAM_SERVICE_ID, IS_THUMBNAIL)) to
speed findByTeamServiceAndIsThumbnailTrue and
findByTeamServiceIdInAndIsThumbnailTrue, and enforce
at-most-one-thumbnail-per-service by adding a uniqueness constraint/index that
applies only when IS_THUMBNAIL = 1 (e.g., a function-based unique index or
equivalent filtered/partial unique index on TEAM_SERVICE_ID where IS_THUMBNAIL =
1) so findThumbnailUrls' associateBy won’t drop duplicates; implement these
changes in the TEAM_SERVICE_IMAGES DDL (adjust
V6__add_team_service_image_table.sql) alongside the FK constraint.

In
`@src/test/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageServiceTest.kt`:
- Around line 104-141: Test coverage misses the branch where updateThumbnail
creates a new thumbnail; add a scenario to AdminTeamServiceManageServiceTest
that calls adminTeamServiceManageService.updateTeamService with a
thumbnailImageUrl when the TeamService has no existing thumbnail to assert
createThumbnail branch behavior. Specifically, add a scenario similar to the
others that saves a team and service without a thumbnail, invokes
updateTeamService (use the existing updateRequest helper) with thumbnailImageUrl
set (e.g. "https://image.yapp.co.kr/new.png"), then assert that
teamServiceImageRepository.findByTeamServiceAndIsThumbnailTrue(service)?.imageUrl
equals that URL to validate the updateThumbnail -> createThumbnail path is
exercised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d130b51d-ebf8-4a47-b1ce-8c8d8fed3607

📥 Commits

Reviewing files that changed from the base of the PR and between fccb53d and bb48c06.

📒 Files selected for processing (18)
  • src/main/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageService.kt
  • src/main/kotlin/co/yappuworld/team/client/application/HistoricalServiceService.kt
  • src/main/kotlin/co/yappuworld/team/client/dto/request/AdminTeamServiceCreateRequest.kt
  • src/main/kotlin/co/yappuworld/team/client/dto/request/AdminTeamServiceUpdateRequest.kt
  • src/main/kotlin/co/yappuworld/team/client/dto/response/AdminTeamServiceDetailResponse.kt
  • src/main/kotlin/co/yappuworld/team/client/dto/response/HistoricalServiceDetailResponse.kt
  • src/main/kotlin/co/yappuworld/team/client/dto/response/HistoricalServicePageResponse.kt
  • src/main/kotlin/co/yappuworld/team/client/presentation/AdminTeamServiceApi.kt
  • src/main/kotlin/co/yappuworld/team/client/presentation/HistoricalServiceApi.kt
  • src/main/kotlin/co/yappuworld/team/infrastructure/TeamServiceImageCommandService.kt
  • src/main/kotlin/co/yappuworld/team/infrastructure/TeamServiceImageFindService.kt
  • src/main/kotlin/co/yappuworld/team/infrastructure/entity/TeamServiceImageEntity.kt
  • src/main/kotlin/co/yappuworld/team/infrastructure/jpa/TeamServiceImageRepository.kt
  • src/main/resources/db/migration/V6__add_team_service_image_table.sql
  • src/main/resources/schema.sql
  • src/test/kotlin/co/yappuworld/support/fixture/TeamFixture.kt
  • src/test/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageServiceTest.kt
  • src/test/kotlin/co/yappuworld/team/client/application/HistoricalServiceServiceTest.kt

Comment thread src/main/resources/db/migration/V6__add_team_service_image_table.sql Outdated

@devmizz devmizz 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.

고생하셨습니다!

지금 작업하신 내용은 클라이언트에서 직접 저장소에 업로드 하는 걸 전제로 해서, URL 체크만 하는 것으로 보여요! 그럴 경우에 두 가지 정도 이슈가 있을 수 있을 거 같아요.

하나는, 이미지에 대한 검수를 저희가 진행할 수 없어요. 용량이나 파일 형식 그리고 이미지에 대한 검증을 진행해야 하는 경우에 서버에서 개입이 불가해요. 프론트에서도 너무 고도화 된 내용은 처리를 할 수가 없고요.

다른 하나는, 이미지 저장소 내 고아 객체에 대한 관리를 할 수가 없어요. 가령 어드민에서 글을 쓰면서 이미지를 업로드했는데 취소를 해버리는 경우, 이미지만 저장소에 업로드 되고 실제 글은 저장이 되면서 사용하지 않는 이미지가 저장소에 남게 돼요.

현재 방식으로 처리하게 되면 위의 문제들을 해결하기가 어려운 부분이 있어서, 이 부분에 대해 조금 고민을 해보시면 좋을 거 같아요!

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/resources/db/migration/V6__add_team_service_image_table.sql (1)

5-11: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

썸네일 단일성에 대한 DB 제약이 없어 중복 레코드가 발생할 수 있습니다.

현재 스키마로는 동일 TEAM_SERVICE_ID에 IS_THUMBNAIL = 1 행이 여러 개 저장될 수 있습니다. 이 경우 단건 조회 경로에서 런타임 예외가 발생하거나, 목록 매핑에서 임의 값으로 덮어써질 수 있습니다. team_service당 썸네일 1개를 DB 레벨에서 강제해 주세요.

제안 패치
 CREATE TABLE TEAM_SERVICE_IMAGES (
     ID RAW(16) PRIMARY KEY,
     TEAM_SERVICE_ID RAW(16) NOT NULL,
     OBJECT_KEY VARCHAR2(2000) NOT NULL,
     IS_THUMBNAIL NUMBER(1) DEFAULT 0 NOT NULL CHECK (IS_THUMBNAIL IN (0, 1)),
     DISPLAY_ORDER NUMBER DEFAULT 0 NOT NULL,
     CREATED_AT TIMESTAMP,
     UPDATED_AT TIMESTAMP,
     CONSTRAINT FK_TEAM_SERVICE_IMAGES_TEAM_SERVICE
         FOREIGN KEY (TEAM_SERVICE_ID) REFERENCES TEAM_SERVICES(ID) ON DELETE CASCADE
 );
+
+CREATE UNIQUE INDEX UK_TEAM_SERVICE_IMAGES_THUMBNAIL
+    ON TEAM_SERVICE_IMAGES (
+        CASE WHEN IS_THUMBNAIL = 1 THEN TEAM_SERVICE_ID END
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/resources/db/migration/V6__add_team_service_image_table.sql` around
lines 5 - 11, 현재 TEAM_SERVICE_IMAGES 스키마는 동일 TEAM_SERVICE_ID에 IS_THUMBNAIL = 1인
행이 여러 개 들어올 수 있으므로, TEAM_SERVICE_ID 당 썸네일을 1개로 DB 레벨에서 강제하도록 수정하세요:
V6__add_team_service_image_table.sql에 TEAM_SERVICE_IMAGES 테이블 정의 후
TEAM_SERVICE_ID와 IS_THUMBNAIL 컬럼을 사용해 유일성 제약을 추가합니다 — Oracle 환경이라면 "CREATE
UNIQUE INDEX UX_TEAM_SERVICE_IMAGES_TEAM_SERVICE_THUMBNAIL ON
TEAM_SERVICE_IMAGES (CASE WHEN IS_THUMBNAIL = 1 THEN TEAM_SERVICE_ID ELSE NULL
END);" 또는 가능한 DB에 맞는 partial unique index / filtered unique constraint 문법을 사용해
IS_THUMBNAIL = 1인 경우에만 TEAM_SERVICE_ID가 유일하도록 만드세요.
🧹 Nitpick comments (2)
src/main/kotlin/co/yappuworld/team/infrastructure/TeamServiceImageFindService.kt (1)

24-27: ⚡ Quick win

빈 ID 목록은 조기 반환으로 DB 조회를 건너뛰어 주세요.

teamServiceIds가 비어있을 때는 결과가 항상 빈 맵이므로 즉시 반환하면 불필요한 쿼리를 줄일 수 있습니다.

제안 패치
-    fun findThumbnailObjectKeys(teamServiceIds: List<UUID>): Map<UUID, String> =
-        teamServiceImageRepository
-            .findThumbnailObjectKeysByTeamServiceIdIn(teamServiceIds)
-            .associateBy({ it.teamServiceId }, { it.objectKey })
+    fun findThumbnailObjectKeys(teamServiceIds: List<UUID>): Map<UUID, String> {
+        if (teamServiceIds.isEmpty()) return emptyMap()
+        return teamServiceImageRepository
+            .findThumbnailObjectKeysByTeamServiceIdIn(teamServiceIds)
+            .associateBy({ it.teamServiceId }, { it.objectKey })
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/kotlin/co/yappuworld/team/infrastructure/TeamServiceImageFindService.kt`
around lines 24 - 27, The function findThumbnailObjectKeys should early-return
an empty map when teamServiceIds is empty to avoid unnecessary DB queries;
update findThumbnailObjectKeys to check if teamServiceIds.isEmpty() and return
emptyMap() before calling
teamServiceImageRepository.findThumbnailObjectKeysByTeamServiceIdIn(teamServiceIds),
keeping the rest of the logic (associateBy using it.teamServiceId and
it.objectKey) unchanged.
src/test/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageServiceTest.kt (1)

196-220: ⚡ Quick win

파일 업로드와 removeThumbnail=true 동시 요청 실패 케이스를 테스트에 추가해 주세요.

서비스 구현에서 thumbnailImage != null && removeThumbnail이면 BusinessException(TeamError.INVALID_THUMBNAIL_REQUEST)를 던지므로, 이 분기 회귀를 막는 시나리오가 있으면 더 안전합니다.

🧪 제안 테스트 추가안
         feature("어드민 팀 서비스 수정") {
+            scenario("썸네일 이미지와 삭제 요청을 동시에 보내면 예외가 발생한다") {
+                val team = teamRepository.save(getTeamEntityFixture())
+                val service = teamServiceRepository.save(getTeamServiceEntityFixture(team = team))
+
+                shouldThrow<BusinessException> {
+                    adminTeamServiceManageService.updateTeamService(
+                        updateRequest(
+                            id = service.id,
+                            teamId = team.id,
+                            removeThumbnail = true
+                        ),
+                        thumbnailImage("conflict.png")
+                    )
+                }.error shouldBe TeamError.INVALID_THUMBNAIL_REQUEST
+            }
+
             scenario("기존 썸네일이 있으면 업로드 URL로 변경한다") {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageServiceTest.kt`
around lines 196 - 220, Add a new scenario to the "어드민 팀 서비스 수정" feature that
covers the invalid simultaneous upload+remove case: create a team and team
service fixture as in the existing scenario, then call
adminTeamServiceManageService.updateTeamService with an Update request that
includes both a non-null thumbnailImage (use the existing
thumbnailImage("file.png") helper) and removeThumbnail = true, and assert that
the call throws BusinessException with TeamError.INVALID_THUMBNAIL_REQUEST;
reuse the same transaction setup (newTransactionTemplate().executeWithoutResult
{ ... }) and helpers (getTeamEntityFixture, getTeamServiceEntityFixture) so the
test mirrors the existing flow and prevents regressions in the branch where
thumbnailImage != null && removeThumbnail.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageService.kt`:
- Around line 150-162: The current buildObjectKey(serviceId: UUID, file:
MultipartFile) only checks filename extension and size, which allows disguised
non-image uploads; update this function to also validate the uploaded file's
actual content by checking the MultipartFile.contentType (MIME) against allowed
image MIME types and performing a magic-byte signature check on the file's first
bytes (read a small header byte[] from file.inputStream) before accepting; keep
existing size and extension checks, and throw
BusinessException(TeamError.INVALID_IMAGE) if MIME or signature validation
fails; use the same validation approach for the other upload validation spots
referenced (lines ~167-169) so all image upload paths use consistent MIME +
magic-byte + size checks with ALLOWED_IMAGE_EXTENSIONS and MAX_IMAGE_FILE_SIZE.

---

Outside diff comments:
In `@src/main/resources/db/migration/V6__add_team_service_image_table.sql`:
- Around line 5-11: 현재 TEAM_SERVICE_IMAGES 스키마는 동일 TEAM_SERVICE_ID에 IS_THUMBNAIL
= 1인 행이 여러 개 들어올 수 있으므로, TEAM_SERVICE_ID 당 썸네일을 1개로 DB 레벨에서 강제하도록 수정하세요:
V6__add_team_service_image_table.sql에 TEAM_SERVICE_IMAGES 테이블 정의 후
TEAM_SERVICE_ID와 IS_THUMBNAIL 컬럼을 사용해 유일성 제약을 추가합니다 — Oracle 환경이라면 "CREATE
UNIQUE INDEX UX_TEAM_SERVICE_IMAGES_TEAM_SERVICE_THUMBNAIL ON
TEAM_SERVICE_IMAGES (CASE WHEN IS_THUMBNAIL = 1 THEN TEAM_SERVICE_ID ELSE NULL
END);" 또는 가능한 DB에 맞는 partial unique index / filtered unique constraint 문법을 사용해
IS_THUMBNAIL = 1인 경우에만 TEAM_SERVICE_ID가 유일하도록 만드세요.

---

Nitpick comments:
In
`@src/main/kotlin/co/yappuworld/team/infrastructure/TeamServiceImageFindService.kt`:
- Around line 24-27: The function findThumbnailObjectKeys should early-return an
empty map when teamServiceIds is empty to avoid unnecessary DB queries; update
findThumbnailObjectKeys to check if teamServiceIds.isEmpty() and return
emptyMap() before calling
teamServiceImageRepository.findThumbnailObjectKeysByTeamServiceIdIn(teamServiceIds),
keeping the rest of the logic (associateBy using it.teamServiceId and
it.objectKey) unchanged.

In
`@src/test/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageServiceTest.kt`:
- Around line 196-220: Add a new scenario to the "어드민 팀 서비스 수정" feature that
covers the invalid simultaneous upload+remove case: create a team and team
service fixture as in the existing scenario, then call
adminTeamServiceManageService.updateTeamService with an Update request that
includes both a non-null thumbnailImage (use the existing
thumbnailImage("file.png") helper) and removeThumbnail = true, and assert that
the call throws BusinessException with TeamError.INVALID_THUMBNAIL_REQUEST;
reuse the same transaction setup (newTransactionTemplate().executeWithoutResult
{ ... }) and helpers (getTeamEntityFixture, getTeamServiceEntityFixture) so the
test mirrors the existing flow and prevents regressions in the branch where
thumbnailImage != null && removeThumbnail.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1008fd5f-2aff-4865-ac0e-5d3ffcb3b1fe

📥 Commits

Reviewing files that changed from the base of the PR and between bb48c06 and cb73ba1.

📒 Files selected for processing (29)
  • .github/workflows/yappu-oci-dev-cd.yaml
  • .github/workflows/yappu-oci-prod-cd.yaml
  • build.gradle.kts
  • gradle/libs.versions.toml
  • src/main/kotlin/co/yappuworld/external/storage/DisabledObjectStorageService.kt
  • src/main/kotlin/co/yappuworld/external/storage/ObjectStorageService.kt
  • src/main/kotlin/co/yappuworld/external/storage/ObjectStorageTransactionSynchronizer.kt
  • src/main/kotlin/co/yappuworld/external/storage/OciObjectStorageConfig.kt
  • src/main/kotlin/co/yappuworld/external/storage/OciObjectStorageProperties.kt
  • src/main/kotlin/co/yappuworld/external/storage/OciObjectStorageService.kt
  • src/main/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageService.kt
  • src/main/kotlin/co/yappuworld/team/client/application/HistoricalServiceService.kt
  • src/main/kotlin/co/yappuworld/team/client/dto/request/AdminTeamServiceUpdateRequest.kt
  • src/main/kotlin/co/yappuworld/team/client/presentation/AdminTeamServiceApi.kt
  • src/main/kotlin/co/yappuworld/team/client/presentation/AdminTeamServiceController.kt
  • src/main/kotlin/co/yappuworld/team/domain/vo/TeamError.kt
  • src/main/kotlin/co/yappuworld/team/infrastructure/TeamServiceImageFindService.kt
  • src/main/kotlin/co/yappuworld/team/infrastructure/entity/TeamServiceImageEntity.kt
  • src/main/kotlin/co/yappuworld/team/infrastructure/jpa/TeamServiceImageRepository.kt
  • src/main/resources/application-dev-oci.yaml
  • src/main/resources/application-local.yaml
  • src/main/resources/application-prod-oci.yaml
  • src/main/resources/application-test.yaml
  • src/main/resources/db/migration/V6__add_team_service_image_table.sql
  • src/main/resources/schema.sql
  • src/test/kotlin/co/yappuworld/support/fixture/TeamFixture.kt
  • src/test/kotlin/co/yappuworld/support/storage/FakeObjectStorageService.kt
  • src/test/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageServiceTest.kt
  • src/test/kotlin/co/yappuworld/team/client/application/HistoricalServiceServiceTest.kt
✅ Files skipped from review due to trivial changes (4)
  • src/main/resources/application-prod-oci.yaml
  • src/test/kotlin/co/yappuworld/support/storage/FakeObjectStorageService.kt
  • src/main/resources/application-test.yaml
  • src/main/kotlin/co/yappuworld/external/storage/ObjectStorageTransactionSynchronizer.kt
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/resources/schema.sql
  • src/test/kotlin/co/yappuworld/support/fixture/TeamFixture.kt
  • src/main/kotlin/co/yappuworld/team/client/application/HistoricalServiceService.kt
  • src/test/kotlin/co/yappuworld/team/client/application/HistoricalServiceServiceTest.kt

@devmizz devmizz 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.

수정하느라 고생 많으셨습니다! 두 가지 추가 사항이 있습니다.

  1. 팀 삭제 시에 서비스 이미지도 삭제되는지 확인 필요
  2. OCI Storage 비용 구조 어떻게 되는지 확인 필요

name = ["enabled"],
havingValue = "false"
)
class DisabledObjectStorageService : ObjectStorageService {

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.

C) 요건 왜 있는 건가요? Disabled 접두사가 있어서, 비활성화된 거면 왜 구현체로 있는지 이해가 안 됩니다! NoUsage라서 더더욱이요!

@eeun1225 eeun1225 Jun 11, 2026 •

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.

로컬, 테스트 환경에서는 oci를 사용하지 않지만, ObjectStorageService 의존성 주입을 유지하기 위한 대체 구현체로 추가했습니다!

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.

@eeun1225 그런 의도라면 Mock이나 Fake 같은 용어를 쓰면 좋을 거 같아요!

var teamService: TeamServiceEntity = teamService
private set

@Column(name = "object_key", nullable = false, length = 2000)

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.

A) 이건 기존에 image_url 이었던 거 같은데, object_key로 변경하신 이유가 있을까요? image 테이블이라 image_url 해도 큰 지장은 없어보여서요!

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.

dev/prod 버킷을 분리해 환경별 base url이 다르고, 추후 도메인이나 버킷 경로 변경 시 DB 데이터까지 수정해야 하는 문제를 방지하기 위해 objectKey만 저장하고 조회 시 url을 생성하도록 변경했습니다!

@devmizz devmizz Jun 12, 2026 •

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.

@eeun1225 key가 혹시 버킷 경로(패키지 경로)를 제외한 파일명만 해당하는 건가요?

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.

아니요! team-services/{serviceId}/{uuid}.{extension} 형태로 버킷 내부 경로까지 포함한 값으로 저장하고 있습니당


import org.springframework.web.multipart.MultipartFile

interface ObjectStorageService {

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.

C) Manager 라는 이름은 어떨까요? 업로드랑 삭제를 하면서 객체들을 관리한다는 점에서요!

@eeun1225

Copy link
Copy Markdown
Contributor Author
  1. 팀 삭제는 한 번 테스트해보겠습니당
  2. OCI에서는 용량 10GB, API 호출 월 50,000건까지 무료로 지원하고 있어서 추가 비용은 발생하지 않을 것 같습니다!

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/kotlin/co/yappuworld/external/storage/MockObjectStorageManager.kt (1)

16-20: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

비활성 스토리지 경로에서 의도된 상태를 500으로 노출하고 있습니다.

Line 19에서 INTERNAL_SERVER_ERROR를 고정으로 던지면, object-storage.enabled=false 프로필에서 썸네일 업로드 요청이 설정 기반 제한임에도 항상 일반 500으로 처리됩니다. 비활성화 상태를 나타내는 명시적 에러(예: 기능 비활성/설정 오류 전용 코드)로 분기해 API 의미를 분리하는 게 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/kotlin/co/yappuworld/external/storage/MockObjectStorageManager.kt`
around lines 16 - 20, The upload method in MockObjectStorageManager is throwing
a generic INTERNAL_SERVER_ERROR, but when the object storage is disabled via
configuration (object-storage.enabled=false), this misleads clients into
thinking there's an actual server error rather than a feature limitation.
Replace the INTERNAL_SERVER_ERROR with a more specific and semantically
appropriate error code from GlobalError that indicates the storage feature is
disabled or unavailable (such as a feature disabled error or configuration error
code). This way, the API response accurately reflects the true nature of the
issue: a configuration-based restriction rather than an actual internal server
error.
🧹 Nitpick comments (1)
src/test/kotlin/co/yappuworld/team/client/application/AdminTeamServiceTest.kt (1)

191-191: 💤 Low value

삭제 순서에 의존하는 단언문이 테스트 불안정성을 유발할 수 있습니다.

shouldBe listOf(firstObjectKey, secondObjectKey)는 삭제 순서가 특정 순서임을 가정합니다. findObjectKeys의 반환 순서가 보장되지 않으면 테스트가 간헐적으로 실패할 수 있습니다. 순서에 무관한 검증을 권장합니다.

♻️ 순서 무관 검증으로 변경
-objectStorageManager.deletedObjectKeys shouldBe listOf(firstObjectKey, secondObjectKey)
+objectStorageManager.deletedObjectKeys shouldContainExactlyInAnyOrder listOf(firstObjectKey, secondObjectKey)

shouldContainExactlyInAnyOrder 사용을 위해 import 추가:

import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/kotlin/co/yappuworld/team/client/application/AdminTeamServiceTest.kt`
at line 191, The assertion in AdminTeamServiceTest.kt using shouldBe with a
specific list order (firstObjectKey, secondObjectKey) depends on deletion order
which is not guaranteed, causing potential test instability. Replace this
order-dependent assertion with shouldContainExactlyInAnyOrder to verify that the
deleted object keys contain exactly the expected elements regardless of order.
Add the necessary import for shouldContainExactlyInAnyOrder from
io.kotest.matchers.collections.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/main/kotlin/co/yappuworld/external/storage/MockObjectStorageManager.kt`:
- Around line 16-20: The upload method in MockObjectStorageManager is throwing a
generic INTERNAL_SERVER_ERROR, but when the object storage is disabled via
configuration (object-storage.enabled=false), this misleads clients into
thinking there's an actual server error rather than a feature limitation.
Replace the INTERNAL_SERVER_ERROR with a more specific and semantically
appropriate error code from GlobalError that indicates the storage feature is
disabled or unavailable (such as a feature disabled error or configuration error
code). This way, the API response accurately reflects the true nature of the
issue: a configuration-based restriction rather than an actual internal server
error.

---

Nitpick comments:
In
`@src/test/kotlin/co/yappuworld/team/client/application/AdminTeamServiceTest.kt`:
- Line 191: The assertion in AdminTeamServiceTest.kt using shouldBe with a
specific list order (firstObjectKey, secondObjectKey) depends on deletion order
which is not guaranteed, causing potential test instability. Replace this
order-dependent assertion with shouldContainExactlyInAnyOrder to verify that the
deleted object keys contain exactly the expected elements regardless of order.
Add the necessary import for shouldContainExactlyInAnyOrder from
io.kotest.matchers.collections.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c5354146-fcad-4568-b5cf-51f78206582a

📥 Commits

Reviewing files that changed from the base of the PR and between cb73ba1 and 95f96d3.

📒 Files selected for processing (14)
  • src/main/kotlin/co/yappuworld/external/storage/MockObjectStorageManager.kt
  • src/main/kotlin/co/yappuworld/external/storage/ObjectStorageManager.kt
  • src/main/kotlin/co/yappuworld/external/storage/ObjectStorageTransactionSynchronizer.kt
  • src/main/kotlin/co/yappuworld/external/storage/OciObjectStorageManager.kt
  • src/main/kotlin/co/yappuworld/team/client/application/AdminTeamService.kt
  • src/main/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageService.kt
  • src/main/kotlin/co/yappuworld/team/client/application/HistoricalServiceService.kt
  • src/main/kotlin/co/yappuworld/team/infrastructure/TeamServiceImageFindService.kt
  • src/main/resources/application-dev-oci.yaml
  • src/main/resources/application-prod-oci.yaml
  • src/test/kotlin/co/yappuworld/support/storage/FakeObjectStorageManager.kt
  • src/test/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageServiceTest.kt
  • src/test/kotlin/co/yappuworld/team/client/application/AdminTeamServiceTest.kt
  • src/test/kotlin/co/yappuworld/team/client/application/HistoricalServiceServiceTest.kt
✅ Files skipped from review due to trivial changes (2)
  • src/main/kotlin/co/yappuworld/external/storage/ObjectStorageManager.kt
  • src/test/kotlin/co/yappuworld/support/storage/FakeObjectStorageManager.kt
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/main/resources/application-dev-oci.yaml
  • src/main/kotlin/co/yappuworld/external/storage/ObjectStorageTransactionSynchronizer.kt
  • src/main/kotlin/co/yappuworld/team/client/application/HistoricalServiceService.kt
  • src/main/kotlin/co/yappuworld/team/infrastructure/TeamServiceImageFindService.kt
  • src/main/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageService.kt
  • src/test/kotlin/co/yappuworld/team/client/application/HistoricalServiceServiceTest.kt
  • src/test/kotlin/co/yappuworld/team/client/application/AdminTeamServiceManageServiceTest.kt

@devmizz devmizz 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.

고생하셨습니다~

@eeun1225
eeun1225 merged commit 27d4a40 into dev Jun 24, 2026
2 checks passed
@eeun1225
eeun1225 deleted the feat/team-images branch June 24, 2026 08:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🖌️ feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants