Skip to content

Feat : 어드민 동아리 업로드 API 구현#353

Closed
rlagkswn00 wants to merge 45 commits intodevelopfrom
feat/admin-upload-club-info
Closed

Feat : 어드민 동아리 업로드 API 구현#353
rlagkswn00 wants to merge 45 commits intodevelopfrom
feat/admin-upload-club-info

Conversation

@rlagkswn00
Copy link
Member

@rlagkswn00 rlagkswn00 commented Feb 27, 2026

#️⃣ 이슈

📌 요약

  • 어드민 동아리 정보 업로드 API(POST /api/v2/admin/clubs)를 추가했습니다.
  • 동아리 생성 요청/응답 DTO, UseCase/Port, Persistence, Service 로직을 구현했습니다.
  • 이벤트 기반 이미지 업로드 흐름(AFTER_COMMIT)을 적용했습니다.
  • 동아리 생성 인수테스트와 도메인 enum 테스트를 보강했습니다.

🛠️ 상세

  • 마이그레이션
    club.icon_image_path 컬럼 추가

  • API/DTO
    AdminCommandApiV2 동아리 생성 엔드포인트 추가
    AdminClubCreateRequest, AdminClubCreateResponse 추가

  • 도메인/서비스
    Club 생성 필드 확장(icon/poster, 모집 정보 등)
    카테고리/소속 한글 매핑 지원
    중복 체크(name + division) 및 입력 검증(날짜/URL/필수 이미지) 반영

  • 저장/이벤트
    ClubCommandPort, ClubEventPort, ClubCreateAdminUseCase 등 계약 추가
    ClubPersistenceAdapter, ClubRepository, ClubSnsRepository 구현
    ClubCreateEvent, StorageEventListener, StorageCommandService로 이미지 업로드 처리

  • 테스트
    AdminAcceptanceTest 동아리 업로드 케이스 추가
    AdminStep에 요청 직렬화/업로드 헬퍼 추가
    ClubCategoryTest, ClubDivisionTest 정리

  • 이벤트 기반 업로드 동작 흐름

  1. 어드민이 POST /api/v2/admin/clubs로 multipart 요청을 보냅니다.
  2. 서비스에서 입력값(필수값/날짜/URL/중복)을 검증합니다.
  3. club, club_sns를 트랜잭션 내에서 DB에 저장합니다.
  4. 저장된 이미지 경로 정보로 ClubCreateEvent를 발행합니다.
  5. 트랜잭션 커밋 이후 @TransactionalEventListener(AFTER_COMMIT)가 이벤트를 수신합니다.
  6. 스토리지 업로드 유스케이스가 icon/poster 파일을 순차 업로드합니다.
  7. 업로드 실패는 애플리케이션 로그로 남기고(현재 정책), DB 트랜잭션은 유지됩니다.
  • 기술적 고민/의사결정
  • 파일 업로드 간 초기에 @Async를 도입했으나, MultipartFile InputStream의 수명 이슈가 있어, AFTER_COMMIT 동기 처리로 구현했습니다.

💬 기타

  • lat/lon/building/room 같은 위치 정보는 향후 별도 테이블(예: building/location)로 분리 관리가 필요합니다.
  • 위치 정보 정규화는 별도 이슈를 생성해 후속 작업으로 관리하겠습니다.

Summary by CodeRabbit

  • 새로운 기능

    • 관리자용 동아리 생성 API 추가: 기본정보·모집기간·SNS 링크 입력과 아이콘/포스터 이미지 업로드 지원
    • 생성 후 이미지 업로드가 트랜잭션 커밋 후 비동기 처리되어 자동 저장됨
  • 개선 사항

    • 동아리 중복 검사 강화(이름·분류 기준)
    • SNS URL 분류 로직 개선(호스트 기반 매핑)
  • 테스트

    • 관리자 동아리 생성 통합 테스트 및 SNS URL 분류 단위 테스트 추가

@rlagkswn00 rlagkswn00 self-assigned this Feb 27, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

Walkthrough

관리자용 동아리 생성 기능을 추가합니다. 새로운 API 엔드포인트와 DTO, 어플리케이션 포트/서비스, 영속성·도메인 변경, 이벤트 기반 이미지 업로드 흐름 및 관련 테스트·DB 마이그레이션이 포함됩니다.

Changes

Cohort / File(s) Summary
Admin API
src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminCommandApiV2.java, src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminClubCreateRequest.java, src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminClubCreateResponse.java
POST /api/v2/admin/clubs 엔드포인트 추가: multipart/form-data로 JSON 요청과 아이콘/포스터 파일 수신, AdminClubCreateRequest → AdminClubCreateCommand 매핑 및 use case 호출.
Application 포트/서비스
src/main/java/com/kustacks/kuring/club/application/port/in/ClubCreateAdminUseCase.java, src/main/java/com/kustacks/kuring/club/application/port/in/dto/AdminClubCreateCommand.java, src/main/java/com/kustacks/kuring/club/application/service/ClubCommandService.java
관리자 동아리 생성 유스케이스 정의 및 구현 추가(검증, URL 정규화, 중복검사, 엔티티·SNS 생성, 이벤트 발행, 이미지 경로 생성).
출력 포트·이벤트 어댑터
src/main/java/com/kustacks/kuring/club/application/port/out/ClubCommandPort.java, src/main/java/com/kustacks/kuring/club/application/port/out/ClubEventPort.java, src/main/java/com/kustacks/kuring/admin/adapter/out/event/ClubEventAdapter.java
명령·이벤트 포트 추가 및 구현: Club 저장, ClubSns 일괄 저장, ClubCreateEvent 생성(아이콘·포스터 변환) 및 발행.
영속성 변경
src/main/java/com/kustacks/kuring/club/adapter/out/persistence/ClubPersistenceAdapter.java, src/main/java/com/kustacks/kuring/club/adapter/out/persistence/ClubRepository.java, src/main/java/com/kustacks/kuring/club/adapter/out/persistence/ClubSnsRepository.java
ClubPersistenceAdapter에 ClubCommandPort 구현 추가, existsByNameAndDivision 쿼리 추가, ClubSnsRepository 도입 및 saveAll 위임 메서드 도입.
도메인 변경
src/main/java/com/kustacks/kuring/club/domain/Club.java, .../ClubCategory.java, .../ClubDivision.java, .../ClubSns.java, .../ClubSnsType.java
Club에 iconImagePath·posterImagePath 필드 및 생성자 추가, 한글명 기반 fromKorName 메서드 추가, ClubSns url을 Url 임베디드로 변경, ClubSnsType에 호스트 기반 매핑·fromUrl 로직 추가.
스토리지/이벤트 업로드
src/main/java/com/kustacks/kuring/storage/adapter/in/event/StorageEventListener.java, src/main/java/com/kustacks/kuring/storage/adapter/in/event/dto/ClubCreateEvent.java, src/main/java/com/kustacks/kuring/storage/application/StorageCommandService.java, src/main/java/com/kustacks/kuring/storage/application/port/in/StorageUploadUseCase.java, src/main/java/com/kustacks/kuring/storage/application/port/in/dto/UploadFileCommand.java
트랜잭션 커밋 후 ClubCreateEvent 수신 → UploadFileCommand 변환 → StorageUploadUseCase.uploadAll로 파일 업로드 수행(입출력·예외 처리 포함).
응답·에러 코드 및 DB 마이그레이션
src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java, src/main/java/com/kustacks/kuring/common/exception/code/ErrorCode.java, src/main/resources/db/migration/V260227__Add_icon_image_path_to_club.sql
ADMIN_CLUB_CREATE_SUCCESS 응답 코드 추가, CLUB_DUPLICATED 에러 코드 추가, club 테이블에 icon_image_path VARCHAR(255) 컬럼 추가 마이그레이션.
테스트 변경
src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java, src/test/java/com/kustacks/kuring/acceptance/AdminStep.java, src/test/java/com/kustacks/kuring/club/domain/ClubCategoryTest.java, src/test/java/com/kustacks/kuring/club/domain/ClubDivisionTest.java, src/test/java/com/kustacks/kuring/club/domain/ClubSnsTypeTest.java
관리자 동아리 생성 인수테스트 및 헬퍼 추가(멀티파트 요청 검사), 도메인 테스트 메서드명 갱신, ClubSnsType 단위테스트 추가.

Sequence Diagram(s)

sequenceDiagram
    actor Admin as 관리자
    participant API as AdminCommandApiV2
    participant UseCase as ClubCreateAdminUseCase
    participant Service as ClubCommandService
    participant CommandPort as ClubCommandPort
    participant EventPort as ClubEventPort
    participant DB as Database
    participant EventListener as StorageEventListener
    participant StorageService as StorageUploadUseCase
    participant Storage as CloudStorage

    Admin->>API: POST /api/v2/admin/clubs (JSON + icon/poster 파일)
    API->>UseCase: createClub(AdminClubCreateCommand)
    UseCase->>Service: createClub(command)

    rect rgba(100,200,150,0.5)
    Service->>CommandPort: existsByNameAndDivision(name, division)
    end

    rect rgba(150,150,200,0.5)
    Service->>CommandPort: save(Club)
    CommandPort->>DB: INSERT club
    DB-->>CommandPort: clubId
    end

    rect rgba(200,150,150,0.5)
    Service->>CommandPort: saveAll(List<ClubSns>)
    Service->>EventPort: publishClubCreate(clubId, icon, poster, paths)
    end

    API-->>Admin: 201 ADMIN_CLUB_CREATE_SUCCESS

    Note over EventListener: 트랜잭션 커밋 후 이벤트 수신
    EventListener->>StorageService: uploadAll(UploadFileCommand)
    StorageService->>Storage: 파일 업로드
    Storage-->>StorageService: 업로드 결과
Loading

Possibly related PRs

Poem

🐰 새 동아리 문을 열었네, 뛰어들자 당차게,
아이콘은 반짝이고, 포스터는 바람에 날리네,
이벤트가 톡톡, 파일이 구름으로 흘러가고,
DB엔 한 줄 새겨지고, 로그엔 기쁨이 쌓이네,
축하하라, 동아리여! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 어드민 동아리 업로드 API 구현의 주요 변경사항을 명확하게 요약하며, 변경 세트의 핵심 목표와 일치합니다.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/admin-upload-club-info

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.

@github-actions
Copy link

github-actions bot commented Feb 27, 2026

Unit Test Results

  82 files  +  1    82 suites  +1   1m 33s ⏱️ -2s
582 tests +12  575 ✔️ +12  7 💤 ±0  0 ±0 
585 runs  +12  578 ✔️ +12  7 💤 ±0  0 ±0 

Results for commit 5eced2e. ± Comparison against base commit e5e2d49.

♻️ This comment has been updated with latest results.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (4)
src/test/java/com/kustacks/kuring/club/domain/ClubCategoryTest.java (2)

19-25: ⚠️ Potential issue | 🟡 Minor

ClubDivisionTest와 동일한 네이밍 불일치 문제가 있습니다.

메서드 이름은 fromKorNameName이지만 실제로는 ClubCategory.fromName(name)을 테스트하고 있으며, 테스트 데이터도 영문 값(academic, culture_art 등)을 사용합니다. 메서드 이름을 실제 테스트 대상과 일치시켜 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/kustacks/kuring/club/domain/ClubCategoryTest.java` around
lines 19 - 25, Rename the misnamed test method fromKorNameName to reflect what's
being tested: e.g., fromName (or fromEnglishName); update the test method name
that calls ClubCategory.fromName(name) and asserts equality with the expected
ClubCategory so the method name matches the tested function
ClubCategory.fromName and the English test data (academic, culture_art, etc.).

29-39: ⚠️ Potential issue | 🟡 Minor

예외 테스트도 동일한 네이밍 불일치가 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/kustacks/kuring/club/domain/ClubCategoryTest.java` around
lines 29 - 39, The test method name has a typo "fromKorNameNameException"
causing a naming mismatch; rename the test method to a clear, consistent name
(e.g., "fromKorNameException" or "fromNameException") so it matches the other
tests, and keep the test body unchanged — locate the method in ClubCategoryTest
and update the method declaration name that wraps the ThrowingCallable calling
ClubCategory.fromName(name).
src/test/java/com/kustacks/kuring/club/domain/ClubDivisionTest.java (2)

19-25: ⚠️ Potential issue | 🟡 Minor

테스트 메서드 이름과 실제 테스트 대상이 불일치합니다.

메서드 이름은 fromKorNameName이지만 실제로는 ClubDivision.fromName(name)을 테스트하고 있습니다. 만약 fromKorName 메서드를 테스트하려는 의도였다면 테스트 본문과 테스트 데이터도 함께 수정해야 합니다. 기존 fromName 테스트를 유지하려는 의도라면 메서드 이름을 원래대로 유지하거나, 적절한 이름(예: fromNameTest)으로 변경하는 것이 좋겠습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/kustacks/kuring/club/domain/ClubDivisionTest.java` around
lines 19 - 25, The test method name fromKorNameName does not match the exercised
method; update the test to match intent: either rename the test method
fromKorNameName to a descriptive name like fromNameTest (or
fromName_whenValidName_returnsDivision) to reflect that it calls
ClubDivision.fromName(name), or change the test body to call
ClubDivision.fromKorName(name) and adjust test data accordingly; locate the
method in class ClubDivisionTest and update the method name or the invoked
static method (ClubDivision.fromName vs ClubDivision.fromKorName) so name and
behavior are consistent.

29-39: ⚠️ Potential issue | 🟡 Minor

동일한 네이밍 불일치 문제가 있습니다.

위의 fromKorNameName과 동일하게, 메서드 이름은 fromKorNameNameException이지만 실제로는 ClubDivision.fromName(name)의 예외 케이스를 테스트합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/kustacks/kuring/club/domain/ClubDivisionTest.java` around
lines 29 - 39, Test method name fromKorNameNameException mismatches the
behavior: it calls ClubDivision.fromName(name) but the name implies a "Kor"
variant; rename the test to reflect what's being tested (e.g., fromNameException
or fromNameThrowsNotFoundException) or change the call to
ClubDivision.fromKorName(name) to match the existing name; update the test
method name (fromKorNameNameException) or the invoked method
(ClubDivision.fromName(name)) so the test name and tested symbol align.
🧹 Nitpick comments (6)
src/main/java/com/kustacks/kuring/storage/application/StorageCommandService.java (1)

29-43: 업로드 실패 시 부분 실패 추적 고려.

PR 목표에 따라 업로드 실패 시 로그만 남기고 DB는 유지하는 것이 의도된 설계입니다. 그러나 현재 구현에서는 어떤 파일이 성공/실패했는지 호출자가 알 수 없습니다.

향후 운영 모니터링 또는 재시도 로직을 위해 실패 건수나 실패한 파일 목록을 반환하거나 메트릭으로 노출하는 것을 고려해 볼 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/kustacks/kuring/storage/application/StorageCommandService.java`
around lines 29 - 43, The upload method in StorageCommandService currently
swallows failures and only logs them, so callers cannot know which files
succeeded or failed; change upload(UploadFileCommand.UploadFile file) to return
a result object (e.g., UploadResult with fields success:boolean, key:String,
error:String) or at minimum a boolean, populate it from the try/catch around
storagePort.upload, and propagate that result up to the caller so callers can
track successful vs failed files; alternatively (or additionally) emit a metric
or increment a failure counter (e.g., via MeterRegistry) inside the catch blocks
including file.key() and e.getMessage() to expose failures to monitoring.
src/main/java/com/kustacks/kuring/storage/adapter/in/event/StorageEventListener.java (1)

11-11: 사용되지 않는 @Slf4j 어노테이션.

@Slf4j 어노테이션이 있지만 클래스 내에서 로깅이 사용되지 않습니다. 이벤트 수신 로깅을 추가하거나 어노테이션을 제거해 주세요.

💡 이벤트 수신 로깅 추가 예시
     `@TransactionalEventListener`(phase = TransactionPhase.AFTER_COMMIT) // 트랜잭션 커밋 후 진행.
     public void uploadClubImages(ClubCreateEvent event) {
+        log.info("동아리 생성 이벤트 수신, 이미지 업로드 시작");
         storageUploadUseCase.uploadAll(event.toCommand());
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/kustacks/kuring/storage/adapter/in/event/StorageEventListener.java`
at line 11, 클래스에 붙은 사용되지 않는 `@Slf4j` 어노테이션을 정리하세요: StorageEventListener 클래스에서 로깅을
전혀 사용하지 않으면 `@Slf4j` 주석을 제거하고 불필요한 import를 삭제하거나, 이벤트 수신을 기록하려면
onApplicationEvent(또는 해당 이벤트 핸들러 메서드) 안에 수신 시점과 주요 페이로드(예: 이벤트 타입, id 등)를 기록하는
log.debug/info 호출을 추가해 주세요; 결정한 방식에 따라 `@Slf4j` 유지 시 log를 실제로 사용하거나, 사용하지 않으면
어노테이션과 lombok.slf4j.Slf4j import를 제거하십시오.
src/main/java/com/kustacks/kuring/storage/application/port/in/dto/UploadFileCommand.java (1)

9-14: 불완전한 주석 제거 필요.

Line 11의 주석 // 아래 내용들을.이 불완전합니다. 의미 있는 설명으로 수정하거나 제거해 주세요.

또한, InputStream을 record에 저장하는 것은 리소스 관리에 주의가 필요합니다. 현재 StorageCommandService.upload()에서 try-with-resources로 적절히 처리하고 있으나, 이 DTO를 사용하는 다른 곳에서도 스트림 종료를 보장해야 합니다.

💡 불완전한 주석 제거
     public record UploadFile(
             String key,
-            InputStream inputStream, // 아래 내용들을.
+            InputStream inputStream,
             String contentType,
             Long size
     ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/kustacks/kuring/storage/application/port/in/dto/UploadFileCommand.java`
around lines 9 - 14, Remove the incomplete inline comment "// 아래 내용들을." from the
UploadFile record declaration and either replace it with a brief, meaningful
comment (e.g. noting that InputStream must be closed by the caller) or remove
the comment entirely; also add a short note in the Javadoc or comment for the
UploadFile record that InputStream lifecycle is the caller's responsibility and
callers (including StorageCommandService.upload()) must close the stream (e.g.
via try-with-resources) to avoid leaks, referencing the UploadFile record and
StorageCommandService.upload() so reviewers can verify all usages close the
stream.
src/main/java/com/kustacks/kuring/storage/adapter/in/event/dto/ClubCreateEvent.java (1)

25-38: InputStream을 record 필드로 저장하는 것은 주의가 필요합니다.

InputStream은 한 번만 읽을 수 있고 내부 상태가 변경되므로, record의 불변성 개념과 맞지 않습니다. PR 목표에서 언급된 대로 @Async 대신 AFTER_COMMIT 동기 처리를 선택한 것은 이해하지만, 다음 사항들을 확인해 주세요:

  1. 이벤트 리스너에서 InputStream이 정확히 한 번만 소비되는지
  2. 업로드 완료 후 InputStream이 적절히 close 되는지
  3. 이벤트가 여러 리스너에게 전달되지 않는지
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/kustacks/kuring/storage/adapter/in/event/dto/ClubCreateEvent.java`
around lines 25 - 38, ClubCreateImage currently stores an InputStream
(pathAndName, inputstream, contentType, size) which breaks record immutability
and risks double-consumption/ leaking; change ClubCreateImage to capture the
file contents as an immutable byte[] (or store the MultipartFile) instead of an
InputStream, read and close file.getInputStream() inside the ClubCreateImage
constructor, and update toUploadFile() to return UploadFileCommand.UploadFile
with a fresh ByteArrayInputStream(new byte[]) so consumers can safely read/close
independently; also audit usages of ClubCreateImage and listeners to ensure the
stream is consumed exactly once by converting any direct InputStream consumers
to use toUploadFile() which provides a fresh stream.
src/main/java/com/kustacks/kuring/club/application/port/out/ClubEventPort.java (1)

5-7: 포트 인터페이스에서 MultipartFile 사용은 Spring 프레임워크와 결합을 만듭니다.

헥사고날 아키텍처에서 포트 인터페이스는 프레임워크에 독립적인 것이 이상적입니다. MultipartFile 대신 byte[]InputStream을 사용하면 더 나은 추상화가 가능합니다.

다만 PR 설명에서 언급된 MultipartFile InputStream 수명 이슈로 인해 현재 설계를 선택한 것으로 보입니다. 향후 리팩토링 시 고려해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/kustacks/kuring/club/application/port/out/ClubEventPort.java`
around lines 5 - 7, The ClubEventPort interface currently depends on Spring's
MultipartFile via the method publishClubCreate in ClubEventPort; replace the
MultipartFile parameters with framework-agnostic types (e.g., byte[] or
InputStream) to decouple the port from Spring, update all
implementations/adapters that implement publishClubCreate to accept and
propagate the new types, and ensure any callers (controllers/adapters) convert
MultipartFile to the chosen byte[]/InputStream while handling stream lifecycle
and buffering concerns so input streams are consumed/closed before leaving the
adapter layer.
src/main/java/com/kustacks/kuring/club/application/service/ClubCommandService.java (1)

184-233: 이미지 파일의 실제 타입 검증을 추가하는 것이 좋습니다.

현재는 비어있는지/확장자 형식만 확인하므로, 비이미지 파일이 이미지로 업로드될 수 있습니다.

개선 예시
+    // 예: 허용 타입 화이트리스트
+    // private static final Set<String> ALLOWED_CONTENT_TYPES = Set.of("image/jpeg", "image/png", "image/webp");
+    // private static final Set<String> ALLOWED_EXTENSIONS = Set.of("jpg", "jpeg", "png", "webp");

     private void validateRequiredIconImage(MultipartFile iconImage) {
         if (iconImage == null || iconImage.isEmpty()) {
             throw new InvalidStateException(API_MISSING_PARAM);
         }
+        // validateImageFile(iconImage);
     }

+    // private void validateOptionalPosterImage(MultipartFile posterImage) { ... }
+    // private void validateImageFile(MultipartFile file) { ...contentType + extension 검사... }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/kustacks/kuring/club/application/service/ClubCommandService.java`
around lines 184 - 233, The image upload currently only checks emptiness and
filename extension (validateRequiredIconImage, extractExtension,
generateFileName) which allows non-image files; add actual image type validation
by reading the uploaded MultipartFile content (e.g., using ImageIO or checking
magic bytes) to confirm it decodes as a supported image format
(jpg/jpeg/png/gif) and determine the canonical extension; if decoding fails or
the detected format is not allowed, throw
InvalidStateException(API_INVALID_PARAM). Also update generateFileName to use
the detected canonical extension instead of the raw filename extension so stored
filenames reflect the real image type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminCommandApiV2.java`:
- Around line 145-147: Rename the multipart parameter to match the domain naming
to avoid missing poster uploads: change the `@RequestPart`(name = "postImage",
required = false) MultipartFile postImage parameter in AdminCommandApiV2 to use
"posterImage" (both the request part name and the local variable), and update
the call site that passes it into request.toCommand(iconImage, postImage) to
pass the new variable name instead (e.g., request.toCommand(iconImage,
posterImage)); ensure any other references in the same method or class are
updated accordingly.

In
`@src/main/java/com/kustacks/kuring/admin/adapter/out/event/ClubEventAdapter.java`:
- Around line 22-24: The poster image is being constructed with the wrong path:
in ClubEventAdapter when creating the ClubCreateImage for the poster you pass
iconImagePath instead of the poster's path variable; update the
ClubCreateImage(...) call used to build "poster" so it uses the poster image
path (the correct posterImagePath/variable) while keeping the same posterImage
content, then raise the ClubCreateEvent(clubId, icon, poster) unchanged.
- Around line 29-30: In the catch(IOException e) inside ClubEventAdapter (the
block that currently throws new
InvalidStateException(ErrorCode.FILE_IO_EXCEPTION)), preserve the original
exception by passing it as the cause to the InvalidStateException (or calling
initCause) so the IOException stacktrace is retained; update the
InvalidStateException construction to accept the cause (or add an appropriate
constructor) and include e when rethrowing alongside
ErrorCode.FILE_IO_EXCEPTION.

In
`@src/main/java/com/kustacks/kuring/club/application/port/in/ClubCreateAdminUseCase.java`:
- Around line 5-6: The use-case interface ClubCreateAdminUseCase currently
declares void createClub(AdminClubCreateCommand) so the created entity ID from
ClubCommandService.createClub (where savedClub is obtained) cannot be returned
to the controller; change the interface method signature to return either Long
or AdminClubCreateResponse (e.g., Long createClub(... ) or
AdminClubCreateResponse createClub(...)), update the implementing method
ClubCommandService.createClub to return the savedClub.getId() or build and
return an AdminClubCreateResponse, and propagate that return value back to the
controller so it passes the real clubId instead of null.

In
`@src/main/java/com/kustacks/kuring/club/application/port/in/dto/AdminClubCreateCommand.java`:
- Line 11: Change the nullable wrapper type for the isAlways field in
AdminClubCreateCommand from Boolean to the primitive boolean to enforce
non-nullability at the application layer; update the declaration and any
constructor/record component, accessors or usages within AdminClubCreateCommand
(and any places that construct it) to use primitive boolean, relying on the
`@NotNull` validation performed in AdminClubCreateRequest to guarantee a value.

In
`@src/main/java/com/kustacks/kuring/club/application/port/out/ClubCommandPort.java`:
- Around line 8-12: The interface ClubCommandPort is missing the
existsByNameAndDivision method used by ClubPersistenceAdapter; add a declaration
boolean existsByNameAndDivision(String name, ClubDivision division) to
ClubCommandPort (and import ClubDivision) so the persistence adapter can
implement it consistently alongside save(Club) and saveAll(List<ClubSns>);
alternatively, if you prefer separation of command vs query responsibilities,
move that method to a new or existing query port and update
ClubPersistenceAdapter to implement the appropriate port.

In
`@src/main/java/com/kustacks/kuring/club/application/service/ClubCommandService.java`:
- Around line 139-142: 추가 동시성 안전장치가 필요합니다: 엔티티의 (name, division) 컬럼에 DB 유니크 제약을
추가하고, 애플리케이션 레벨의 validateDuplicateClub(AdminClubCreateCommand, ClubDivision) 체크는
유지하되 저장 시 발생하는 제약 위반을 예외로 변환하도록 예외 처리를 추가하세요—즉 Club 엔티티에 복합 유니크 인덱스(또는 DDL 제약)를
선언하고, 실제 저장 호출을 수행하는 코드(예: ClubCommandService의 저장 메서드 또는
clubRepository.save/clubCommandPort.save 호출부)에서 DataIntegrityViolationException
같은 DB 제약 위반 예외를 잡아 InvalidStateException(CLUB_DUPLICATED)로 재던지도록 구현합니다.

In `@src/main/java/com/kustacks/kuring/club/domain/ClubSnsType.java`:
- Around line 17-23: fromUrl currently uses startsWith on the whole URL causing
false positives (e.g. "youtube" in malicious domains); change
ClubSnsType.fromUrl to parse the host via new URL(url).getHost() (handle
MalformedURLException by returning ETC) and compare that host against each
clubSnsType.urls entry using exact match or suffix match with a dot prefix (e.g.
host.equalsIgnoreCase(pattern) || host.endsWith("."+pattern)) instead of
startsWith; update the filtering lambda that currently references
clubSnsType.urls.stream().anyMatch(url::startsWith) to use the parsed host and
the safer equality/suffix checks.

In
`@src/main/java/com/kustacks/kuring/storage/adapter/in/event/dto/ImageDeleteEvent.java`:
- Around line 1-6: ImageDeleteEvent is currently unused; either remove the
record ImageDeleteEvent(String fileName) from the PR if not needed now, or if
you intend it for future work, add a clear comment/Javadoc above the
ImageDeleteEvent record explaining its intended future use and lifecycle and
optionally annotate/suppress unused warnings so static analysis doesn't flag it;
update the commit accordingly and ensure no imports/reference remain broken when
removing or annotating ImageDeleteEvent.

In
`@src/main/java/com/kustacks/kuring/storage/application/port/in/dto/UploadSingleImageCommand.java`:
- Line 6: The incomplete inline comment on the byte[] fileBytes parameter in
UploadSingleImageCommand is unclear; either remove the “// 아래 내용들을.” comment or
replace it with a concise, meaningful description of the parameter’s purpose
(e.g., "file bytes of the uploaded image" or "raw image data") to improve
readability and maintainability; update the JavaDoc or inline comment near the
UploadSingleImageCommand constructor/field to reflect the chosen wording.
- Line 6: Remove the leftover inline comment "// 아래 내용들을." from the record
component declaration in UploadSingleImageCommand and add a compact constructor
that makes a defensive copy of the byte[] fileBytes (e.g., allocate new
byte[fileBytes.length] and System.arraycopy) and assign the copy to
this.fileBytes (or validate null and copy accordingly) so external mutations
cannot affect the record's internal array; keep the rest of the record
unchanged.

---

Outside diff comments:
In `@src/test/java/com/kustacks/kuring/club/domain/ClubCategoryTest.java`:
- Around line 19-25: Rename the misnamed test method fromKorNameName to reflect
what's being tested: e.g., fromName (or fromEnglishName); update the test method
name that calls ClubCategory.fromName(name) and asserts equality with the
expected ClubCategory so the method name matches the tested function
ClubCategory.fromName and the English test data (academic, culture_art, etc.).
- Around line 29-39: The test method name has a typo "fromKorNameNameException"
causing a naming mismatch; rename the test method to a clear, consistent name
(e.g., "fromKorNameException" or "fromNameException") so it matches the other
tests, and keep the test body unchanged — locate the method in ClubCategoryTest
and update the method declaration name that wraps the ThrowingCallable calling
ClubCategory.fromName(name).

In `@src/test/java/com/kustacks/kuring/club/domain/ClubDivisionTest.java`:
- Around line 19-25: The test method name fromKorNameName does not match the
exercised method; update the test to match intent: either rename the test method
fromKorNameName to a descriptive name like fromNameTest (or
fromName_whenValidName_returnsDivision) to reflect that it calls
ClubDivision.fromName(name), or change the test body to call
ClubDivision.fromKorName(name) and adjust test data accordingly; locate the
method in class ClubDivisionTest and update the method name or the invoked
static method (ClubDivision.fromName vs ClubDivision.fromKorName) so name and
behavior are consistent.
- Around line 29-39: Test method name fromKorNameNameException mismatches the
behavior: it calls ClubDivision.fromName(name) but the name implies a "Kor"
variant; rename the test to reflect what's being tested (e.g., fromNameException
or fromNameThrowsNotFoundException) or change the call to
ClubDivision.fromKorName(name) to match the existing name; update the test
method name (fromKorNameNameException) or the invoked method
(ClubDivision.fromName(name)) so the test name and tested symbol align.

---

Nitpick comments:
In
`@src/main/java/com/kustacks/kuring/club/application/port/out/ClubEventPort.java`:
- Around line 5-7: The ClubEventPort interface currently depends on Spring's
MultipartFile via the method publishClubCreate in ClubEventPort; replace the
MultipartFile parameters with framework-agnostic types (e.g., byte[] or
InputStream) to decouple the port from Spring, update all
implementations/adapters that implement publishClubCreate to accept and
propagate the new types, and ensure any callers (controllers/adapters) convert
MultipartFile to the chosen byte[]/InputStream while handling stream lifecycle
and buffering concerns so input streams are consumed/closed before leaving the
adapter layer.

In
`@src/main/java/com/kustacks/kuring/club/application/service/ClubCommandService.java`:
- Around line 184-233: The image upload currently only checks emptiness and
filename extension (validateRequiredIconImage, extractExtension,
generateFileName) which allows non-image files; add actual image type validation
by reading the uploaded MultipartFile content (e.g., using ImageIO or checking
magic bytes) to confirm it decodes as a supported image format
(jpg/jpeg/png/gif) and determine the canonical extension; if decoding fails or
the detected format is not allowed, throw
InvalidStateException(API_INVALID_PARAM). Also update generateFileName to use
the detected canonical extension instead of the raw filename extension so stored
filenames reflect the real image type.

In
`@src/main/java/com/kustacks/kuring/storage/adapter/in/event/dto/ClubCreateEvent.java`:
- Around line 25-38: ClubCreateImage currently stores an InputStream
(pathAndName, inputstream, contentType, size) which breaks record immutability
and risks double-consumption/ leaking; change ClubCreateImage to capture the
file contents as an immutable byte[] (or store the MultipartFile) instead of an
InputStream, read and close file.getInputStream() inside the ClubCreateImage
constructor, and update toUploadFile() to return UploadFileCommand.UploadFile
with a fresh ByteArrayInputStream(new byte[]) so consumers can safely read/close
independently; also audit usages of ClubCreateImage and listeners to ensure the
stream is consumed exactly once by converting any direct InputStream consumers
to use toUploadFile() which provides a fresh stream.

In
`@src/main/java/com/kustacks/kuring/storage/adapter/in/event/StorageEventListener.java`:
- Line 11: 클래스에 붙은 사용되지 않는 `@Slf4j` 어노테이션을 정리하세요: StorageEventListener 클래스에서 로깅을
전혀 사용하지 않으면 `@Slf4j` 주석을 제거하고 불필요한 import를 삭제하거나, 이벤트 수신을 기록하려면
onApplicationEvent(또는 해당 이벤트 핸들러 메서드) 안에 수신 시점과 주요 페이로드(예: 이벤트 타입, id 등)를 기록하는
log.debug/info 호출을 추가해 주세요; 결정한 방식에 따라 `@Slf4j` 유지 시 log를 실제로 사용하거나, 사용하지 않으면
어노테이션과 lombok.slf4j.Slf4j import를 제거하십시오.

In
`@src/main/java/com/kustacks/kuring/storage/application/port/in/dto/UploadFileCommand.java`:
- Around line 9-14: Remove the incomplete inline comment "// 아래 내용들을." from the
UploadFile record declaration and either replace it with a brief, meaningful
comment (e.g. noting that InputStream must be closed by the caller) or remove
the comment entirely; also add a short note in the Javadoc or comment for the
UploadFile record that InputStream lifecycle is the caller's responsibility and
callers (including StorageCommandService.upload()) must close the stream (e.g.
via try-with-resources) to avoid leaks, referencing the UploadFile record and
StorageCommandService.upload() so reviewers can verify all usages close the
stream.

In
`@src/main/java/com/kustacks/kuring/storage/application/StorageCommandService.java`:
- Around line 29-43: The upload method in StorageCommandService currently
swallows failures and only logs them, so callers cannot know which files
succeeded or failed; change upload(UploadFileCommand.UploadFile file) to return
a result object (e.g., UploadResult with fields success:boolean, key:String,
error:String) or at minimum a boolean, populate it from the try/catch around
storagePort.upload, and propagate that result up to the caller so callers can
track successful vs failed files; alternatively (or additionally) emit a metric
or increment a failure counter (e.g., via MeterRegistry) inside the catch blocks
including file.key() and e.getMessage() to expose failures to monitoring.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e5e2d49 and de5ca50.

📒 Files selected for processing (34)
  • src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminCommandApiV2.java
  • src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminClubCreateRequest.java
  • src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminClubCreateResponse.java
  • src/main/java/com/kustacks/kuring/admin/adapter/out/event/ClubEventAdapter.java
  • src/main/java/com/kustacks/kuring/club/adapter/out/persistence/ClubPersistenceAdapter.java
  • src/main/java/com/kustacks/kuring/club/adapter/out/persistence/ClubRepository.java
  • src/main/java/com/kustacks/kuring/club/adapter/out/persistence/ClubSnsRepository.java
  • src/main/java/com/kustacks/kuring/club/application/port/in/ClubCreateAdminUseCase.java
  • src/main/java/com/kustacks/kuring/club/application/port/in/dto/AdminClubCreateCommand.java
  • src/main/java/com/kustacks/kuring/club/application/port/out/ClubCommandPort.java
  • src/main/java/com/kustacks/kuring/club/application/port/out/ClubEventPort.java
  • src/main/java/com/kustacks/kuring/club/application/port/out/ClubQueryPort.java
  • src/main/java/com/kustacks/kuring/club/application/service/ClubCommandService.java
  • src/main/java/com/kustacks/kuring/club/domain/Club.java
  • src/main/java/com/kustacks/kuring/club/domain/ClubCategory.java
  • src/main/java/com/kustacks/kuring/club/domain/ClubDivision.java
  • src/main/java/com/kustacks/kuring/club/domain/ClubSns.java
  • src/main/java/com/kustacks/kuring/club/domain/ClubSnsType.java
  • src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java
  • src/main/java/com/kustacks/kuring/common/exception/code/ErrorCode.java
  • src/main/java/com/kustacks/kuring/storage/adapter/in/event/StorageEventListener.java
  • src/main/java/com/kustacks/kuring/storage/adapter/in/event/dto/ClubCreateEvent.java
  • src/main/java/com/kustacks/kuring/storage/adapter/in/event/dto/ImageDeleteEvent.java
  • src/main/java/com/kustacks/kuring/storage/adapter/out/MockStorageAdapter.java
  • src/main/java/com/kustacks/kuring/storage/adapter/out/S3CompatibleStorageAdapter.java
  • src/main/java/com/kustacks/kuring/storage/application/StorageCommandService.java
  • src/main/java/com/kustacks/kuring/storage/application/port/in/StorageUploadUseCase.java
  • src/main/java/com/kustacks/kuring/storage/application/port/in/dto/UploadFileCommand.java
  • src/main/java/com/kustacks/kuring/storage/application/port/in/dto/UploadSingleImageCommand.java
  • src/main/resources/db/migration/V260227__Add_icon_image_path_to_club.sql
  • src/test/java/com/kustacks/kuring/acceptance/AdminAcceptanceTest.java
  • src/test/java/com/kustacks/kuring/acceptance/AdminStep.java
  • src/test/java/com/kustacks/kuring/club/domain/ClubCategoryTest.java
  • src/test/java/com/kustacks/kuring/club/domain/ClubDivisionTest.java

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/main/java/com/kustacks/kuring/admin/adapter/out/event/ClubEventAdapter.java (1)

29-30: ⚠️ Potential issue | 🟡 Minor

IOException 원인(e)을 함께 전달해 주세요.

현재는 예외 원인 스택이 유실됩니다. InvalidStateException에 cause를 넘겨서 장애 추적 가능성을 유지하는 게 좋습니다.

제안 수정
-        } catch (IOException e) {
-            throw new InvalidStateException(ErrorCode.FILE_IO_EXCEPTION);
+        } catch (IOException e) {
+            throw new InvalidStateException(ErrorCode.FILE_IO_EXCEPTION, e);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/kustacks/kuring/admin/adapter/out/event/ClubEventAdapter.java`
around lines 29 - 30, The IOException caught in ClubEventAdapter should be
propagated as the cause when throwing InvalidStateException so the original
stacktrace isn't lost; update the catch block that currently does "throw new
InvalidStateException(ErrorCode.FILE_IO_EXCEPTION);" to pass the caught
exception (e) as the cause via InvalidStateException's constructor or initCause
(e.g., new InvalidStateException(ErrorCode.FILE_IO_EXCEPTION, e)), ensuring
InvalidStateException's constructor supports a Throwable cause or add one if
needed.
🧹 Nitpick comments (1)
src/main/java/com/kustacks/kuring/admin/adapter/out/event/ClubEventAdapter.java (1)

22-27: 이벤트 발행 분기 중복을 줄이면 더 읽기 쉬워집니다.

Events.raise(...) 호출이 분기마다 반복되어 있어, 포스터만 먼저 계산하고 이벤트는 한 번만 발행하면 가독성이 좋아집니다.

리팩터링 예시
-            if (posterImage != null) {
-                ClubCreateImage poster = new ClubCreateImage(posterImagePath, posterImage);
-                Events.raise(new ClubCreateEvent(clubId, icon, poster));
-            } else {
-                Events.raise(new ClubCreateEvent(clubId, icon, null));
-            }
+            ClubCreateImage poster = null;
+            if (posterImage != null) {
+                poster = new ClubCreateImage(posterImagePath, posterImage);
+            }
+            Events.raise(new ClubCreateEvent(clubId, icon, poster));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/kustacks/kuring/admin/adapter/out/event/ClubEventAdapter.java`
around lines 22 - 27, posterImage 분기에 따라 Events.raise(...) 호출이 중복되어 있으니 먼저
posterImage를 기반으로 ClubCreateImage poster 변수를 계산(포스터가 없으면 null)하고 그 후 한 번만
Events.raise(new ClubCreateEvent(clubId, icon, poster))를 호출하도록 변경하세요; 관련 식별자:
posterImage, posterImagePath, ClubCreateImage, poster, Events.raise,
ClubCreateEvent, clubId, icon.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@src/main/java/com/kustacks/kuring/admin/adapter/out/event/ClubEventAdapter.java`:
- Around line 29-30: The IOException caught in ClubEventAdapter should be
propagated as the cause when throwing InvalidStateException so the original
stacktrace isn't lost; update the catch block that currently does "throw new
InvalidStateException(ErrorCode.FILE_IO_EXCEPTION);" to pass the caught
exception (e) as the cause via InvalidStateException's constructor or initCause
(e.g., new InvalidStateException(ErrorCode.FILE_IO_EXCEPTION, e)), ensuring
InvalidStateException's constructor supports a Throwable cause or add one if
needed.

---

Nitpick comments:
In
`@src/main/java/com/kustacks/kuring/admin/adapter/out/event/ClubEventAdapter.java`:
- Around line 22-27: posterImage 분기에 따라 Events.raise(...) 호출이 중복되어 있으니 먼저
posterImage를 기반으로 ClubCreateImage poster 변수를 계산(포스터가 없으면 null)하고 그 후 한 번만
Events.raise(new ClubCreateEvent(clubId, icon, poster))를 호출하도록 변경하세요; 관련 식별자:
posterImage, posterImagePath, ClubCreateImage, poster, Events.raise,
ClubCreateEvent, clubId, icon.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de5ca50 and bb6a1b8.

📒 Files selected for processing (2)
  • src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminCommandApiV2.java
  • src/main/java/com/kustacks/kuring/admin/adapter/out/event/ClubEventAdapter.java

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/test/java/com/kustacks/kuring/club/domain/ClubSnsTypeTest.java (1)

37-49: InvalidStateException 발생 경로에 대한 테스트 누락

현재 테스트 케이스들(빈 문자열, 공백, "not-a-url")은 URI.create()에서 예외를 발생시키지 않고 null 호스트로 처리되어 ETC를 반환합니다. 프로덕션 코드의 InvalidStateException 발생 경로(Line 44-45)를 검증하는 테스트가 없습니다.

✅ 예외 발생 케이스 테스트 추가 제안
+    `@DisplayName`("유효하지 않은 URI 형식은 예외를 발생시킨다")
+    `@Test`
+    void fromUrl_withMalformedUri_throwsException() {
+        assertThatThrownBy(() -> ClubSnsType.fromUrl("https://example.com/path with spaces"))
+                .isInstanceOf(InvalidStateException.class);
+    }

필요한 import 추가:

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import com.kustacks.kuring.common.exception.InvalidStateException;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/kustacks/kuring/club/domain/ClubSnsTypeTest.java` around
lines 37 - 49, The test suite is missing coverage for the code path that throws
InvalidStateException in ClubSnsType.fromUrl; add a unit test that invokes
ClubSnsType.fromUrl with a URL that causes URI.create() to throw and assert that
InvalidStateException is thrown (e.g., a clearly malformed URI), using
assertThatThrownBy to verify the exception type; update imports to include
static org.assertj.core.api.Assertions.assertThatThrownBy and
com.kustacks.kuring.common.exception.InvalidStateException and add the new test
method (or parameterized case) alongside fromUrl_withInvalidOrUnsupported to
validate the exception path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/com/kustacks/kuring/club/domain/ClubSnsType.java`:
- Around line 40-47: The extractHost(String url) method can NPE on url.trim()
when callers pass null (e.g., fromUrl(null)); add a null/blank guard at the
start of extractHost to validate the incoming url (check for null or empty after
trimming) and throw the same InvalidStateException(ErrorCode.API_INVALID_PARAM)
for invalid input; then proceed to URI.create(url.trim()) and return
uri.getHost() as before.

---

Nitpick comments:
In `@src/test/java/com/kustacks/kuring/club/domain/ClubSnsTypeTest.java`:
- Around line 37-49: The test suite is missing coverage for the code path that
throws InvalidStateException in ClubSnsType.fromUrl; add a unit test that
invokes ClubSnsType.fromUrl with a URL that causes URI.create() to throw and
assert that InvalidStateException is thrown (e.g., a clearly malformed URI),
using assertThatThrownBy to verify the exception type; update imports to include
static org.assertj.core.api.Assertions.assertThatThrownBy and
com.kustacks.kuring.common.exception.InvalidStateException and add the new test
method (or parameterized case) alongside fromUrl_withInvalidOrUnsupported to
validate the exception path.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb6a1b8 and aca5447.

📒 Files selected for processing (3)
  • src/main/java/com/kustacks/kuring/club/application/port/in/dto/AdminClubCreateCommand.java
  • src/main/java/com/kustacks/kuring/club/domain/ClubSnsType.java
  • src/test/java/com/kustacks/kuring/club/domain/ClubSnsTypeTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/kustacks/kuring/club/application/port/in/dto/AdminClubCreateCommand.java

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/main/java/com/kustacks/kuring/club/domain/ClubSnsType.java (1)

35-46: ⚠️ Potential issue | 🟠 Major

유효하지 않은 URL이 ETC로 우회될 수 있습니다.

Line 42의 URI.create(url.trim())는 빈 문자열/상대 URI에서 예외 없이 host == null을 반환할 수 있고, Line 37에서 그대로 ETC로 매핑됩니다. 또한 host 소문자 정규화가 없어 대문자 도메인 입력 시 오분류될 수 있습니다.

🔧 제안 수정안
 import java.net.URI;
+import java.util.Locale;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
@@
     private static String extractHost(String url) {
         try {
-            URI uri = URI.create(url.trim());
-            return uri.getHost();
-        } catch (IllegalArgumentException | NullPointerException e) {
-            throw new InvalidStateException(ErrorCode.API_INVALID_PARAM);
+            if (url == null || url.isBlank()) {
+                throw new InvalidStateException(ErrorCode.API_INVALID_PARAM);
+            }
+            URI uri = URI.create(url.trim());
+            String host = uri.getHost();
+            if (host == null || host.isBlank()) {
+                throw new InvalidStateException(ErrorCode.API_INVALID_PARAM);
+            }
+            return host.toLowerCase(Locale.ROOT);
+        } catch (IllegalArgumentException e) {
+            throw new InvalidStateException(ErrorCode.API_INVALID_PARAM, e);
         }
     }
In Java (JDK), for java.net.URI:
1) Does URI.create("instagram.com/path").getHost() return null without throwing?
2) Is URI#getHost case-normalized, or should callers manually lowercase for case-insensitive host matching?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/kustacks/kuring/club/domain/ClubSnsType.java` around lines
35 - 46, The current ClubSnsType.fromUrl / extractHost flow can silently treat
invalid or relative URLs (and uppercase hosts) as ETC because
URI.create(url).getHost() can return null; update extractHost(String url) to:
trim input, attempt to ensure a scheme (e.g., if url does not start with a
scheme, prepend "https://") before creating the URI, check uri.getHost() for
null and if null throw InvalidStateException(ErrorCode.API_INVALID_PARAM)
instead of returning null, and normalize the returned host to lowercase before
returning so HOST_TO_TYPE lookup is case-insensitive; keep the caller (fromUrl)
using HOST_TO_TYPE.getOrDefault(host, ETC) after these validations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/main/java/com/kustacks/kuring/club/domain/ClubSnsType.java`:
- Around line 35-46: The current ClubSnsType.fromUrl / extractHost flow can
silently treat invalid or relative URLs (and uppercase hosts) as ETC because
URI.create(url).getHost() can return null; update extractHost(String url) to:
trim input, attempt to ensure a scheme (e.g., if url does not start with a
scheme, prepend "https://") before creating the URI, check uri.getHost() for
null and if null throw InvalidStateException(ErrorCode.API_INVALID_PARAM)
instead of returning null, and normalize the returned host to lowercase before
returning so HOST_TO_TYPE lookup is case-insensitive; keep the caller (fromUrl)
using HOST_TO_TYPE.getOrDefault(host, ETC) after these validations.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aca5447 and 5eced2e.

📒 Files selected for processing (1)
  • src/main/java/com/kustacks/kuring/club/domain/ClubSnsType.java

@rlagkswn00
Copy link
Member Author

서비스 로직 수정사항이 확인되서 수정하고 다시 올리겠습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant